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