MapStruct는 Object Mapping을 할 때 사용하는 라이브러리로 유사 라이브러리로는 ModelMapper
라는 것이 존재한다.
주로 ModelMapper
나 MapStruct
는 Entity를 DTO로 변환하거나 DTO를 Entity로 변환할 때 사용한다.
주로 Entity에서 DTO로 변환할 때, Builder를 이용하여 객체를 생성한다. 하지만, 직접 builder를 이용한 메소드 선언방식의 경우 다음과 같은 문제가 발생한다.
생산성
및 유지보수
가 좋지 못하게 된다.
의존성 추가
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
플러그인 추가
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
Interface 설정에 앞서 여기서 예시로 사용될 Entity와 DTO를 작성하겠다.
@Getter
@Builder
@AllArgsConstructor
public class Test {
private String name;
private int score;
private String grade;
private String teacher;
private List<Integer> problemNums;
}
@Getter
@Builder
@AllArgsConstructor
public class TestDto {
private String name;
private int score;
}
위에서 언급한 Entity와 DTO를 엮어서 Mapping을 진행해줄 Mapper용 Interface가 필요하다.
@Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR)
public interface TestMapper {
TestMapper INSTANCE = Mappers.getMapper(TestMapper.class);
@Mapping(target = "grade", ignore = true)
@Mapping(target = "teacher", ignore = true)
@Mapping(target = "problemNums", ignore = true)
Test toTestEntity(TestDto dto);
}
Mapping을 진행해줄 Interface의 경우 선언후 @Mapper
어노테이션을 붙여줘야한다.
또한, 변환을 진행할 메소드에 @Mapping
어노테이션을 붙여준다.
위의 예시를 볼 경우 DTO에서는 Entity의 필드 중에서 name
과 score
만 사용하는 것을 볼 수 있는데, 나머지 Entity의 3개 필드를 무시하기 위해서는 target
값에 무시할 필드명과 ignore
에 true를 주면 된다.
public class TestMapperTest {
@Test
@DisplayName("property ignore")
void toTestEntity() {
//given
final TestDto dto = TestDto.builder().name("Test").score(80).build();
//when
final com.example.demo.entity.Test test = TestMapper.INSTANCE.toTestEntity(dto);
//then
assertNotNull(test);
assertThat(test.getName()).isEqualTo(dto.getName());
assertThat(test.getScore()).isEqualTo(dto.getScore());
assertThat(test.getGrade()).isEqualTo(null);
assertThat(test.getTeacher()).isEqualTo(null);
assertThat(test.getProblemNums()).isEqualTo(null);
}
}
테스트를 진행하면 정상적으로 통과하는 것을 알 수 있다.
@AllArgsConstructor
@Getter
@Builder
public class Order {
private Long id;
private String name;
private String product;
private Integer price;
private String address;
private LocalDateTime orderedTime;
}
@Getter
@AllArgsConstructor
@Builder
public class OrderDto {
private String name;
private String product;
private Integer price;
private String address;
private String img;
private LocalDateTime orderedTime;
}
@Mapper
public interface OrderMapper {
OrderMapper INSTANCE = Mappers.getMapper(OrderMapper.class);
@Mapping(target = "id", constant = "0L")
Order toOrder(OrderDto dto);
@Mapping(target = "img", expression = "java(order.getProduct() + \".jpg\")")
OrderDto toOrderDto(Order order);
}
OrderDTO
에는 Order
와는 달리 id
에 대한 필드가 없지만 기본값을 필요로 할 수도 있다. 이 경우에는 @Mapping
안에 target
에 필드명과 함께 constant
로 default 값을 넣어줄 수 있다.
public class OrderMapperTest {
@Test
@DisplayName("DTO to Entity")
void toOrder() {
//given
final OrderDto dto = OrderDto.builder()
.name("Test")
.product("Candy")
.price(200)
.address("Seoul")
.orderedTime(LocalDateTime.now())
.build();
//when
final Order order = OrderMapper.INSTANCE.toOrder(dto);
//then
assertNotNull(order);
assertThat(order.getName()).isEqualTo("Test");
assertThat(order.getProduct()).isEqualTo("Candy");
assertThat(order.getPrice()).isEqualTo(200);
assertThat(order.getAddress()).isEqualTo("Seoul");
assertThat(order.getOrderedTime()).isEqualTo(dto.getOrderedTime());
assertThat(order.getId()).isEqualTo(0L);
}
@Test
@DisplayName("Entity to DTO")
void toOrderDto() {
//given
final Order order = new Order(1L, "Test", "Candy", 1000, "Seoul", LocalDateTime.now());
//when
final OrderDto orderDto = OrderMapper.INSTANCE.toOrderDto(order);
//then
assertNotNull(orderDto);
assertThat(orderDto.getName()).isEqualTo("Test");
assertThat(orderDto.getProduct()).isEqualTo("Candy");
assertThat(orderDto.getPrice()).isEqualTo(1000);
assertThat(orderDto.getAddress()).isEqualTo("Seoul");
assertThat(orderDto.getOrderedTime()).isEqualTo(order.getOrderedTime());
assertThat(orderDto.getImg()).isEqualTo("Candy.jpg");
}
}
변환시 한쪽에 없는 필드를 추가하고 싶을 수도 있다. 다음과 같이 사용한다.
@Getter
@Builder
@AllArgsConstructor
public class Message {
private String id;
private String to;
private String title;
private String body;
private String messageType;
private String status = "READY";
private String statusMessage;
private OffsetDateTime createdDateTime = OffsetDateTime.now();
private OffsetDateTime updatedDateTime = OffsetDateTime.now();
}
@Getter
@Builder
@AllArgsConstructor
public class MessageResult {
private String id;
private String to;
private String title;
private String body;
private String messageType;
private String status;
private String statusMessage;
private OffsetDateTime createdDateTime;
private OffsetDateTime updatedDateTime;
private String sender;
private int senderReplyCode;
private Collection<Exception> exceptions;
}
@Mapper
public interface MessageMapper {
MessageMapper INSTANCE = Mappers.getMapper(MessageMapper.class);
MessageResult toMessageResult(Message message, String sender, int senderReplyCode, Collection<Exception> exceptions);
}
Entity
에서 DTO
로 변환할 때, 없는 필드가 추가로 필요할 수도 있다. 이 경우에는 변환시 사용하는 메소드에 기존 변환에 사용할 Entity
클래스, 추가로 필요한 필드
들을 파라미터로 넣어주면 알아서 변환해준다.
public class MessageMapperTest {
@Test
@DisplayName("Object + Object")
void toMessageResult() {
//given
final Message message =
new Message("Test", "Google", "plus test", "hello", "type", "Ready", "Ready"
, OffsetDateTime.now(), OffsetDateTime.now());
//when
final MessageResult result = MessageMapper.INSTANCE.toMessageResult(message, "Park", 200, null);
//then
assertNotNull(result);
assertThat(result.getId()).isEqualTo(message.getId());
assertThat(result.getTo()).isEqualTo(message.getTo());
assertThat(result.getTitle()).isEqualTo(message.getTitle());
assertThat(result.getBody()).isEqualTo(message.getBody());
assertThat(result.getMessageType()).isEqualTo(message.getMessageType());
assertThat(result.getStatus()).isEqualTo(message.getStatus());
assertThat(result.getStatusMessage()).isEqualTo(message.getStatusMessage());
assertThat(result.getCreatedDateTime()).isEqualTo(message.getCreatedDateTime());
assertThat(result.getUpdatedDateTime()).isEqualTo(message.getUpdatedDateTime());
assertThat(result.getSender()).isEqualTo("Park");
assertThat(result.getSenderReplyCode()).isEqualTo(200);
assertThat(result.getExceptions()).isEqualTo(null);
}
}