[TIL] MapStruct - 데이터 변환 코드 생성기

phdljr·2023년 12월 12일
0

TIL

목록 보기
45/70
post-custom-banner

스프링 프로젝트를 진행하다보면, 계층 간의 데이터를 전달하는데에 DTO를 많이 사용한다. 때로는 VO를 쓰기도 한다.

해당되는 데이터를 DTO로 만들어 줄 때, 주로 생성자나 Builder, 또는 따로 만들어둔 메소드를 통해 만들게 된다.

그러나, 서비스가 많아지게 된다면 DTO 또한 많아지게 되며 관리하기가 까다로워질 수도 있다.

이러한 문제를 해결하는데 도움을 주는 기술인 MapStruct에 대해 알아보는 시간을 가져본다.


MapStruct

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

특징

  • 컴파일 시점에 코드를 생성하여 런타임에서 안정성을 보장
  • 다른 매핑 라이브러리보다 속도가 빠름
  • 반복되는 객체 매핑에서 발생할 수 있는 오류를 줄일 수 있음
  • 구현 코드를 자동으로 만들어주기 떄문에 사용이 쉬움
  • 어노테이션 기반으로 객체 간 매핑을 자동으로 제공
  • Lombok 라이브러리에 먼저 의존성이 추가되어야 함
    • MapStruct는 Lombok의 getter, builder를 이용하여 생성됨
    • source에는 getter, target에는 builder가 필요함
    • builder가 없다면 생성자를, 기본 생성자만 있다면 기본 생성자와 setter, getter를 사용한다.
      • 웬만하면 builder와 getter를 쓰자

설정

    implementation 'org.mapstruct:mapstruct:1.5.5.Final'
    annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'
  • Lombok을 먼저 의존성을 달아준 뒤, MapStruct 의존성 달기

  • unmmappedTargetPolicy 설정(선택)

    • 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 설정(옵션)

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

Mapper 사용 방법

  • Mapper를 생성한 뒤, INSTANCE로 선언한 필드를 통해 메소드를 호출한다.
@Mapper(unmmapedTargetPolicy = ReportingPolicy.{ERROR,WARN,IGNORE})
public interface MessageMapper {
        MessageMapper INSTANCE = Mappers.getMapper(MessageMapper.class);

        MessageBodyDto toMessageBodyDto(RequestDto requestDto);
}
public class BoardService {

    public BoardResponseDto toDto() {
        Board board = Board.builder()
            .id(1)
            .title("title")
            .user(User.builder().username("username").build())
            .userType(UserType.ADMIN).build();

        return BoardMapper.INSTANCE.toBoardResponseDto(board);
    }
}

Mapper 메소드 생성 방법

1. 기본 사용 방법

  • 필드 명이 서로 같다면, 따로 어노테이션을 달아줄 필요가 없음
public class RequestDto {
        private String title;
        private String content;
        private String sender;
        private List<String> receiver;
        private LocalDateTime requestTime;
        private String type;
}

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

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

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

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

  • 기본적으로 필드 명이 같으면 따로 어노테이션을 설정해주지 않아도 됨
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);
}

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

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

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

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

4. 매핑 시 default 값 지정 및 특정 필드 매핑 무시

  • messageId 값이 null인 경우, UUID 값을 대입
  • type 값이 null인 경우, SMS 값을 대입
  • sender 필드는 매핑하지 않음
  • 날짜 데이터도 변환해주는 기술이 있음(찾아보기)
@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);
}

5. 특정 필드 매핑 시 지정 메소드 이용

  • default로 선언된 메소드는 toBoardVo 메소드에서 매핑될 필드의 타입이 다를 때 알아서 사용됨
    • Timestamp createTimestamp 필드를 String createTimestamp으로 변환할 때
    • UserEntity userEntity 필드를 String username으로 변환할 때
    • 간단한 경우에는 @Mapping(source = "userEntity.username", target = "username") 으로 설정 가능
  • 기존에 만들어 둔 매핑 메소드를 통해 Collection을 반환하도록 설정도 가능
  • 간단한 enum -> String 변환은 자동으로 이뤄짐
@Mapper
public interface BoardProviderImplMapper {
    BoardProviderImplMapper INSTANCE = Mappers.getMapper(BoardProviderImplMapper.class);

    @Mapping(source = "createTimestamp", target = "createTimestamp")
    default String toStringTime(Timestamp timestamp) {
        return timestampToString(timestamp);
    }

    @Mapping(source = "userEntity", target = "username")
    default String toUsername(UserEntity userEntity) {
        return userEntity.getUsername();
    }

    @Mapping(source = "userEntity", target = "username")
    BoardVo toBoardVo(BoardEntity boardEntity);

    List<BoardVo> toBoardVoList(List<BoardEntity> boardEntityList);
}

6. 필드가 추가됐을 때

  • source에 .으로 넘기고 default 메소드를 따로 만들어서 값을 리턴
    @Mapper
    public interface BoardMapper {

        BoardMapper INSTANCE = Mappers.getMapper(BoardMapper.class);

        @Mapping(source = "user", target = "username")
        default String toUsername(User user) {
            return user.getUsername();
        }

        @Mapping(target = ".", source = "likeCount")
        default int toLikeCount(Board board) {
            return 1;
        }

        @Mapping(source = "title", target = "boardTitle")
        @Mapping(source = "user", target = "username")
        @Mapping(source = "userType", target = "writerType")
        @Mapping(source = ".", target = "likeCount")
        BoardResponseDto toBoardResponseDto(Board board);
    }

7. 객체 내부의 collection을 매핑해줄 때

  • 해당 collection을 매핑해줄 새로운 매핑 메소드를 생성한다.
  • 새로운 매핑 메소드에서도 필드 명을 지정해줄 수 있다.
  • 참고로, record를 사용할 수 있다.
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "TB_BOARD")
public class Board extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    private String content;

    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user;

    @OneToMany(mappedBy = "board", cascade = CascadeType.ALL, orphanRemoval = true)
    public Set<Comment> comments = new LinkedHashSet<>();

    @OneToMany(mappedBy = "board", cascade = CascadeType.ALL, orphanRemoval = true)
    public Set<BoardLike> likes = new LinkedHashSet<>();

    @Builder
    public Board(final String title, final String content, final User user) {
        this.title = title;
        this.content = content;
        this.user = user;
    }
}

public record BoardGetResponseDto(
    String title,
    String nickname,
    String content,
    LocalDateTime createdAt,
    List<CommentResponseDto> comments
) {}

@Mapper
public interface BoardMapper {

    BoardMapper INSTANCE = Mappers.getMapper(BoardMapper.class);

    @Mapping(source = "user.nickname", target = "nickname")
    CommentResponseDto toCommentResponseDto(Comment comment);

    @Mapping(source = "user.nickname", target = "nickname")
    BoardGetResponseDto toBoardResponseDto(Board board);
}

참고

profile
난 Java도 좋고, 다른 것들도 좋아
post-custom-banner

0개의 댓글