본문 바로가기
Spring

[Spring] MapStruct 사용하여 Mapper 구현하기

by Bhinney 2022. 10. 20.

✔️ Mapper을 구현하기 위해 MapStruct를 사용하면서 새롭게 알게 된 사실들을 정리하려고 한다.


✅ Mapper 구현 이유

  • DTO 클래스와 Entity 클래스의 변환
  • 코드 단순화
  • 계층간의 역할 분리
  • REST API 독립성 확보

✅ MapStruct

  • Mapper를 지원해주는 라이브러리
  • gradle.yml 파일에서 아래를 추가
dependencies {
	...
	implementation 'org.mapstruct:mapstruct:1.4.2.Final'
	annotationProcessor 'org.mapstruct:mapstruct-processor:1.4.2.Final'
}

// 버전은 바뀔수도 있음.

✔️ Check Mistake

  • annotationProcessor를 추가하지 않고 작업을 하다가 에러를 만났다.
  • 처음에는 MapperImple 생성자가 없다며 설정하라는 에러가 나타났다. 
  • 하지만 나는 @Builder를 사용했기 때문에 에러가 나면 안되는 상황이었다.
  • 그 다음에는 아래의 있는 코드 에러가 났다.
  • 다 바꿔보았는데 해결이 되지 않아, gradle.yml 파일을 확인하니 annotationProcessor가 추가되지 않았다...(머쓱타드)
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException at ConstructorResolver.java:800
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException at DefaultListableBeanFactory.java:1801

✅ MapStruct를 사용하여 MapperImple 자동 구현

  • MapStruct에 있는 @Mapper 어노테이션을 이용하면 쉽게 MapperImple를 구현 할 수 있다.
  • ✨ 애트리뷰트를 componetModel = "spring" 으로 설정해주면 SpringBean으로 등록이 된다. ✨
    • MapperImple 클래스에 @Componet 어노테이션이 붙는다!
@Mapper(componentModel = "spring")
public interface BoardMapper {
    Board boardPostDtoToBoard(BoardDto.Post boardPostDto);
    Board boardPatchDtoToBoard(BoardDto.Patch boardPatchDto);
    BoardDto.Response boardToBoardResponseDto(Board board);
    List<BoardDto.Response> boardToBoardResponseDtos(List<Board> boards);
}

✔️ Check Point

  • DTO의 내부 클래스에서 Lombok 어노테이션에 따라 MapperImple의 세부 구성이 달라진다.

 

1️⃣ PostDto, PatchDto

  • Getter 어노테이션을 사용했기 때문에, 필드를 읽어서 set() 메서드로 값을 넣어준다.
public class BoardDto {

    @Getter
    public static class Post{

        @NotBlank
        private String writer;

        @NotBlank
        private String comment;
    }
    ...
}
@Component
public class BoardMapperImpl implements BoardMapper {
    public BoardMapperImpl() {
    }

    public Board boardPostDtoToBoard(BoardDto.Post boardPostDto) {
        if (boardPostDto == null) {
            return null;
        } else {
            Board board = new Board();
            board.setWriter(boardPostDto.getWriter());
            board.setComment(boardPostDto.getComment());
            return board;
        }
    }

    public Board boardPatchDtoToBoard(BoardDto.Patch boardPatchDto) {
        if (boardPatchDto == null) {
            return null;
        } else {
            Board board = new Board();
            board.setBoardId(boardPatchDto.getBoardId());
            board.setWriter(boardPatchDto.getWriter());
            board.setComment(boardPatchDto.getComment());
            board.setBoardStatus(boardPatchDto.getBoardStatus());
            return board;
        }
    }
    ...
}

2️⃣ ResponseDto

  • 이 MapperImple를 여러번 시도해보게 된 이유다.
  • Lombok 어노테이션을 어떻게 사용하느냐에 따라 구현이 달라진다.
  • 아래의 우선 조건을 확인해보기 위해 시도해보았다.

⭐️❗️MapStruct가 Mapping을 정상적으로 하기 위한 우선 조건❗️⭐️

(1번부터 우선 순위가 높고, 우선 순위가 높은 조건에 먼저 일치하면 해당 조건으로 매핑을 진행)

 

  1. Builder 패턴이 적용되어 있는 경우
  2. 모든 필드를 파라미터로 갖는 생성자가 있는 경우 (단, 기본 생성자가 포함되어 있을 경우 2의 생성자는 역할을 못함.)
  3. Setter 메서드가 있는 경우

 

✔️@Getter + @Builder

  • @Builder : 빌더 패턴 코드 빌드
public class BoardDto {
    ...
    
    @Getter
    @Builder
    public static class Response{
        private long boardId;
        private String writer;
        private String comment;
        private Board.BoardStatus boardStatus;

        private String getBoardStatus(){return boardStatus.getStatus();}
    }
}
@Component
public class BoardMapperImpl implements BoardMapper {
    ...

    public BoardDto.Response boardToBoardResponseDto(Board board) {
        if (board == null) {
            return null;
        } else {
        
           // builder()
            BoardDto.Response.ResponseBuilder response = Response.builder();
            response.boardId(board.getBoardId());
            response.writer(board.getWriter());
            response.comment(board.getComment());
            response.boardStatus(board.getBoardStatus());
            return response.build();
        }
    }

   ...
}

✔️ @Getter + @AllArgsConstructor

  • @Getter : get 메서드를 생성
  • @AllArgsConstructor : 모든 필드를 파라미터로 받는 생성자 생성
public class BoardDto {
	...

    @Getter
    @AllArgsConstructor
    public static class Response{
        private long boardId;
        private String writer;
        private String comment;
        private Board.BoardStatus boardStatus;

        private String getBoardStatus(){return boardStatus.getStatus();}
    }
}
@Component
public class BoardMapperImpl implements BoardMapper {
    ...
    public BoardDto.Response boardToBoardResponseDto(Board board) {
        if (board == null) {
            return null;
        } else {
            // 변수 선언 및 초기화
            long boardId = 0L;
            String writer = null;
            String comment = null;
            Board.BoardStatus boardStatus = null;

            // 변수에 값 할당
            boardId = board.getBoardId();
            writer = board.getWriter();
            comment = board.getComment();
            boardStatus = board.getBoardStatus();

            // 모든 필드를 파라미터로 받는 생성자 생성하여, 파라미터를 넣어줌.
            BoardDto.Response response = new BoardDto.Response(boardId, writer, 
            						comment, boardStatus);
            return response;}
	}
}

✔️@Getter + @Setter + @NoArgsConstructor

  • @Setter : set 메서드를 생성
  • @NoArgsConstructor : 파라미터가 없는 기본 생성자를 생성
public class BoardDto {
	...

    @Getter
    @Setter
    @NoArgsConstructor
    public static class Response{
        private long boardId;
        private String writer;
        private String comment;
        private Board.BoardStatus boardStatus;

        private String getBoardStatus(){return boardStatus.getStatus();}
    }
}
@Component
public class BoardMapperImpl implements BoardMapper {
    ...
    public BoardDto.Response boardToBoardResponseDto(Board board) {
        if (board == null) {
            return null;
        } else {

            // 기본 생성자 생성
            BoardDto.Response response = new BoardDto.Response();

            // setter 메서드로 호출하여 값을 넣어줌.
            response.setBoardId(board.getBoardId());
            response.setWriter(board.getWriter());
            response.setComment(board.getComment());
            response.setBoardStatus(board.getBoardStatus());
            return response;
        }
    }
}

댓글