MapStruct 사용법 - g-market/b-shop-backend GitHub Wiki

MapStruct란?

  • Dto<->Entity간 객체 Mapping을 편하게 도와주는 라이브러리
  • 비슷한 라이브러리로 ModelMapper가 있다.
  • ModelMapper는 변환과정에서 리플랙션이 발생한다. MapStruct는 컴파일 시점에 구현체를 만들어내기 때문에 리플랙션이 발생하지 않아 보다 빠르다.
  • 리플랙션이란?

MapStruct 적용

1. dependency 추가

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

주의할 점은 MapSturct가 Lombok보다 뒤에 Dependency 선언이 되어야 한다. MapStruct는 Lombok의 getter, setter, builder를 이용하여 생성되므로 Lombok보다 먼저 선언되는 경우 정상적으로 실행할 수 없다.

2. 1.4.2.Final version 선택 이유

  • 안정성 높은 버전
  • Lombok 라이브러리 선언 순서에 따른 버그 fix
  • Collection Mapping의 strictness 완화
  • Builder 패턴 사용 클래스 개선

Mapstruct 사용법

1. 기본 매핑

@Mapper
public interface CarMapper {

    @Mapping(source = "make", target = "manufacturer")
    @Mapping(source = "numberOfSeats", target = "seatCount")
    CarDto carToCarDto(Car car);

    @Mapping(source = "name", target = "fullName")
    PersonDto personToPersonDto(Person person);
}
  • Car에 있는 make, numberOfSeats 변수를 CarDto의 manufacturer, seatCount로 매핑해서 변환해준다.

2. custom Mapping(default method)

@Mapper
public interface CarMapper {

    @Mapping(...)
    ...
    CarDto carToCarDto(Car car);

    default PersonDto personToPersonDto(Person person) {
        //hand-written mapping logic
    }
}
  • 사용자가 직접 매핑 메소드를 작성할 수도 있다.

3. dto안의 객체 변수 Mapping

   @Mapping(source = "item.id", target = "itemId")
   @Mapping(source = "option.id", target = "optionId")
   OrderItemDto orderItemToOrdersDto(OrderItem orderItem);
  • OrderItem의 Item객체의 id를 OrderItemDto의 itemId 변수에 매핑

4. dto안의 dto Mapping

   @Mappings({
      @Mapping(source = "orderItems", target = "orderItemDtoList")
   })
   OrderCreateResponseDto ordersCreateResponseDto(Orders orders);
  • Orders의 orderItemDtoList를 OrderCreateResponseDto의 orderItem에 매핑

5. annotation "." 사용법

 @Mapper
 public interface CustomerMapper {
     @Mapping( target = ".", source = "record" )
     @Mapping( target = ".", source = "account" )
     Customer customerDtoToCustomer(CustomerDto customerDto);
 }
  • customDto안의 record 객체의 모든 변수들을 Customer의 이름이 일치하는 변수들과 매핑해준다.

6. 이미 생성된 객체에 매핑

@Mapper
public interface UserInfoMapper {
 
    UserInfoMapper INSTANCE = Mappers.getMapper(UserInfoMapper.class);
 
    @Mapping(source = "user.name", target = "userName")
    @Mapping(source = "address.si", target = "si")
    @Mapping(source = "address.dong", target = "dong")
    void write(UserDto user, AddressDto address, @MappingTarget UserInfo userInfo);
}
  • 새로운 인스턴스를 생성하는 것이 아니라 기존에 이미 생성되어 있는 객체에 매핑이 필요한 경우 @MappingTarget 어노테이션을 사용해서 매핑

7. 타입 변환

@Mapper
public interface CarMapper {
 
    @Mapping(source = "price", numberFormat = "$#.00")
    CarDto carToCarDto(Car car);
 
    @IterableMapping(numberFormat = "$#.00")
    List<String> prices(List<Integer> prices);
}
  • @IterableMapping 어노테이션을 이용해 Integer pirce값을 String으로 변환
  • 날짜 데이터를 문자열로 변환하는 @dateFormat 어노테이션도 존재.

8. null 정책, 특정 필드 매핑 무시

 @Mapping(
            source = "description", 
            target = "description", 
            ignore = true,
            nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.SET_TO_DEFAULT
    )
    Car to(CarDto carDto);
  • SET_TO_DEFAULT로 설정하면, 빈 Target Object를 반환해준다.
  • ignore = true 옵션을 설정하면 해당 필드는 무시한다.

9. 매핑 전처리, 후처리

@Mapper(
        unmappedTargetPolicy = ReportingPolicy.ERROR,
        nullValueMappingStrategy = NullValueMappingStrategy.RETURN_NULL,
        componentModel = "spring"
)
public abstract class CarMapper {
 
    @BeforeMapping
    protected void setColor(CarDto carDto, @MappingTarget Car car) {
        if (carDto.getName().equals("bmw x4")) {
            car.setModelColor("red");
        } else {
            car.setModelColor("black");
        }
 
    }
 
    @Mapping(source = "name", target = "modelName")
    @Mapping(target = "modelColor", ignore = true)
    @Mapping(source = "price", target = "modelPrice", numberFormat = "$#.00")
    public abstract Car to(CarDto carDto);
 
    @AfterMapping
    protected void setDescription(@MappingTarget Car car) {
        car.setDescription(car.getModelName() + " " + car.getModelColor());
    }
}
 
<Generate Code>
 
@Generated(
    value = "org.mapstruct.ap.MappingProcessor"
)
@Component
public class CarMapperImpl extends CarMapper {
 
    @Override
    public Car to(CarDto carDto) {
        if ( carDto == null ) {
            return null;
        }
 
        Car car = new Car();
 
        setColor( carDto, car );
 
        car.setModelName( carDto.getName() );
        if ( carDto.getPrice() != null ) {
            car.setModelPrice( new DecimalFormat( "$#.00" ).format( carDto.getPrice() ) );
        }
        car.setDescription( carDto.getDescription() );
 
        setDescription( car );
 
        return car;
    }
}
  • 전처리 후처리를 위한 메소드는 private를 사용해서는 안된다. genarate된 코드에 전,후 처리 메소드가 들어가는 것이 아니라 추상 클래스에 있는 메소드를 그대로 사용하기 때문이다.

Mapstruct 공식 문서

1.4.2.Final version

⚠️ **GitHub.com Fallback** ⚠️