모르지 않다는 것은 아는것과 다르다.

Spring

MapStruct

채마스 2022. 4. 9. 12:32

MapStruct

  • Layer 간의 객체 변환 로직은 개발자가 직접 구현해도 되지만, 반복적이고 불필요한 코드가 많아지고, 단순한 실수로 인한 개발 생산성이 떨어지게 된다.
  • 이를 위해 많이 사용하는 매핑 라이브러리로 ModelMapper 가 있는데, MapStruct 를 사용하면 ModelMapper 와 비교해서 몇몇 장점을 가지고 있다.

 

MapStruct vs ModelMapper

  • ModelMapper 는 리플렉션 기반으로 동작하여 실제 매핑 로직을 쉽게 파악하기 어렵다.
  • MapStruct 는 코드 생성 방식으로 동작하기 때문에 생성된 코드를 통해 로직을 쉽게 파악할 수 있다.
  • 생성된 코드를 눈으로 보며 디버깅이 가능해서 로직 분석도 가능하고 수정도 가능하다.
  • MapStruct 는 컴파일 타임에 매핑 오류를 인지하고 설정에 따라 빌드 시 에러를 던질 수도 있다.
  • 성능적인 측면에서도 MapStruct 가 훨씬 더 좋다고 알려져 있다.

 

의존성 추가

implementation 'org.mapstruct:mapstruct:1.4.2.Final'
annotationProcessor "org.mapstruct:mapstruct-processor:1.4.2.Final"
annotationProcessor('org.projectlombok:lombok-mapstruct-binding:0.1.0')
  • 위와 같이 의존성을 추가해 준다.

 

구현

@Mapper(
        componentModel = "spring",
        injectionStrategy = InjectionStrategy.CONSTRUCTOR,
        unmappedTargetPolicy = ReportingPolicy.ERROR
)
public interface SrDtoMapper {

    Sr of(SrRegisterDto registerDto);
}
  • @Mapper 안에 설정 메타 정보를 지정한다.
  • 위와 같이 SrRegisterDto(source) 를 Sr(target) 로 매핑 시켜줄 수 있다.
  • 그렇게 되면 아래와 같이 변환 코드가 생성된다.
  • 물론 이 경우는 source 와 target 의 attribute 가 완전히 동일하다고 가정한 경우이다.
  • 다른 경우는 아래에서 알아 보도록 하자.
@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2022-04-02T01:13:45+0900",
    comments = "version: 1.4.2.Final, compiler: javac, environment: Java 16 (Oracle Corporation)"
)
@Component
public class SrDtoMapperImpl implements SrDtoMapper {

    @Override
    public Sr of(SrRegisterDto registerDto) {
        if ( registerDto == null ) {
            return null;
        }

        SrBuilder sr = Sr.builder();

        sr.sysNm( registerDto.getSysNm() );
        sr.jobGb( registerDto.getJobGb() );
        sr.reqGb( registerDto.getReqGb() );
        sr.state( registerDto.getState() );
        sr.sbjct( registerDto.getSbjct() );
        sr.dscr( registerDto.getDscr() );
        sr.reqDttm( registerDto.getReqDttm() );
        sr.cmplDttm( registerDto.getCmplDttm() );
        sr.srDetail( registerDto.getSrDetail() );
        sr.regTim( registerDto.getRegTim() );

        return sr.build();
    }
}

 

Source 와 target 의 type 은 동일하나 attribute 가 다른 경우

  • 이 경우에는 별도의 매핑 선언이 필요한 attribute 만 정의하면 된다.
  • 아래와 같이 Source (ItemDto.RegisterItemRequest) 에 해당하는 클래스가 있다고 가정하자
public class ItemDto {

    @Getter
    @Setter
    @ToString
    public static class RegisterItemRequest {
        private String partnerToken;
        private String itemName;
        private Long itemPrice;
        private List<RegisterItemOptionGroupRequest> itemOptionGroupList;
    }
}
  • 다음은 Target (ItemCommand.RegisterItemRequest) 에 해당하는 클래스를 정의해보자.
public class ItemCommand {

    @Getter
    @Builder
    @ToString
    public static class RegisterItemRequest {
        private final String itemName;
        private final Long itemPrice;
        private final List<RegisterItemOptionGroupRequest> itemOptionGroupRequestList; 
    }
}
  • 위에서 구현한 ItemDto.RegisterItemRequest -> ItemCommand.RegisterItemRequest 로 변환하려고 한다.
  • 하지만 itemName 과 itempPrice 와는 다르게 itemOptionGroupList 와 itemOptionGroupRequestList 는 명칭이 다르다 그렇기 때문에 아래와 같이 attribute 에 대한 정의가 필요하다.
@Mapper(
        componentModel = "spring",
        injectionStrategy = InjectionStrategy.CONSTRUCTOR,
        unmappedTargetPolicy = ReportingPolicy.ERROR
)
public interface SrDtoMapper {

    @Mappings({@Mapping(source = "request.itemOptionGroupList", target = "itemOptionGroupRequestList")})
    ItemCommand.RegisterItemRequest of(ItemDto.RegisterItemRequest request);

 

매핑 로직이 복잡해서 java expression 의 사용이 필요한 경우

@Mappings({
    @Mapping(source = "order.id", target = "orderId"),
    @Mapping(source = "order.deliveryFragment", target = "deliveryInfo"),
    @Mapping(expression = "java(order.calculateTotalAmount())", target = "totalAmount"),
    @Mapping(expression = "java(order.getStatus().name())", target = "status"),
    @Mapping(expression = "java(order.getStatus().getDescription())", target = "statusDescription")
})
OrderInfo.Main of(Order order, List<OrderItem> orderItemList);

 

매핑 대상 객체에 Collection 요소가 포함된 경우

  • 아래와 같이 매핑 대상 객체에 Collection 요소가 포함되었을 수 있다.
@Getter
@Builder
@ToString
public static class Main {
    private final Long orderId;
    private final Long totalAmount;
    private final ZonedDateTime orderedAt;
    private final List<OrderItem> orderItemList;
}
  • 이런 경우 OrderItem 에 대한 매핑 로직도 같이 선언해야 한다.
@Mappings({
    @Mapping(source = "orderedAt", target = "orderedAt", dateFormat = "yyyy-MM-dd HH:mm:ss")
})
OrderDto.Main of(OrderInfo.Main mainResult);

OrderDto.OrderItem of(OrderInfo.OrderItem orderItemResult);




REFERENCES

  • 이희창님의 Java/Spring 기반 서비스 개발

'Spring' 카테고리의 다른 글

JDK 동적 프록시  (0) 2022.08.06
ApplicationEventPublisher  (0) 2022.06.11
Handler Methods  (0) 2022.04.09
함수 기반 API 설계  (0) 2022.03.19
Spring Cache Abstraction  (0) 2022.03.19