반응형

Custom Mapper with MapStruct

개요

MapStruct를 사용한 사용자 정의 매퍼 방법에 대하여 정리해 봅니다.

현재 실무에서 MapStruct 라이브러리를 활용해 Java Bean 유형 간의(DTO <> Domain) 매핑 구현에 사용하고 있습니다.

유사한 기능의 ModelMapper 라이브러리도 존재하지만 맵핑이 일어나는 시점에 리플렉션 API 방식으로 처리되기 때문에 B2C or 대용량 트래픽을 핸들링하는 사이트에서는 성능 이슈로 사용 하기에는 적절하지 않습니다. 반면에 MapStruct는 컴파일 시점에 구현체가 만들어지는 Code Generated 방식으로 성능상의 월등한 차이로 많이 활용하고 있습니다.

MapStruct 및 Bean Object Patten

일반적으로 Java 애플리케이션에서 POJO를 다른 POJO로 변환하는 로직을 흔히 볼 수 있습니다. 예를 들어, 도메인에서 가져오는 Entity와 클라이언트로 전달하는 DTO간에 일반적인 유형의 변환이 발생하는데, 이것을 손쉽게 해결하는 것이 MapStruct입니다. 뭐, 수동으로 Bean mapper를 만들 수도 있겠지만 시간이 많이 소요되고 노가다성 코드가 발생하게 됩니다. 그러나 MapStructs는 자동으로 Bean mapper Class를 생성할 수 있습니다.

Dependency

maven 에 아래 종속성을 추가해 줍니다.

MapStruct의 안정적인 최신 릴리스 버전은 maven central repository에서 사용할 수 있습니다.

Processor는 컴파일 시점에 구현체를 생성하는 역할을 담당하게 됩니다.

<dependency>
  <groupId>org.mapstruct</groupId>
  <artifactId>mapstruct</artifactId>
  <version>1.4.1.Final</version>
</dependency>
<dependency>
  <groupId>org.mapstruct</groupId>
  <artifactId>mapstruct-processor</artifactId>
  <version>1.4.2.Final</version>
</dependency>

MapStruct 활용

Entity 및 DTO 객체를 만들어 보겠습니다.

@Getter @Setter
public class Purchase {
    private String orderName;
    private String orderDescription;
}

@Getter @Setter
public class PurchaseDTO {
    private String orderName;
    private String orderDescription;
}

Mapper Interface

객체 맵핑을 위해 아래와 같은 인터페이스만 생성해 주면 됩니다. 그 이유는 구현체는 MapStruct-Processor 가 알아서 생성해 주기 때문입니다. 다만, 아래와 같은 인터페이스를 별도로 생성해주는 작업은 modelMapper에 비해 약간의 번거로움이 있습니다. 그럼에도 애플리케이션의 성능 향상을 위해서는 필수 요소라고 생각이 됩니다.

@Mapper
public interface PurchaseMapStruct {
  PurchaseMapStruct INSTANCE = Mappers.getMapper(PurchaseMapStruct.class);

  PurchaseDTO toPurchaseDto(Purchase entity);
}

Genernat 된 mapper

Maven clean install을 실행하면 /target/generated-sources/annotations/ 경로에 아래와 같은 구현체가 생성이 됩니다. MapStruct가 자동으로 생성해주는 클래스입니다.

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2022-10-28T16:52:20+0900",
    comments = "version: 1.4.2.Final, compiler: javac, environment: Java 1.8.0_282 (AdoptOpenJDK)"
)
public class PurchaseMapStructImpl implements PurchaseMapStruct {

    @Override
  public PurchaseDTO toPurchaseDto(Purchase dto) {
      if ( dto == null ) {
        return null;
    }

    PurchaseDTO purchaseDTO = new PurchaseDTO();
    purchaseDTO.setOrderName( dto.getOrderName() );
    purchaseDTO.setOrderDescription( dto.getOrderDescritpion() );

    return purchaseDTO;
    }
}    

Mapping Fields With Different Field Names

Enitity와 DTO 필드의 이름이 다른 경우에도 Java Bean 객체를 자동으로 매핑할 수 있습니다. 아래 예제를 보겠습니다.

@Getter @Setter
public class Purchase {
    private String ordNm;
  private String ordDesc;
  private String ordDate;
  private OrdItem ordItem;
}

@Getter @Setter
public class PurchaseDTO {
    private String orderName;
  private String orderDescription;
  private Date orderDate;
  Private OrderItem orderItem;
}

Mapper Interface 선언

다른 필드 이름을 매핑할 때 @Mapping 애너테이션을 추가하여 source와 target을 정의해 주면 됩니다.

@Mapper
public interface PurchaseMapStruct {
  PurchaseMapStruct INSTANCE = Mappers.getMapper(PurchaseMapStruct.class);

  @Mapping(target="orderName", source="entity.ordNm")
  @Mapping(target="orderDescription", source="entity.ordDesc")
  PurchaseDTO toPurchaseDto(Purchase entity);
}

Mapping With Type And Value Conversion

String Type의 날짜를 Date 객체로 변환할 수도 있습니다.

@Mapping(target="orderDate", source="entity.ordDate", dateFormat="yyyy-MM-dd HH:mm:ss")
PurchaseDTO toPurchaseDto(Purchase entity);

때로는 Value에 대한 커스터 마이징이 필요할 경우도 있습니다. Interface 내에 default method를 정의 하여 다른 빈에 대한 참조로 새로운 맵핑을 할 수도 있습니다.

@Mapping(target="orderDate", source="entity.orderDate", qualifiedByName="toOrderYear")
@Mapping(target="orderItem", source="entity.ordItem", qualifiedByName="toOrderItem")
PurchaseDTO toPurchaseDto(Purchase entity);

@Named("toOtderYear")
default String toOrderYear(String ordDate) {
    if(ordDate == null){
      return null;
  }

  return ordDate.substring(0,4);
}

@Named("toOrderItem")
default OrderItem toOrderItem(OrdItem item){
    if(item == null){
      return null;
  }

  OrderItem orderItem = new OrderItem();
  orderItem.setItemName(item.getItemNm());
  orderItem.setOrderPrice(item.getPrice());
  return orderItem;
}    

Service Layer 호출

Stream에서 map과 함께 entity to DTO 변환에 아래와 같이 적용 할 수 있습니다.

public List<PurchaseDTO> getPurchases() {
    return purchaseComponent.getPurchases(purchaseReq)
                          .stream()
                          .map(PurchaseMapStruct.INSTANCE::toPurchaseDto)
                          .collect(Collectors.toList());
}  

결론

MapStruct를 활용한 Java Bean 객체간의 전환에 대하여 기본적인 표현식과 다른 객체를 맵핑하는 방법에 대하여 소개하였습니다.

반응형

+ Recent posts