MapStruct 정리

shane·2021년 10월 31일
2

MapStruct

MapStruct는 Object Mapping을 할 때 사용하는 라이브러리로 유사 라이브러리로는 ModelMapper라는 것이 존재한다.
주로 ModelMapperMapStruct는 Entity를 DTO로 변환하거나 DTO를 Entity로 변환할 때 사용한다.

왜 사용하는가?

주로 Entity에서 DTO로 변환할 때, Builder를 이용하여 객체를 생성한다. 하지만, 직접 builder를 이용한 메소드 선언방식의 경우 다음과 같은 문제가 발생한다.

  • 필드의 개수가 증가할수록 가독성 저해 발생
  • 반복적인 작업으로 인해 효율성이 저하
  • 코드 작성시 실수 발생의 가능성이 존재

생산성유지보수가 좋지 못하게 된다.

Maven 의존성 설정

의존성 추가

	<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>

Mapper Interface

Interface 설정에 앞서 여기서 예시로 사용될 Entity와 DTO를 작성하겠다.

Entity

@Getter
@Builder
@AllArgsConstructor
public class Test {
	private String name;
	private int score;
	private String grade;
	private String teacher;
	private List<Integer> problemNums;
}

DTO

@Getter
@Builder
@AllArgsConstructor
public class TestDto {
	private String name;
	private int score;
}

위에서 언급한 Entity와 DTO를 엮어서 Mapping을 진행해줄 Mapper용 Interface가 필요하다.

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의 필드 중에서 namescore만 사용하는 것을 볼 수 있는데, 나머지 Entity의 3개 필드를 무시하기 위해서는 target 값에 무시할 필드명과 ignore에 true를 주면 된다.

Test

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);
	}
}

테스트를 진행하면 정상적으로 통과하는 것을 알 수 있다.

번외

존재하지 않는 필드의 기본값 설정

Entity

@AllArgsConstructor
@Getter
@Builder
public class Order {
	private Long id;
	private String name;
	private String product;
	private Integer price;
	private String address;
	private LocalDateTime orderedTime;
}

DTO

@Getter
@AllArgsConstructor
@Builder
public class OrderDto {
	private String name;
	private String product;
	private Integer price;
	private String address;
	private String img;
	private LocalDateTime orderedTime;
}

Interface

@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 값을 넣어줄 수 있다.

Test

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");
	}
}

없는 필드를 추가하여 변환

변환시 한쪽에 없는 필드를 추가하고 싶을 수도 있다. 다음과 같이 사용한다.

Entity

@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();
}

DTO

@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;
}

Interface

@Mapper
public interface MessageMapper {
	MessageMapper INSTANCE = Mappers.getMapper(MessageMapper.class);
	
	MessageResult toMessageResult(Message message, String sender, int senderReplyCode, Collection<Exception> exceptions);
}

Entity에서 DTO로 변환할 때, 없는 필드가 추가로 필요할 수도 있다. 이 경우에는 변환시 사용하는 메소드에 기존 변환에 사용할 Entity 클래스, 추가로 필요한 필드들을 파라미터로 넣어주면 알아서 변환해준다.

Test

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);
	}
}
profile
개발 관련 소통을 좋아하는 백엔드 개발자입니다.

0개의 댓글