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/생성자 등이 적절하지 못했을 수 있다.
참고 자료
'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 |