MapStruct?
안녕하세요~! ㅎㅎ 오늘은 Spring을 쓰면서 자주 쓰게 되는 라이브러리를 하나 소개할까 합니다!
바로 그것이 MapStruct 인데요!
이 Mapstruct란?
MapStruct is a code generator that greatly simplifies the implementation of mappings between Java bean types based on a convention over configuration approach.
------------------------------번역---------------------------------------
MapStruct는 구성 접근법에 대한 규약에 근거하여 Java Bean 종류 간의 매핑 구현을 크게 단순화한 code generator 이다.
홈페이지에 있는 소개글을 가져왔는데요,
읽으면 아시겠다 싶이 바로 DTO <-> Entity 간 객체 Mapping을 편하게 도와주는 라이브러리 입니다!
Spring을 많이 사용하신 분이라면 이런 생각이 들것 같아요
어라?
근데 modelMapper가 존재하지 않나요?
modelMapper는 분명 좋은 Mapper 클래스임에는 분명합니다!
하지만 이 글을 보신다면 생각이 바뀌어질 수도 있겠네요
객체 Mapping을 해주는 라이브러리에 대한 성능 비교를 한 글입니다!
요약하자면
- Mapstruct는 압도적으로 빠르다 ( mapstruct: 10^-5 ms, modelmapper: 0.002 )
- Throughput(처리량)이 그래서 엄청 빠르다 ( mapstruct : 37149, modelmapper: 439 ) // ms당 operation횟수
- 그냥 쓰자 너무 좋다
Dependecy 설정(의존성 설정)
크게는 2가지 방법이 있죠~!
먼저 maven설정입니다
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.3.1.Final</version>
</dependency>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.4.2.Final</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
mapstruct 라이브러리를 가져오고 어노테이션에 대한 설정도 가져와야합니다~!
하지만 저는 gradle을 더 좋아하기에 ㅎㅎ
dependencies {
implementation 'org.mapstruct:mapstruct:1.4.2.Final'
annotationProcessor "org.mapstruct:mapstruct-processor:1.4.2.Final"
// lombok 에서 mapstruct binding 해주는 기능이 추가되었습니다.!
annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0'
compileOnly 'org.projectlombok:lombok:1.18.22'
annotationProcessor 'org.projectlombok:lombok:1.18.22'
}
의존성 아래에 해당 스크립트를 사용하시면 됩니다~!
사용법
그럼 이제 어떻게 사용하는지 한번 알아볼까요?
먼저 예시를 위한 Entity와 DTO 클래스를 만들어보겠습니다!
package com.huisam.springstudy.mapstruct;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import java.time.LocalDateTime;
@AllArgsConstructor
@Getter
@Builder
@EqualsAndHashCode
public class Order {
private Long id;
private String name;
private String product;
private Integer price;
private String address;
private LocalDateTime orderedTime;
}
먼저 Entity 클래스입니다~!
간단하게 주문정보에 대한 객체를 만들어보았어요!
다음은 DTO 입니다!
package com.huisam.springstudy.mapstruct;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import java.time.LocalDateTime;
@AllArgsConstructor
@Builder
@Getter
class OrderDto {
private String name;
private String product;
private Integer price;
private String address;
private String img;
private LocalDateTime orderedTime;
}
아뿔싸 DTO 클래스에는 id(Long)이라는 필드가 없고, img(String)이라는 필드가 생겼네요!
그러면 우리가 일반적으로 mapping을 하기 위해서는
Order -> OrderDto : id필드를 제외, img필드를 추가 하고,
OrderDto -> Order : img필드를 제외, id필드를 추가해야겠네요!
아 참 그리고, 변환과정에서 꺼내오는 객체(source)에는 Getter가 있어야 하고,
변환해서 저장하고자 하는 객체(target)에는 Builder 혹은 Setter가 있어야 합니다~!
2021.11.27 기준 수정하여 추가합니다.
변환해서 저장하고자 하는 객체(target)에는 Builder 혹은 모든 필드를 담을 수 있는 생성자가 있어야 합니다~!
그럼 이를 매핑해주는 OrderMapper 를 만들어볼까요? ㅎㅎ
package com.huisam.springstudy.mapstruct;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
@Mapper // 1
public interface OrderMapper {
OrderMapper INSTANCE = Mappers.getMapper(OrderMapper.class); // 2
@Mapping(target = "id", constant = "0L") // 3
Order orderDtoToEntity(OrderDto orderDto);
@Mapping(target = "img", expression = "java(order.getProduct() + \".jpg\")") // 4
OrderDto orderToDto(Order order);
}
해당 주석의 순서대로 설명을 드릴게요~!
- @Mapper 어노테이션이 있어야 MapStruct을 활용할 수 있습니다~! 단, parameter는 정말 많으니 한번 스스로 확인해보세요!
- 해당하는 Instance가 OrderMapper를 상속받아서 OrderMapperImpl로 구현되게 될 것입니다! 저희가 만든 OrderMapper를 기반으로 메서드와 실제 로직들이 생성될 것이라고 명시하는 것이에요!
- 일반적인 경우에는 @Mapping 어노테이션만 붙이면 되요! 하지만 우리는 제외해야 되는 필드값이 있죠. 아까 말씀드린 orderDto -> order의 경우에는 id필드가 존재하기 때문에 이를 상수로 0L로 지정하겠다는 의미에요! 기본적으로 orderDto에만 있는 img필드는 꺼내오지 않고 매핑도 되지 않을껍니다!
- 반대의 경우인 order -> OrderDto 에는 img 필드를 채워야하기 때문에 expression을 기반으로 product.jpg로 만들겠다는 의미입니다!
그 외에도 @Mapping 어노테이션에는 다양한 parameter들이 있으니 직접 참고해보세요~!ㅎㅎ
그러면 저렇게 생성해 놓은 interface가 어떻게 되냐?
1. Builder 버젼
package com.huisam.springstudy.mapstruct;
import com.huisam.springstudy.mapstruct.Order.OrderBuilder;
import com.huisam.springstudy.mapstruct.OrderDto.OrderDtoBuilder;
import javax.annotation.processing.Generated;
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2020-03-25T12:58:19+0900",
comments = "version: 1.3.1.Final, compiler: javac, environment: Java 11.0.6 (AdoptOpenJDK)"
)
public class OrderMapperImpl implements OrderMapper {
@Override
public Order orderDtoToEntity(OrderDto orderDto) {
if ( orderDto == null ) {
return null;
}
OrderBuilder order = Order.builder();
order.name( orderDto.getName() );
order.product( orderDto.getProduct() );
order.price( orderDto.getPrice() );
order.address( orderDto.getAddress() );
order.orderedTime( orderDto.getOrderedTime() );
order.id( (long) 0L );
return order.build();
}
@Override
public OrderDto orderToDto(Order order) {
if ( order == null ) {
return null;
}
OrderDtoBuilder orderDto = OrderDto.builder();
orderDto.name( order.getName() );
orderDto.product( order.getProduct() );
orderDto.price( order.getPrice() );
orderDto.address( order.getAddress() );
orderDto.orderedTime( order.getOrderedTime() );
orderDto.img( order.getProduct() + ".jpg" );
return orderDto.build();
}
}
2. 생성자 버젼
package com.huisam.springstudy.mapstruct;
import java.time.LocalDateTime;
import javax.annotation.processing.Generated;
@Generated(
value = "org.mapstruct.ap.MappingProcessor"
)
public class OrderMapperImpl implements OrderMapper {
@Override
public Order orderDtoToEntity(OrderDto orderDto) {
if ( orderDto == null ) {
return null;
}
String name = null;
String product = null;
Integer price = null;
String address = null;
LocalDateTime orderedTime = null;
name = orderDto.getName();
product = orderDto.getProduct();
price = orderDto.getPrice();
address = orderDto.getAddress();
orderedTime = orderDto.getOrderedTime();
Long id = (long) 0L;
Order order = new Order( id, name, product, price, address, orderedTime );
return order;
}
@Override
public OrderDto orderToDto(Order order) {
if ( order == null ) {
return null;
}
String name = null;
String product = null;
Integer price = null;
String address = null;
LocalDateTime orderedTime = null;
name = order.getName();
product = order.getProduct();
price = order.getPrice();
address = order.getAddress();
orderedTime = order.getOrderedTime();
String img = order.getProduct() + ".jpg";
OrderDto orderDto = new OrderDto( name, product, price, address, img, orderedTime );
return orderDto;
}
}
그럼 실제로 이런식으로 알아서 코드를 작성해줍니다! ㅎㅎ
정말 편하고 좋죠~?
@어노테이션만 했을 뿐인데, 알아서 코드가 만들어진다니!!!
정말 같은지 테스트 해볼까요?
package com.huisam.springstudy.mapstruct;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertNotNull;
class OrderMapperTest {
@Test
@DisplayName("DTO에서 Entity로 변환하는 테스트")
void test_dto_to_event() {
/* given */
final OrderDto orderDto = OrderDto.builder()
.name("테스트")
.product("사탕")
.price(1000)
.address("Seoul")
.orderedTime(LocalDateTime.now())
.build();
/* when */
final Order order = OrderMapper.INSTANCE.orderDtoToEntity(orderDto);
/* then */
assertNotNull(order);
assertThat(order.getName()).isEqualTo("테스트");
assertThat(order.getProduct()).isEqualTo("사탕");
assertThat(order.getPrice()).isEqualTo(1000);
assertThat(order.getAddress()).isEqualTo("Seoul");
assertThat(order.getOrderedTime()).isBefore(LocalDateTime.now());
assertThat(order.getId()).isEqualTo(0L);
}
@Test
@DisplayName("Entity에서 DTO 변환하는 테스트")
void test_event_to_dto() {
/* given */
final Order order = new Order(1L, "테스트", "사탕", 1000, "Seoul", LocalDateTime.now());
/* when */
final OrderDto orderDto = OrderMapper.INSTANCE.orderToDto(order);
/* then */
assertNotNull(orderDto);
assertThat(orderDto.getName()).isEqualTo("테스트");
assertThat(orderDto.getProduct()).isEqualTo("사탕");
assertThat(orderDto.getPrice()).isEqualTo(1000);
assertThat(orderDto.getAddress()).isEqualTo("Seoul");
assertThat(orderDto.getOrderedTime()).isBefore(LocalDateTime.now());
assertThat(orderDto.getImg()).isEqualTo("사탕.jpg");
}
}
다 통과 되네요 ㅎㅎ
Why Mapstruct?
실제 거대한 시스템을 만들다보면, 다양한 도메인 영역들이 있고, 이를 변환하는 작업은 개발자에게 있어서
매우 귀찮고, 실수하기 딱 좋고, 번거로운 경우가 굉장히 많습니다
우리는 이를 실수하지 않게 검증된 라이브러리를 사용하는 것이죠!!
심지어 따른 Mapper 라이브러리보다도 훨씬 빠르다구요~~
다같이 MapStruct를 쓰면서 재밌게 Spring 개발에 임하셨으면 좋겠습니다!ㅎㅎ
유익하게 써주시면 좋을 것 같아요!!
참고 자료
MapStruct Docs
Mapstruct.org
'Developer > Spring' 카테고리의 다른 글
Spring AOP의 원리 - CGlib vs Dynamic Proxy (11) | 2020.08.18 |
---|---|
Spring에서 API 문서를 자동화하는 방법 - Spring REST Docs (0) | 2020.06.22 |
Spring Boot 에서 log를 남기는 방법 - Spring log 남기기 (4) | 2020.06.13 |
Spring Valid - 스프링에서 Reqeust Body를 Validation 하는 방법 (0) | 2020.05.08 |
Spring 이란? - Spring에 대한 소개 (1) DI / MVC의 관점에서 (0) | 2020.02.14 |