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
728x90
'Spring' 카테고리의 다른 글
HikariCP란 무엇일까? (0) | 2022.10.27 |
---|---|
@Scheduled를 사용하여 API를 주기적으로 호출 (0) | 2022.06.07 |
spring boot 2.6.x에 swagger 3.0 설정해보자(+수정 openapi 3.0 2022.05.30) (0) | 2022.05.11 |
Filed Injection을 지양하자! (0) | 2022.05.02 |
JSON 변환 시 지연로딩에 따른 InvalidDefinitionException 에러 해결 (0) | 2022.04.17 |