MapStruct 사용법 - g-market/b-shop-backend GitHub Wiki
- Dto<->Entity간 객체 Mapping을 편하게 도와주는 라이브러리
- 비슷한 라이브러리로 ModelMapper가 있다.
- ModelMapper는 변환과정에서 리플랙션이 발생한다. 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 패턴 사용 클래스 개선
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된 코드에 전,후 처리 메소드가 들어가는 것이 아니라 추상 클래스에 있는 메소드를 그대로 사용하기 때문이다.