[Mapstruct] @Mapping, @Mapper(uses={})

귀찮Lee·2023년 2월 12일
0
  • 아래 사용할 용어 정리
    • source : method에 인자로 주어질 객체
    • target : return 값으로 나올 객체
    • mapping : source의 있는 값을 target

◎ @Mapping

  • 자동으로 mapping하는 경우

    • source와 target의 field 이름과 Type이 동일하다면 따로 설정하지 않아도 자동으로 Mapping 한다.
    • source와 target의 field 이름이 동일하고 따로 Mapping한 method가 존재한다면 자동으로 Mapping 한다.
  • 수동으로 mapping 하는 방법

    • @Mapping(target = "필드 이름", source = "필드 이름") 형식으로 mapping 가능
      • 예시 : @Mapping(target = "member.memberId", source = "memberId")
    • target을 자기 자신으로 하고 싶을 때는 "."을 이용
      • 예시 : @Mapping(target = ".", source = "followQuestion.question")
    • @Mapping(target = "필드 이름", expression = "java()") 형식으로 결과값을 커스텀하여 넣을 수 있다.
      • 예시 : @Mapping(target = "answerCount", expression = "java(question.getAnswers().size())")

◎ @Mapper(uses={})

  • 다른 Mapper의 method를 사용하고 싶을 때, @Mapper의 uses에 다른 Mapper.class를 넣어서 사용할 수 있다.
    • field 값에 A -> B 로 mapping 해야 하는 경우가 있다면, 해당 mapping이 구현된 다른 class를 넣어준다면 자동적으로 mapping을 한다.
    • 구현 방법 : Mapstruct에서 파일 생성시 @Autowired 필드 주입을 통해 구현함
    • 주의사항 : CMapper와 DMapper가 서로 uses로 사용하게 되면 순환 참조 현상으로 에러가 발생함

◎ @Mapper(uses={}) test code 작성

  • uses 사용시, Spring Container에서 주입하는 과정이 필요하기 때문에 @SpringBootTest를 이용하여야 한다.
    • classes={}를 사용한다면 특정 파일만 주입하므로 test 시간이 더 짧아진다.

◎ 실제 사용 예시

  • 자동으로 mapping되는 경우

    @Mapper(componentModel = "spring")
    public interface MemberMapper {
         // displayName, image, location, memberTitle, aboutMe가 자동으로 mapping 된다.
         Member memberPatchDtoToMember(MemberDto.Patch patch);
         ...
    }
    
    @Getter
    public class MemberDto {
    
        @AllArgsConstructor
        @NoArgsConstructor
        @Getter
        @Setter
        public static class Patch{
            private String displayName;
            private String image;
            private String location;
            private String memberTitle;
            private String aboutMe;
        }
    }
    
    @NoArgsConstructor
    @Getter
    @Setter
    @Entity
    public class Member extends Auditable {
    
        private long memberId;
        private String email;
        private String password;
        private String displayName;
        private String image;
        private String location;
        private String memberTitle;
        private String aboutMe;
        ...
    
    }
    • @Mapping을 이용해 수동으로 mapping 하는 경우
    @Mapper(componentModel = "spring")
    public interface QuestionMapper {
        // Member의 field 중 하나인 memberId를 mapping함
        @Mapping(target = "member.memberId", source = "memberId")
        Question questionPostDtoToQuestion(QuestionDto.Post post);
    }
    
    @NoArgsConstructor
    @Getter
    @Setter
    @Entity
    public class Question extends Auditable {
        private long questionId;
        private String title;
        private String content;
        private long view = 0;
        private long vote = 0;
        private Member member;
    }
    
    @Getter
    public class QuestionDto {
    
        @AllArgsConstructor
        @NoArgsConstructor
        @Getter
        @Setter
        public static class Post{
            private String title;
            private String content;
            private long memberId;
        }
    }
  • @Mapping에서 custom 값을 이용하는 경우

    @Mapper(componentModel = "spring")
    public interface QuestionMapper {
    
        // QuestionDto.Response의 answerCount값을 question.getAnswers().size()의 결과값이 들어간다.
        // QuestionDto.Response의 bestAnswerId 값을 
        // question.getBestAnswer() != null ? question.getBestAnswer().getAnswerId() : null의 결과값이 들어간다.
    
        @Mapping(target = "answerCount", expression = "java(question.getAnswers().size())")
        @Mapping(target = "bestAnswerId",
                expression = "java(question.getBestAnswer() != null ? question.getBestAnswer().getAnswerId() : null)")
        QuestionDto.Response questionToQuestionResponseDto(Question question);
    }
  • @Mapper(uses={}) 사용 예시

    @Mapper(componentModel = "spring", uses = {MemberMapper.class, AnswerMapper.class})
    public interface FollowAnswerMapper {
        // MemberMapper에 Member를 MemberDto.SubResponse으로 mapping하는 method가 구현되어 있음
        // AnswerMApper에 Answer를 AnswerDto.Response으로 mapping하는 method가 구현되어 있음
        FollowAnswerDto.PostResponse followAnswerToFollowAnswerPostResponseDto(FollowAnswer followAnswer);
    }
    
    @NoArgsConstructor
    @AllArgsConstructor
    @Getter
    @Setter
    public class FollowAnswer {
        private long faId;
        private Answer answer;
        private Member member;
        private LocalDateTime createdAt;
    }
    
    public class FollowAnswerDto {
    
        @AllArgsConstructor
        @Getter
        @Setter
        public static class PostResponse {
            private MemberDto.SubResponse member;
            private AnswerDto.Response answer;
        }
    }
  • @Mapper(uses={})를 사용한 Class에 test code 작성

    @SpringBootTest(classes = {QuestionMapperImpl.class, AnswerMapperImpl.class,
            MemberMapperImpl.class, FollowAnswerMapperImpl.class})
    public class FollowAnswerMapperTest {
    
        @Autowired
        private  FollowAnswerMapper mapper;
    
        private static final FollowAnswer MOCK_ENTITY = FollowAnswerStub.getEntity;
    
        @Test
        void followAnswerToFollowAnswerPostResponseDtoTest() {
            FollowAnswerDto.PostResponse expected
                    = new FollowAnswerDto.PostResponse(MemberStub.getSubResponse(), AnswerStub.getResponse());
    
            FollowAnswerDto.PostResponse result
                    = mapper.followAnswerToFollowAnswerPostResponseDto(MOCK_ENTITY);
    
            assertThat(result).usingRecursiveComparison().isEqualTo(expected);
        }

◎ 참고 자료

profile
배운 것은 기록하자! / 오류 지적은 언제나 환영!

0개의 댓글