본문 바로가기

항해 99/Spring

MapStruct

MapStruct

Java bean 유형 간의 매핑 구현을 단순화하는 코드 생성기

 

특징

  • 컴파일 시점에 코드를 생성하여 런타임에서 안정성을 보장함
  • 다른 매핑 라이브러리보다 속도가 빠릅니다
  • 반복되는 객체 매핑에서 발생할 수 잇는 오류를 줄일 수 있으며, 구현 코드를 자동으로 만들어주기 때문에 사용이 쉬움
  • Annotation processor를 이용하여 객체 간 매핑을 자동으로 제공함
  • 다만, Lombok 라이브러리에 먼저 dependency(의존성) 추가가 되어있어야 함, MapStruct는 Lombok의 getter, setter, builder를 이용하여 생성되므로 Lombok보다 먼저 의존성이 선언된 경우 실행할 수 없음

MapStruct 사용 방법

1. 기본 사용방법

MapStruct를 사용하기 위해서는 먼저 dependency(의존성) 추가가 필요함

dependencies {
    ...
    implementation 'org.mapstruct:mapstruct:1.5.3.Final'
    annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.3.Final'
    ...
}

 

주의할 점은 MapStruct가 Lombok보다 뒤에 dependency 선언이 되어야 함(MapStruct는 Lombok의 getter, setter, builder를 이용하여 생성되므로 Lombok보다 먼저 선언되는 경우 정상적으로 실행할 수 없음)

 

API를 통해 메세지를 보낼 body를 받았다고 가정, 해당 메세지는 RequestDto에 담겨져 있음

public class RequestDto {
        private String title;
        private String content;
        private String sender;
        private List<String> receiver;
        private LocalDateTime requestTime;
        private String type;
}

 

RequestDto에 담긴 내용을 MessageBodyDto에 매핑하려고 함

public class MessageBodyDto {
        private String title;
        private String content;
        private String sender;
        private List<String> receiver;
        private LocalDateTime requestTime;
        private String type;
}

 

매핑을 위한 Interface를 만들어 준다

@Mapper
public interface MessageMapper {
	MessageMapper INSTANCE = Mappers.getMapper(MessageMapper.class);

         // RequestDto -> MessageBodyDto 매핑
	MessageBodyDto toMessageBodyDto(RequestDto requestDto);
}

 

Mapper 인터페이스에 @Mapper 어노테이션을 붙이면 MapStruct가 자동으로 MessageMapper의 구현체를 생성해 줌

 

위에서 사용된 MessageMapper INSTANCE = Mappers.getMapper(MessageMapper.class)는 매퍼 클래스에서 MessageMapper를 찾을 수 있도록 하는 방법임

매퍼 interface에서 위와 같이 Instance를 선언해주면 매퍼에 대한 접근이 가능함

 

매핑하려는 객체는 필드값이 동일하기 때문에, 구현 코드를 작성 또는 수정하지 않고 쉽게 매핑할 수 있음

 

자동으로 생성되는 구현체는 아래와 같음

public class MessageMapperImpl implements MessageMapper {

    @Override
    public MessageBodyDto toMessageBodyDto(RequestDto requestDto) {
        if ( requestDto == null ) {
            return null;
        }

        MessageBodyDto.MessageBodyDtoBuilder messageBodyDto = MessageBodyDto.builder();

        messageBodyDto.title( requestDto.getTitle() );
        messageBodyDto.content( requestDto.getContent() );
        messageBodyDto.sender( requestDto.getSender() );
        List<String> list = requestDto.getReceiver();
        if ( list != null ) {
            messageBodyDto.receiver( new ArrayList<String>( list ) );
        }
        messageBodyDto.requestTime( requestDto.getRequestTime() );
			  messageBodyDto.requestType( requestDto.getRequestType() );

        return messageBodyDto.build();
    }
}

 

매퍼를 위한 interface만 만들면, 매핑이 필요한 객체에 대해 자동으로 구현체를 만들어줌, 위 구현체는 빌드 시 build/classes/java/main/ 에 매핑 인터페이스가 위치한 곳에 만들어지게 됨

 

2. 매핑에 여러 객체가 필요한 경우

여러 객체를 하나의 객체에 매핑하는 경우

PageDto와 위에 사용된 RequestDto를 MessageServiceDto에 매핑

public class PageDto {
	private Integer pageIndex;
	private Integer pageCount
}
public class MessageServiceDto {
  private String title;
	private String content;
	private String sender;
	private List<String> receiver;
	private String type;
	private Integer pageIdx;
	private Integer pageCnt;
}
public interface MessageMapper {
	MessageMapper INSTANCE = Mappers.getMapper(MessageMapper.class);

         //PageDto, RequestDto -> MessageServiceDto 매핑
	@Mapping(source="pageDto.pageIndex", target="pageIdx")
        @Mapping(source="pageDto.pageCount", target="pageCnt")
	MessageServiceDto toMessageServiceDto(PageDto pageDto, RequestDto requestDto);
}

 

매핑하려는 모든 컬럼들이 같다면 별도의 어노테이션으로 표시할 필요가 없지만, 만약 지정해야 하는 경우가 있다면 예시처럼 @Mapping을 이용하여 source에는 매핑값을 가지고 올 대상, target에는 매핑할 대상을 각각 작성해준다

 

이렇게 코드를 작성하면 매핑할 필드명이 다르거나, 두 객체 간 같은 필드가 있어도 특정 필드를 지정하여 매핑할 수 있음

 

구현체는 아래와 같이 자동으로 만들어짐

 @Override
    public MessageServiceDto toMessageServiceDto(PageDto pageDto, RequestDto requestDto) {
        if ( pageDto == null && requestDto == null ) {
            return null;
        }

        MessageServiceDto.MessageServiceDtoBuilder messageServiceDto = MessageServiceDto.builder();

        if ( pageDto != null ) {
            messageServiceDto.pageIdx( pageDto.getPageIndex() );
            messageServiceDto.pageCnt( pageDto.getPageCount() );
        }
        if ( requestDto != null ) {
            messageServiceDto.title( requestDto.getTitle() );
            messageServiceDto.content( requestDto.getContent() );
            messageServiceDto.sender( requestDto.getSender() );
            List<String> list = requestDto.getReceiver();
            if ( list != null ) {
                messageServiceDto.receiver( new ArrayList<String>( list ) );
            }
            messageServiceDto.type( requestDto.getType() );
        }

        return messageServiceDto.build();
    }

 

3. 매핑에 여러 파라미터가 필요한 경우

RequestDto와 여러 가지 다른 인자값이 MessageListServieDto에 매핑되어야 하는 경우

public class MessageListServiceDto {
	private String messageId;
	private Integer count;
	private String title;
	private String content;
	private String sender;
	private List<String> receiver;
	private LocalDateTime requestTime;
}

 

Mapper Interface를 작성할 때 위에서 작성한 것처럼 작성하되, 필요한 파라미터를 추가로 작성

public interface MessageMapper {
	MessageMapper INSTANCE = Mappers.getMapper(MessageMapper.class);

         //messageId, count, requestDto -> MessageServiceDto 매핑
	MessageListServiceDto toMessageListServiceDto(String messageId, Integer count, RequestDto requestDto);
}

 

MapStruct에서 파라미터를 포함하여 매핑하는 구현체를 자동으로 만들어줌

 @Override
    public MessageListServiceDto toMessageListServiceDto(String messageId, Integer count, RequestDto requestDto) {
        if ( messageId == null && count == null && requestDto == null ) {
            return null;
        }

        MessageListServiceDto.MessageListServiceDtoBuilder messageListServiceDto = MessageListServiceDto.builder();

        if ( requestDto != null ) {
            messageListServiceDto.title( requestDto.getTitle() );
            messageListServiceDto.content( requestDto.getContent() );
            messageListServiceDto.sender( requestDto.getSender() );
            List<String> list = requestDto.getReceiver();
            if ( list != null ) {
                messageListServiceDto.receiver( new ArrayList<String>( list ) );
            }
            messageListServiceDto.requestTime( requestDto.getRequestTime() );
        }
        messageListServiceDto.messageId( messageId );
        messageListServiceDto.count( count );

        return messageListServiceDto.build();
    }

 

4. 추가 매핑 방법 with Custom

4-1 매핑 시 default 값 지정

source 객체에 빈 값이 들어오는 경우, NPE를 피하기 위해, 특정 default 값이 지정되어야 하는 경우 등의 상황에서 defaultValue와 defaultExpression을 이용해서 default 값을 지정할 수 있음

 

RequestDto를 MessageListServiceDto에 매핑할 때, messageId가 null 인 경우 UUID 값을 default 값으로 채워주는 예시

@Mapper(imports = UUID.class)
public interface MessageMapper {
	MessageMapper INSTANCE = Mappers.getMapper(MessageMapper.class);

        @Mapping(source = "messageId", target = "messageId", defaultExpression = "java(UUID.randomUUID().toString())")
        @Mapping(source = "requestDto.type", target = "type", defaultValue = "SMS")
        @Mapping(source = "requestDto.sender", target="sender", ignore=true)
        MessageListServiceDto toMessageListServiceDto(String messageId, Integer count, RequestDto requestDto);
}

 

4-2 매핑 시 특정 필드 매핑 무시

특정 필드를 빼고 매핑해야 하는 경우 ignore를 사용해서 제외할 수 있음

@Mapper(imports = UUID.class)
public interface MessageMapper {
	MessageMapper INSTANCE = Mappers.getMapper(MessageMapper.class);

        @Mapping(source = "requestDto.sender", target="sender", ignore=true)
	MessageListServiceDto toMessageListServiceDto(String messageId, Integer count, RequestDto requestDto);
}

 

4-3 특정 필드 매핑 시 지정 메서드 이용

이외에 별도 메소드를 매핑에 이용한 방법

MessageListServiceDto의 type이 아래와 같이 enum으로 변경되고, RequestDto가 MessageListServiceDto에 매핑될 때 type의 데이터 타입이 enum으로 변경되어야 한다고 가정

public class MessageListServiceDto {
   private String messageId;
   private Integer count;
   private String title;
   private String content;
   private String sender;
   private List<String> receiver;
   private LocalDateTime requestTime;
   private Type type;
}
public enum Type {
	SMS("SMS"),
	LMS("LMS"),
	MMS("MMS");

	private String code;

	@Override
	public String toString(){
		return this.code;
	}
}

 

enum class가 위와 같고, mapper에 enum으로 매핑하기 위한 메소드를 작성

@Mapping(source = "requestDto.type", target = "type", qualifiedByName = "typeToEnum")
    MessageListServiceDto toMessageListServiceDto(String messageId, Integer count, RequestDto requestDto);

    @Named("typeToEnum")
    static Type typeToEnum(String type) {
     switch (type.toUpperCase()) {
      case "LMS":
       return Type.LMS;
      case "MMS":
       return Type.MMS;
      default:
       return Type.SMS;
     }
    }

 

qualifiedByName에 매핑할때 이용할 메소드를 지정해주고, 커스텀 메소드에는 @Named()를 이용해 매핑에 이용될 메소드라는 것을 명시

 

아래와 같이 작성해도 동일하게 동작

default {TargetFieldDataType} To{TargetFieldName} ({SourceFieldDataType} SourceFieldName) {
   ...
}


enum 매핑 코드를 qualifiedByName 없이 작성하면 아래와 같이 수정할 수 있음

MessageListServiceDto toMessageListServiceDto(String messageId, Integer count, RequestDto requestDto);

default Type toType(String type) {
	switch (type.toUpperCase()) {
		case "LMS":
			return Type.LMS;
		case "MMS":
			return Type.MMS;
		default:
			return Type.SMS;
	}
}

 

4-4 사용자 정의 매퍼 메서드

매핑이 까다로운 경우, MapStruct에서 자동으로 구현되는 매핑 메소드 외에 직접 매핑 메소드를 구현해야 하는 경우가 있음

 

default를 붙여 메소드를 만들어주면, 구현 메소드 대신 default 로 정의한 메소드를 사용할 수 있음

 

toMessageListServiceDto를 default메소드로 지정

default MessageListServiceDto toMessageListServiceDto(String messageId, Integer count, RequestDto requestDto) {
   String messageType = Optional.ofNullable(requestDto.getType()).orElse("sms").toUpperCase();
   Type msgType = Type.SMS;

   if (messageType.equals("LMS")) {
      msgType = Type.LMS;
   } else if (messageType.equals("MMS")){
      msgType = Type.MMS;
   }

   return MessageListServiceDto.builder()
      .messageId(Optional.ofNullable(messageId).orElse(UUID.randomUUID().toString()))
      .count(Optional.ofNullable(count).orElse(0))
      .title(requestDto.getTitle())
      .content(requestDto.getContent())
      .sender(requestDto.getSender())
      .receiver(Optional.ofNullable(requestDto.getReceiver()).orElse(Collections.EMPTY_LIST))
      .requestTime(LocalDateTime.now())
      .type(msgType)
      .build();
}

 

아래와 같이 default를 붙여서 매퍼를 사용하면, 위에서 직접 작성한 default 메소드를 매핑 시 사용하게 됨

MessageListServiceDto messageListServiceDto = MessageMapper.INSTANCE.toMessageListServiceDto(messageId, count, resDto);

 

MapStruct Processor 옵션 및 매핑 정책

MapStruct 는 Annotation Processor 를 이용한 매핑인 만큼, Annotation 을 통한 옵션이나, 매핑에 대한 정책을 @Mapper 에 설정할 수 있음

 

ComponentModel

매퍼를 빈으로 만들어야 하는 경우, 아래와 같이 설정하면 빈으로 등록할 수 있음

@Mapper(componentModel = "spring")
public interface MessageMapper {
	...
}

 

생성된 매퍼는 싱글톤 범위의 빈이며, @Autowired를 통해 빈을 조회할 수 있음

 

매퍼를 빈으로 등록해서 사용하는 경우나 매퍼 내부에서 다른 빈을 주입받아 사용이 필요한 경우, 위와 같이 빈을 등록하여 사용할 수 있음

unmmappedTargetPolicy

타겟이 되는 필드에 대한 정책입니다. Target 필드는 존재하는데 source의 필드가 없는 경우에 대한 정책입니다.

 

정책 옵션

  • ERROR : 매핑 대상이 없는 경우, 매핑 코드 생성 시 error 가 발생합니다.
  • WARN : 매핑 대상이 없는 경우, 빌드 시 warn 이 발생합니다.
  • IGNORE : 매핑 대상이 없는 경우 무시하고 매핑됩니다.

정책 설정

@Mapper(unmmapedTargetPolicy = ReportingPolicy.{ERROR,WARN,IGNORE})
public interface MessageMapper {
        MessageMapper INSTANCE = Mappers.getMapper(MessageMapper.class);

        MessageBodyDto toMessageBodyDto(RequestDto requestDto);
}

 

nullValueMappingStrategy / nullValueIterableMappingStrategy

source가 null 인 경우에 제어할 수 있는 null 정책입니다.

 

nullValueIterableMappingStrategy는 iterables나 map에 해당되는 정책입니다.

 

정책 옵션

  • RETURN_NULL : source가 null 일 경우, target을 null 로 설정합니다.
  • RETURN_DEFAULT : source가 null 일 경우, default 값으로 설정됩니다. iterable에는 collection이 매핑 되며, map은 빈 map 으로 매핑이 됩니다.

정책 설정

@Mapper(
         nullValueMapMappingStrategy = NullValueMappingStrategy.{RETURN_NULL,RETURN_DEFAULT},
         nullValueIterableMappingStrategy = NullValueMappingStrategy.{RETURN_NULL,RETURN_DEFAULT}
)
public interface MessageMapper {
        MessageMapper INSTANCE = Mappers.getMapper(MessageMapper.class);

        MessageBodyDto toMessageBodyDto(RequestDto requestDto);
}

 

MapStruct 공식 가이드

 

reference

https://mapstruct.org/
https://mapstruct.org/documentation/stable/reference/html/#checking-source-property-for-null-arguments
https://www.baeldung.com/java-performance-mapping-frameworks
https://trends.google.com/trends/explore?q=MapStruct,ObjectMapper,Orikia

'항해 99 > Spring' 카테고리의 다른 글

WebSocket - 기본 websocket  (0) 2024.03.27
Redis  (1) 2024.03.25
대댓글 기능 구현  (1) 2024.03.21
S3를 이용한 파일 업로드  (0) 2024.03.20
CORS 에러 해결 방법, CORS 보안 취약점 예방 가이드  (0) 2024.03.19