편의성 증가 & 유지보수성의 장점
한번에 많은 데이터를 옮겨 호출이 줄어들기에 효율적이다.
빌더패턴 예시
public Product toEntity() {
return Product.builder()
.name(name)
.description(description)
.price(price)
.stockQuantity(stockQuantity)
.build();
도메인 기능이 늘어나거나 변경될 때마다 매번 DTO 내부에 구현을 하는 것은 굉장히 비효율적이다.
이런 경우 속도가 빠른 외부 라이브러리를 사용할 수 있다.
엔티티 혹은 DTO 클래스 내부에서 변환을 실행하지 않고, 외부 Mapper 클래스를 만들어 사용한다.
public class ProductMapper {
public static Product toEntity(ProductRequest request) {
return Product.builder()
.name(request.getName())
.description(request.getDescription())
.price(request.getPrice())
.stockQuantity(request.getStockQuantity())
.build();
}
// Entity에서 DTO로의 변환 또는 다른 매핑 메서드도 추가 가능
}
Java에서 사용한 매퍼 라이브러리의 비교는 다음과 같다.
해당 데이터에 따르면, MapStruct가 굉장히 빠르게 작동하는 것을 알 수 있다.
스프링에서 코드 작업 시, 레이어간 이동에서 다양한 DTO가 생기게 된다.
계층 간 이동(Controller -> Service, Service -> Repository)으로 데이터가 이동하는 과정에서, 필드 간 차이가 많지 않다면 매핑 과정에서 노가다성 코드만 늘어나게 된다.
이런 상황에서 MapStruct를 통해 객체 내 많은 필드에 대해 쉽게 매핑할 수 잇다.
@Mapper(componentModel = "spring")
를 사용하면 스프링 빈으로 등록되어 의존성 주입(DI)가 가능하다.@Mapping
애노테이션을으로 처리할 수 있다.@Named
어노테이션을 통해 커스텀 로직 추가가 가능하다.target/generated-source/annotations
에 구현 클래스를 생성하므로, 디버깅 시 이를 확인하면 된다. // MapStruct
implementation 'org.mapstruct:mapstruct:1.5.5.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'
implementation 'org.projectlombok:lombok-mapstruct-binding:0.2.0'
annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0'
Lombok과 함께 사용하는경우, Lombok이 제대로 작동하지 않는 문제가 발생할 수 있다.
이러한 상황에서lombok-mapstruct-binding
을 dependency에 추가해 문제를 해결할 수 있다.
(dependency 순서에 따라 해당 문제가 발생하지 않을 수도 있다.)
인터페이스 또는 추상 클래스에 @Mapper
애노테이션을 붙임으로써, 손쉽게 Mapper를 만들 수 있다.
Mapper를 만들게 되면, 자동으로 MapperImpl이 생성되었음을 확인할 수 있다.
@Mapper
public interface CarMapper {
@Mapping(target = "manufacturer", source = "make")
@Mapping(target = "seatCount", srouce = "numberOfSeats")
CarDto carToCarDto(Car car);
@Mapping(target = "fullName", source = "name")
PersonDto personToPersonDto(Person person);
}
// 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를 활용해서 매피을 처리하다 보면, 잘못된 설정으로 인해 의도하지 않은 필드에 매핑되는 경우가 있다.
매핑 메서드를 생성한 이후에는 생성된 MapperImpl를 꼭 확인해서 의도한대로 모든 필드들이 잘 매핑되었는지 확인하는 것이 좋다.
Mapper를 생성할 때 Bean으로 지정하고자 한다면, @Mapper 애노테이션 지정 시 componentModel을 spring으로 지정하면 된다.
@Mapper(componentModel = "spring")
Mapper에서 매핑 시 Mapper 외부의 메서드를 사용하고자 하는 경우, @Mapper의 property 중 하나인 uses를 활용하여 다른 클래스를 지정할 수 있다. 여러 클래스를 활용하고자 한다면, 중괄호 {}를 통해 지정해야 한다.
@Mapper(uses=DataMapper.class)
public interface CarMapper {
CarDto carToCarDto(Car car);
}
@Service
public class ProductService {
private final ProductMapper mapper;
@Autowired
public ProductService(ProductMapper mapper) {
this.mapper = mapper;
}
public Product createProduct(ProductRequest request) {
return mapper.toEntity(request); // **
}
}