MapStruct의 bean mapper를 정의 하는 방법에 대해 설명하는 포스팅입니다.
Table of Contents
기본적인 맵핑 방법
간단하게 mapping 함수를 갖는 인터페이스와 Mapper 어노테이션을 이용하여 Mapper를 만들 수 있음
@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 코드 제너레이터가 build-time에 CarMapper 인터페이스의 구현체를 만듦
-
생성된 코드는 모든 source type(Car 객체)의 readable properties를 target type(Cardio 객체)에 일치하는 property에 복사
- source와 target에서 같은 이름을 갖는 property들은 암시적으로 맵핑
- source와 target에서 다른 이름을 갖는 property들은 @Mapping 어노테이션을 이용해 해당 properties의 이름들을 명시해주어야 함
좀 더 빠른 이해를 위해 실제 MapStruct의 코드 제너레이터를 통해 만들어진 CarMapper의 구현체를 보자
// GENERATED CODE
public class CarMapperImpl 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 personToPersonDto(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;
}
}
-
MapStruct의 철학은 최대한 개발자가 직접 손으로 작성한것과 같은 코드를 생성한다. (Reflection 사용 대신 평범한 getter/setter 함수의 호출을 이용해 mapping)
-
만일 source와 target property의 type이 다르다면 자동으로 conversion 해줌(위 예제의 price property) 또는, 다른 mapping method를 생성 후 호출(위 예제의 driver, engine property)
-
Collection Type property의 경우 element type이 같다면 새로운 collection 객체를 생성하여 값을 복사해주고, 다르다면 각각의 element들을 conversion한 뒤 새로운 collection 객체에 값 복사해준다.
맵퍼에 커스텀 메서드 추가하기
몇몇 케이스의 경우 MapStruct가 자동으로 코드를 생성할 수 없어, 사용자가 수동으로 구현을 해주어야 할 때가 있다. 이때 Java 8에서 도입된 default method, 혹은 abstract class를 사용하여 구현할 수 있다.
위의 예제에서 Person을 PersonDto로 맵핑하는 부분에 특별한 logic이 있어 MapStruct가 자동으로 코드를 생성해 주지 못한다고 가정해보자.
@Mapper
public interface CarMapper {
@Mapping(...)
...
CarDto carToCarDto(Car car);
default PersonDto personToPersonDto(Person person) {
//hand-written mapping logic
}
}
위의 예제와 같이 personToPersonDto를 인터페이스의 default method를 활용하여 구현해주면, carToCarDto()는 MapStruct에 의해 자동으로 코드를 구현해주고 내부적으로 car의 driver property를 Mapping할 때 수동으로 구현된 personToPersonDto()를 호출하여 Conversion 해준다.
default method뿐 아니라, abstract class를 구현하여 Custom Method를 구현할 수도 있다.
@Mapper
public abstract class CarMapper {
@Mapping(...)
...
public abstract CarDto carToCarDto(Car car);
public PersonDto personToPersonDto(Person person) {
//hand-written mapping logic
}
}
abstract class를 사용할 경우, CarMapper를 상속받아 구현하는 구현체를 자동 생성해주고 default method 사용과 마찬가지로 수동으로 구현된 personToPersonDto()를 호출하여 사용한다.