dependencies{
implementation 'org.mapstruct:mapstruct:1.4.2.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.4.2.Final'
}
@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);
}
위와 같은 Mapper를 생성했다면 MapStruct는 아래와 같은 구현체를 자동으로 생성해준다.
// GENERATED CODE
public class CarMapper implements CarMapper{
@Override
public CarDto carToCarDto(Car car){
if( car == null ) {
return null;
}
CarDto carDto = new CarDto();
if ( car.getFeatures() != null ){
carDto.setFeatures( new ArrayList<String> car.getFeatures() );
}
carDto.setManufacturer(car.getMake());
carDto.setSeatCount(car.getNumberOfSeats());
carDto.setDriver(personToPersonDto(car.getDriver()));
carDto.setPrice(String.valueOf(car.getPrice()));
if ( car.getCategory() != null ){
carDto.setCategory( car.getCategory().toString() );
}
carDto.setEngine( engineToEngineDto( car.getEngine() ) );
return carDto;
}
@Override
public PersonDto personDto(Person person){
//...
}
private EngineDto engineToEngineDto(Engine engine){
if ( engine == null ){
return null;
}
EngineDto engineDto = new EngineDto();
engineDto.setHorsePower(engine.getHorsePower());
engineDto.setFuel(engine.getFuel());
return engineDto;
}
}
@Mapper
public interface AddressMapper {
@Mapping(source = "person.description", target = "description")
@Mapping(source = "address.houseNo", target = "houserNumber")
DeliveryAddressDto personAndAddressToDeliveryAddressDto(Person person, Address address);
}
아래와 같이 source로 주어진 파라미터를 직접적으로 target의 속성에 매핑할 수도 있다.
@Mapper
public interface AddressMapper {
@Mapping(source = "person.description", target = "description")
@Mapping(source = "hn", target = "houseNumber")
DeliveryAddressDto personAndAddressToDeliveryAddressDto(Person person, Integer hn);
}
source에 포함된 bean 속성을 target에 매핑하는법
@Mapper
public interface CustomerMapper {
@Mapping(target = "name", source = "recored.name")
@Mapping(target = ".", source = "record")
@Mapping(target = ".", source = "account")
Customer customerDtoToCustomer(CustomerDto customerDto);
}
1) 인터페이스에 default 메소드로 커스텀 매핑을 추가하는 방법
@Mapper
public interface CarMapper {
@Mapping(...)
CarDto carToCarDto(Car car);
default PersonDto personToPersonDto(Person person){
//hand-written mapping logic
}
}
2) Mapper를 추상 클래스로 정의하는 방법
@Mapper
public abstract class CarMapper{
@Mapping(...)
public abstract CarDto carToCarDto(Car car);
public PersonDto personToPersonDto(Person person){
//hand-written mapping logic
}
}
@Mapper
public interface CarMapper{
void updateCarFromDto(CarDto carDto, @MappingTarget Car car);
}
target이 되는 Immutable한 Person 클래스
public class Person{
private final String name;
protected Person(Person.Builder builder){
this.name = builder.name;
}
public static Person.Builder builder(){
return new Person.Builder();
}
public static class Builder {
private String name;
public Builder name(String name){
this.name = name;
return this;
}
public Person create(){
return new Person(this);
}
}
}
Builder를 사용한 Mapper 구현체 예시
@Mapper
public interface PersonMapper{
Person map(PersonDto dto);
}
// GENERATED CODE
public class PersonMapperImpl implements PersonMapper{
public Person map(PersonDto dto){
if(dto == null){
return null;
}
Person.Builder builder = Person.builder();
builder.name(dto.getName());
return builder.create();
}
}
Mapper의 생성자 사용
만약 파라미터가 없는 기본 생성자 없이 여러 생성자가 존재할 경우 MapStruct는 어느 생성자를 사용할 지 판단할 수 없게된다. 이 경우에 컴파일 에러를 발생시킴
public class Vehicle {
protected Vehicle() { }
// MapStruct will use this constructor, because it is a single public constructor
public Vehicle(String color) { }
}
public class Car {
// MapStruct will use this constructor, because it is a parameterless empty constructor
public Car() { }
public Car(String make, String color) { }
}
public class Truck {
public Truck() { }
// MapStruct will use this constructor, because it is annotated with @Default
@Default
public Truck(String make, String color) { }
}
public class Van {
// There will be a compilation error when using this class because MapStruct cannot pick a constructor
public Van(String make) { }
public Van(String make, String color) { }
}
MapStruct가 생성한 구현체를 사용하기 위해선 두 가지 방식이 존재한다.
CarMapper mapper = Mappers.getMapper(CarMapper.class);
위와 같이 org.mapstruct.factory.Mappers 클래스를 사용하면 생성된 Mapper 인스턴스를 받아와 사용할 수 있다. MapStruct는 이 경우에 아래와 같은 관례로 작성하여 사용하길 권장
- 인터페이스 매퍼에 인스턴스를 선언
@Mapper
public interface CarMapper{
CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
CarDto carToCarDto(Car car);
}
@Mapper
public abstract class CarMapper{
public static final CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
CarDto carToCarDto(Car car);
}
이 패턴을 적용하면 mapper를 사용하기 위해 인스턴스를 새로 생성할 필요없이 싱글톤으로 생성된 인스턴스를 사용할 수 있다.
Car car = ...;
CarDto dto = CarMapper.INSTANCE.carToCarDto(car);
DI를 지원하는 프레임워크를 사용한다면 MapStruct로 구현된 매퍼들도 DI를 통해 사용할 수 있다.
Spring 예제
@Mapper(componentModel = "spring")
public interface CarMapper{
CarDto carToCarDto(Car car);
}
위와 같이 매퍼를 구성하면 Spring Context에 빈이 등록되기 때문에 아래와 같이 사용할 수 있다.
@Autowired
private CarMapper mapper;
@RequiredArgsConstructor
public class CarService{
private final CarMapper carMapper;
}