Back-End

Mapstruct 적용기

newny 2024. 5. 16. 22:44
반응형

Intro


Dto와 Entity를 쉽게 매핑하기 위해 MapStruct를 이용해 보았다.
Mapping 라이브러리를 사용하지 않다 보니 Entity 내부에 toDto() 메서드를 만들어야 하거나, Dto 내부에 toEntity() 메서드를 만들어야 하는 일이 발생했다.
단일 책임의 원칙에 위배된다는 생각이 들어 이번에는 MapStruct를 이용하여 매핑 과정을 Dto와 Entity에서 분리할 생각이다.
 
 
 
 

MapStruct란?


MapStruct는 Java bean 유형 간의 매핑 구현을 단순화하는 코드 생성기이다.
 
 
 
 

MapStruct의 장점


  • 컴파일 시점에 코드를 생성
    • 컴파일 시점에 코드가 생성되기 때문에 컴파일러가 코드를 검증하고 타입을 확인한다. 그렇기 때문에 런타임 오류를 방지할 수 있다. 그러므로 안정성을 보장한다고 할 수 있다.
  • 빠른 매핑 속도
    • 해당 링크에서 MapStruct와 다른 매퍼들의 속도 비교 테스트 결과를 볼 수 있다.
  • 반복되는 객체 매핑에서 발생될 수 있는 오류를 줄일 수 있고, 구현 코드를 자동으로 만들어주기 때문에 사용이 쉽다.

 
 
 
 

MapStruct 사용 방법


MapStruct는 어노테이션을 이용하여 매핑하는 방식을 사용하며, @Getter, @Setter, @Builder를 이용하기 때문에 Lombok 라이브러리를 의존성 추가가 선행되어야 한다.

1. 의존성 추가

dependencies {
    ...
    implementation 'org.mapstruct:mapstruct:1.5.5.Final'
    annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'
    testAnnotationProcessor "org.mapstruct:mapstruct-processor:1.5.3.Final"
    ...
}

 
 
2. 매핑할 Dto와 Entity 준비

@Table(name = "members")
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member extends BaseTimeEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String email;
    private String password;

    @Builder
    public Member(String email, String password) {
        this.email = email;
        this.password = password;
    }
}

 

@Getter
@NoArgsConstructor
public class MemberJoinRequestDto {
    private String email;
    private String password;
}

 
 
3. Mapper 인터페이스 생성

@Mapper
public interface MemberEntityMapper {

    MemberEntityMapper INSTANCE = Mappers.getMapper(MemberEntityMapper.class);

		// MemberJoinRequestDto(source)를 Member entity(target)로 매핑
    Member toEntity(MemberJoinRequestDto memberJoinRequestDto);
}

 
 
4. 구현 객체 자동 생성

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "",
    comments = "version: 1.5.5.Final, compiler: javac, environment: Java 17.0.6 (Oracle Corporation)"
)
public class MemberEntityMapperImpl implements MemberEntityMapper {

    @Override
    public Member toEntity(MemberJoinRequestDto memberJoinRequestDto) {
        if ( memberJoinRequestDto == null) {
            return null;
        }

        Member.MemberBuilder member = Member.builder();

        if ( memberJoinRequestDto != null ) {
            member.email( memberJoinRequestDto.getEmail() );
            member.password( memberJoinRequestDto.getPassword() );
        }
        return member.build();
    }
}

 
 
이와 같은 과정을 거쳐서 자동적으로 구현 객체가 생성이 된다. 다음은 내가 프로젝트에 적용한 MapStruct가 제공해 주는 기능이다.
 
 
 
 

매핑할 target 객체의 속성이 source 객체에 없는 경우


@Table(name = "members")
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member extends BaseTimeEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String email;
    private String password;
    private String mid;

    @Builder
    public Member(String email, String password, String mid) {
        this.email = email;
        this.password = password;
        this.mid = mid;
    }
}

 

@Getter
@NoArgsConstructor
public class MemberJoinRequestDto {
    private String email;
    private String password;
}

위의 예시 코드처럼 entity에는 mid라는 속성이 있는 반면 Dto에는 mid라는 속성이 존재하지 않는 경우이다.
 

@Mapper
public interface MemberEntityMapper {

    MemberEntityMapper INSTANCE = Mappers.getMapper(MemberEntityMapper.class);

		// MemberJoinRequestDto를 Member entity로 매핑
    Member toEntity(MemberJoinRequestDto memberJoinRequestDto);
}

위의 코드로 작성하게 되면 Member의 mid 속성을 어떤 것과 매핑해 줄 건지를 묻는 경고가 뜬다.
 

@Mapper
public interface MemberEntityMapper {

    MemberEntityMapper INSTANCE = Mappers.getMapper(MemberEntityMapper.class);

    Member toEntity(MemberJoinRequestDto memberJoinRequestDto, String mid);
}

MapStruct의 경우 메서드의 리턴타입의 속성이 target이고 파라미터로 들어오는 속성들이 source이므로 현재 Member의 mid와 매핑되는 source가 없으므로 mid 파라미터로 추가해 준다. 위의 예시는 두 속성의 네임이 같기에 @Mapping 어노테이션을 사용하지 않았는데, 만약 매핑하는 속성들의 이름이 다르다면 @Mapping 어노테이션을 이용하여 source와 target을 매핑해 줘야 한다.
 
 
 
 

사용자 정의 Mapper (default 메서드)


위의 예시에서 Member 속성의 mid는 난수인 UUID를 갖는다. UUID를 서비스 코드에서 생성하여 매핑할 때 파라미터로 넣어줘도 되지만, MapStruct에서 제공하는 다른 방법이 있다고 하여 해당 방법을 이용해 보았다.
MapStruct의 공식 문서를 살펴보니 사용자 정의 Mapper(default 메서드)로 해결이 가능하다고 한다. 그래서 그대로 적용해 보았다.

public interface MemberEntityMapper {

    MemberEntityMapper INSTANCE = Mappers.getMapper(MemberEntityMapper.class);

    Member toEntity(MemberJoinRequestDto memberJoinRequestDto, String mid);

    default String generateUuid(String value) {
        return (value == null)
            ? UUID.randomUUID().toString().replace("-", "")
            : value;
    }
}

 

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "",
    comments = "version: 1.5.5.Final, compiler: javac, environment: Java 17.0.6 (Oracle Corporation)"
)
public class MemberEntityMapperImpl implements MemberEntityMapper {

    @Override
    public Member toEntity(MemberJoinRequestDto memberJoinRequestDto) {
        if ( memberJoinRequestDto == null) {
            return null;
        }

        Member.MemberBuilder member = Member.builder();

        if ( memberJoinRequestDto != null ) {
            member.email( generateUuid( memberJoinRequestDto.getEmail() ) );
            member.password( generateUuid( memberJoinRequestDto.getPassword() ) );
        }
        member.mid( generateUuid( mid ) );
        
        retu~~r~~n member.build();
    }
}

위와 같이 생성이 됐다. 잘 적용이 되었으나 사용되지 않아도 될 속성에까지 사용자 정의 Mapper가 적용된 것을 확인할 수 있다. 모든 속성들을 매핑할 때마다 해당 메서드가 실행되는 건 비 효율적이라는 생각이 들어서 다른 방법을 찾아보았다.
 
 
 
 

한정자(Qualifier)를 이용한 Mapping


공식 문서에 따르면 한정자(Qualifier)를 이용한 Mapping 방식이 있다. 그중 하나가 @Named 어노테이션을 사용하는 방법인데, 해당 어노테이션은 사용자 정의 Mapper에 이름을 부여하는 메서드이다. @Named 어노테이션을 이용해 이름을 부여하게 되면 해당 메서드를 원하는 곳에 지정하여 사용 가능하다.
이름이 부여된 사용자 정의 Mapper의 사용 방법은 @Mapping 어노테이션의 qualifiedByName 속성을 이용하는 방법이 있다. 아래 코드대로 작성하면 특정 속성에만 사용자 정의 매퍼를 적용할 수 있다.

public interface MemberEntityMapper {

    MemberEntityMapper INSTANCE = Mappers.getMapper(MemberEntityMapper.class);

    @Mapping(target = "mid", qualifiedByName = "generateUuid")
    Member toEntity(MemberJoinRequestDto memberJoinRequestDto, String mid);

    @Named("generateUuid")
    default String generateUuid(String value) {
        return (value == null)
            ? UUID.randomUUID().toString().replace("-", "")
            : value;
    }
}

 

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "",
    comments = "version: 1.5.5.Final, compiler: javac, environment: Java 17.0.6 (Oracle Corporation)"
)
public class MemberEntityMapperImpl implements MemberEntityMapper {

    @Override
    public Member toEntity(MemberJoinRequestDto memberJoinRequestDto) {
        if ( memberJoinRequestDto == null) {
            return null;
        }

        Member.MemberBuilder member = Member.builder();

        if ( memberJoinRequestDto != null ) {
            member.email( memberJoinRequestDto.getEmail() );
            member.password( memberJoinRequestDto.getPassword() );
        }
        member.mid( generateUuid( mid ) );
        
        return member.build();
    }
}

 

 
 
 
 

결론


MapStruct를 이용하니 매핑 코드를 일일이 적을 필요가 없어서 굉장히 유용했다. 사용방법도 어렵지 않아서 이후 프로젝트에서도 계속 사용할 생각이다.
 
 
 
 

참고

MapStruct – Java bean mappings, the easy way!
편리한 객체 간 매핑을 위한 MapStruct 적용기 (feat. SENS)

반응형