자바생
article thumbnail
Published 2022. 5. 30. 13:48
MapStruct Spring
728x90

MapStruct

java mapping framework는 여러 가지가 있다

대표적으로 MapStruct와 ModelMapper가 있는데, 대부분 MapStruct를 사용한다

 

  • 아래는 구글 트렌드에서 캡처한 자료

 

그 이유는 “속도” 차이가 월등히 나기 때문이다

ModelMapper는 매핑이 일어날 때 리플렉션이 발생

MapStruct는 컴파일 시점에서 annotation을 읽어 구현체를 만들어내기 때문에 리플렉션이 발생하지 않는다

(리플렉션은 구글링!)

  • modelmapper는 변환할 때마다 맵핑할 객체를 계속해서 만들어내고(런타임), mapstruct는 빌드 시점에 구현체를 하나 만들어서 계속해서 그 구현체를 사용하기 때문에(컴파일) mapstruct가 더 시간이 빠르지 않을까?

 

그렇다면 MapStruct는 어떻게 사용하는지 알아보자

 

dependency

  • lombok 설치
  • mapstruct 의존성 추가
implementation 'org.mapstruct:mapstruct:1.4.1.Final'
implementation 'org.projectlombok:lombok-mapstruct-binding:0.2.0'
annotationProcessor "org.mapstruct:mapstruct-processor:1.4.1.Final"
annotationProcessor "org.projectlombok:lombok-mapstruct-binding:0.2.0"
  • Java 9와 함께 사용할 수 있다고 한다(map struct 공식 문서)

 

Member.class

@Getter @Setter
@NoArgsConstructor
@ToString(of = {"id", "username", "age"})
@Entity
public class Member {
    @Id
    @GeneratedValue
    @Column(name = "member_id")
    private Long id;

    private String username;

    private int age;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;

    public Member(String username) {
        this(username, 0);
    }

    public Member(String username, int age) {
        this(username, age, null);
    }

    public Member(String username, int age, Team team) {
        this.username = username;
        this.age = age;
        if (team != null) {
            changeTeam(team);
        }
    }
}

 

Team.class

@Entity
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id", "name"})
public class Team {

    @Id
    @GeneratedValue
    @Column(name = "team_id")
    private Long id;

    private String name;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();

    public Team(String name) {
        this.name = name;
    }
}

 

MemberDto.class

@AllArgsConstructor
@Getter
public class MemberDto {

    private String username;
    private int age;
    private String teamName;
}

 

MemberMapper.class(연관관계에서 사용)

@Mapper(componentModel = "spring")
public interface MemberMapper{

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

    @Mapping(target = "teamName", expression = "java(member.getTeam().getName())", ignore = true)
    MemberDto entityToDto(Member member);

    @Mapping(target = "team.name", expression = "java(memberDto.getTeamName())")
    Member dtoToEntity(MemberDto memberDto);
}
  • target은 변환할 필드를 의미
  • expression은 연관관계에서 사용할 수 있는데, 중요한 부분은 java로 감싸줘야 한다
  • ignore = true는 해당 값이 null이든 아니든 무시하고 null을 주입한다

 

MapperTest(Entity -> DTO)

 

    @Test
    @DisplayName("mapper를 이용하여 Entity를 DTO로 변환")
    void entityToDTO() throws Exception {
        //given
        Member member = new Member("member1", 10, new Team("team1"));

        //when
        MemberDto memberDto = MemberMapper.INSTANCE.entityToDto(member);

        //then
        assertAll(
                () -> assertThat(memberDto.getUsername()).isEqualTo(member.getUsername()),
                () -> assertThat(memberDto.getAge()).isEqualTo(member.getAge()),
                () -> assertThat(memberDto.getTeamName()).isNull());
    }
  • 앞서 말했듯이 ignore=true를 넣으면 team에 값이 있더라도 teamName값이 null이 된다

 

MemberMapperImpl

  • MapStruct가 컴파일 시점에 만들어준 MemberMapper의 구현체이다
  • 코드를 보면 전에 MemberMapper에서 ignore = true 부분 때문에 teanName에 null이 들어간 것을 볼 수 있다

 

MapperTest(DTO -> Entity)

    @Test
    @DisplayName("mapper를 이용하여 DTO를 Entity로 변환")
    void dtoToEntity() throws Exception {
        //given
        MemberDto memberDto = new MemberDto("member1", 10, "team1");

        //when
        Member member = MemberMapper.INSTANCE.dtoToEntity(memberDto);

        //then
        assertAll(
                () -> assertThat(memberDto.getUsername()).isEqualTo(member.getUsername()),
                () -> assertThat(memberDto.getAge()).isEqualTo(member.getAge()),
                () -> assertThat(memberDto.getTeamName()).isEqualTo(member.getTeam()
                                                                          .getName()));
    }

 

MemberMapperImpl

  • entityToDto와 다른 점이 무엇일까??
  • dtoToEntity는 생성자를 통해 변환하지만 entityToDto는 setter를 이용해 변환한다
  • Dto는 모든 필드를 인자로 가진 생성자가 존재하고, entity는 그러지 않기 때문이다
  • 즉, 변환할 객체에는 getter가 필수적이고, 변환될 객체에는 모든 필드를 인자로 가진 생성자나 setter가 존재해야 함을 알 수 있다

 

MemberMapper.class(필드 명이 다를 경우)

    @Mapping(source = "age", target = "memberAge")
    @Mapping(source = "username", target = "memberName")
    @Mapping(target = "teamName", expression = "java(member.getTeam().getName())")
    TeamMemberDto entityToTeamMemberDto(Member member);
  • Member의 필드명과 비교했을 때 다르다
  • 따라서 source는 변환할 객체의 필드명, target은 변환될 객체의 필드명을 의미한다
  • member의 age를 TeamMemberDto의 memberAge에 넣는다는 의미

 

TeamMemberDto.class

@AllArgsConstructor
@Getter
public class TeamMemberDto {

    private String teamName;
    private String memberName;
    private int memberAge;
}

 

MapperTest

    @Test
    @DisplayName("entity의 변수명과 Dto의 변수명이 다를 경우의 변환")
    void entityToAnotherDto() throws Exception {
        //given
        Member member = new Member("member1", 10, new Team("team1"));

        //when
        TeamMemberDto teamMemberDto = MemberMapper.INSTANCE.entityToTeamMemberDto(member);

        //then
        assertAll(
                () -> assertThat(teamMemberDto.getMemberName()).isEqualTo(member.getUsername()),
                () -> assertThat(teamMemberDto.getMemberAge()).isEqualTo(member.getAge()),
                () -> assertThat(teamMemberDto.getTeamName()).isEqualTo(member.getTeam()
                                                                              .getName()));
    }

 

MemberMapperImpl

  • TeamMemberDto에도 모든 필드를 인자로 가진 생성자가 존재하기 때문에 따로 setter가 필요하지 않다

 

MemberMapperImpl을 생성한 적이 없는데 이건 무엇인가요?

  • build를 하면  MapStruct가 Mapper Annotation을 가진 인터페이스의 구현체가 생성되고, 빈으로 등록된다
  • 따라서 해당 구현체는 MemberMapper의 추상 메서드를 오버 라이딩해야 하고, 이는 자바 프로퍼티를 이용하여 구현된다
  • 변환할 객체에는 getter가 필수적으로 존재해야 함
  • 변환될 객체에는 모든 필드를 인자로 가진 생성자가 존재하거나, 그것이 아니라면 setter가 존재해야 한다
  • mapper클래스를 빌드하면 generated에 아래와 같은 이름으로 구현체가 생성되는 것을 알 수 있다

 

정리 & 생각해야 할 점

  • DTO를 변환시킬 때는 DTO 클래스에 setter가 있거나 모든 인자를 가진 생성자가 있으면 된다(개인적인 생각,, 다만 @Setter보다는 @AllArgs를 선호)
  • 하지만 entity로 변환시킬 때는 entity에 setter를 지양하고, JPA를 사용한 나는 모든 필드를 인자로 가진 생성자를 작성할 수 없다
    • id값을 어떻게 처리해줘야 할까??
  • 그러면 Mapper인터페이스를 생성하고 이를 구현하는 구현체를 직접 생성하여 오버라이딩하면 어떨까?
    • MapStruct를 사용하는 의미가 없어짐
  • update를 사용할 때는 어떻게 할까?
    • Mapper를 이용하여 DTO를 Entity로 변환시킨 뒤, 엔티티에서 Entity를 parameter로 받아 update를 하면 되지 않을까?

 


REFERENCES

modelmapper와 mapstruct 성능 비교

java mapping framework 성능 비교

MapStruct 공식문서

 

728x90
profile

자바생

@자바생

틀린 부분이 있다면 댓글 부탁드립니다~😀

검색 태그