spring mapstruct

cornpip
|2023. 10. 15. 03:10

mapstruct

dto <-> entity 사이를 변환하는 코드를 생성해 주는 패키지다.

//build.gradle
dependencies {
...
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
    
	//mapstruct를 lombok 보다 뒤에
	implementation 'org.mapstruct:mapstruct:1.5.5.Final'
	annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'
...
}

 

주의

getter, setter, builder 등 lombok을 사용한다면 lombok 코드 생성이 먼저 실행된 후에 mapstruct가 동작해야 한다.

dependencies에서 mapstruct를 lombok 보다 뒤에 추가하면 lombok의 선행을 보장한다. ( dependencies 추가 순서가 실행 순서와 관련이 있는 듯하다. )

mapstruct를 lombok 보다 먼저 추가하면 종종 getter, setter 등의 코드 부재로 컴파일이 실패하는 걸 볼 수 있었다.

사용

//EntityMapper.java
public interface EntityMapper<E, D> {
    E toEntity(D dto);
    D toDto(E entity);
	
    // @BeanMapping(~) source가 null일 때 처리 방법
    // @MappingTarget 이 입력에 따라 수정될 객체
    // Target 쪽에 setter 필요
    @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
    void partialUpdate(D dto, @MappingTarget E entity);
}

위와 같이 공통적인 EntityMapper interface를 만들어두고 사용할 수도 있다.

entity <-> dto 변환 뿐 아니라 부분 수정에 대한 코드도 생성할 수 있다.

@MappingTarget이 수정될 객체이고 Target 쪽에는 setter가 필요하다.

//PostMapper.java
@Mapper(
        componentModel = "spring",
        unmappedTargetPolicy = ReportingPolicy.IGNORE
)
public interface PostMapper extends EntityMapper<Post, PostDto>{
}

기본적으로 @Mapper 어노테이션을 붙인 interface를 작성해 두면 컴파일 과정에서 Impl(구현체)를 생성한다.

componentModel = "spring" 으로 구현체를 bean으로 등록할 수 있다.

@Mapper(
        unmappedTargetPolicy = ReportingPolicy.IGNORE
)
public interface PostMapper extends EntityMapper<Post, PostDto>{
    PostMapper INSTANCE = Mappers.getMapper(PostMapper.class);
}

bean으로 등록하지 않으면 위처럼 사용한다.

수동 매핑

//ReportingPolicy.ERROR 면 일치하지 않는 필드 수동 매핑 전에 예외
@Mapper(
        componentModel = "spring",
        unmappedTargetPolicy = ReportingPolicy.IGNORE
)
public interface PostMapper2 extends EntityMapper<Post, PostDto2>{
    //필드 일치하지 않을 때 수동 매핑
    //source 가 input, target 이 output
    @Override
    @Mapping(source = "title", target = "title2")
    PostDto2 toDto(Post entity);
}

dto와 entity 필드가 일치하지 않는다면 수동 매핑이 필요하다.

위의 경우 entity title을 Dto title2에 매핑했다.

 

매핑 메서드 지정

매핑에 메서드를 지정할 수도 있다.

@Mapper(
        componentModel = "spring",
        unmappedTargetPolicy = ReportingPolicy.IGNORE
)
public interface ProgramResponseMapper {

    @Mapping(source = "exerciseCondition", target = "exerciseConditionId", qualifiedByName = "mapConditionId")
    public ProgramResponseDto toDto(ExerciseProgram program);

    @Named("mapConditionId")
    static Long mapConditionId(ExerciseCondition condition) {
        return condition.getId();
    }
}

entity의 exerciseCondition 필드를 responseDto의 exerciseConditionId 필드에 매핑하고 사용할 매핑 메서드는 mapConditionId 이다.

@Named 어노테이션으로 매핑 메서드 명에 대응하여 동작할 메서드를 지정한다.

 

+) 여러 필드 매핑, A Mapper 에서 B Mapper 사용

@Mapper(
        componentModel = "spring",
        unmappedTargetPolicy = ReportingPolicy.IGNORE,
        uses = {ImageResDtoMapper.class, userResDtoMapper.class}
)
public interface PostMapper extends EntityMapper<Post, PostDto>{
    @Override
    @Mappings({
        @Mapping(source = "image", target = "imageResDto"),
        @Mapping(source = "user", target = "userResDto")
    })
    PostDto toDto(Post entity);
}

uses에 사용할 다른 mapper 클래스를 넣는다.

source 객체와 target 객체가 등록한 mapper에 해당한다면, 자동으로 해당 mapper가 적용된다.

 

위 코드에서 별도의 매핑 메서드를 지정하지 않아도 image => imageResDto로 바꾸는 ImageResDtoMapper.toDto 가 자동 적용된다.

 

생성 코드

@Component
public class PostMapperImpl implements PostMapper {
    public PostMapperImpl() {
    }

    public Post toEntity(PostDto dto) {
        if (dto == null) {
            return null;
        } else {
            Post.PostBuilder post = Post.builder();
            post.title(dto.getTitle());
            post.subTitle(dto.getSubTitle());
            post.subscription(dto.getSubscription());
            return post.build();
        }
    }

    public PostDto toDto(Post entity) {
        if (entity == null) {
            return null;
        } else {
            PostDto.PostDtoBuilder postDto = PostDto.builder();
            postDto.title(entity.getTitle());
            postDto.subTitle(entity.getSubTitle());
            postDto.subscription(entity.getSubscription());
            return postDto.build();
        }
    }

    public void partialUpdate(PostDto dto, Post entity) {
        if (dto != null) {
            if (dto.getTitle() != null) {
                entity.setTitle(dto.getTitle());
            } else {
                entity.setTitle("");
            }

            if (dto.getSubTitle() != null) {
                entity.setSubTitle(dto.getSubTitle());
            } else {
                entity.setSubTitle("");
            }

            if (dto.getSubscription() != null) {
                entity.setSubscription(dto.getSubscription());
            } else {
                entity.setSubscription("");
            }

        }
    }
}

기본적으로 생성되는 코드는 다음과 같다.

꼭 builder가 아니어도 setter, allArgsConstruct 등 있는 lombok에 맞춰 유연하게 생성된다.

mapper 컴파일에 에러가 난다면 구현체를 만들기 위한 get/set/생성자 등이 적절하지 못했을 수 있다.

 

참고 자료

https://mapstruct.org/

 

MapStruct – Java bean mappings, the easy way!

Java bean mappings, the easy way! Get started Download

mapstruct.org

 

'Spring' 카테고리의 다른 글

Spring Stomp로 보는 직렬화/역직렬화  (0) 2024.07.05
Spring) Client와 WebSocket Server 사이를 중계하기  (1) 2023.12.04
spring lombok SuperBuilder  (0) 2023.10.08
spring mongodb repository  (0) 2023.10.01
lombok @Builder  (0) 2023.08.06