MapStruct를 활용한 엔티티 <-> DTO 매핑
MapStruct는 자바 언어를 활용한 코드 생성 기반의 객체 매핑용 라이브러리이다.
어노테이션 등을 활용해 객체 간의 매핑 작업을 편리하게 수행할 수 있도록 지원하고, 주로 DTO(Data Transfer Object)와 엔티티(Entity) 등의 객체를 변환하기 위한 용도로 사용된다.
반복적이고 번거로운 매핑 코드를 빌드 시 자동으로 생성하여 개발자가 코드를 간결하게 작성할 수 있도록 도움을 주기 때문에 자주 사용된다.
1. 환경설정
mapstrut을 사용하기 위해서는 다음과 같은 의존성을 추가해야 한다.
Gradle
1
2
3
4
5
6
7
// lombok
implementation 'org.projectlombok:lombok:1.18.22'
annotationProcessor 'org.projectlombok:lombok:1.18.22'
annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0'
// mapstruct
implementation 'org.mapstruct:mapstruct:1.4.2.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.4.2.Final'
Maven
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<org.mapstruct.version>1.5.5.Final</org.mapstruct.version>
// mapstruct
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
// plugin/annotationProcessorPaths
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path>
</annotationProcessorPaths>
2. 매핑 DTO 및 Mapper 생성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Builder
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@Column
@Comment("고객명")
private String name;
@Column
@Comment("고객 이메일")
private String email;
}
@Builder
@Getter
@AllArgsConstructor
public class MemberRequestDTO {
private String name;
private String email;
}
@Builder
@AllArgsConstructor
public class MemberResultDTO {
private int id;
private String name;
private String email;
}
예시를 위해 Member
엔티티 와 엔티티가 변환할 or 변환될 DTO 클래스를 생성하였다.
이제 엔티티와 DTO를 매핑시켜 줄 Mapper 인터페이스를 생성해보자.
1
2
3
4
5
6
7
8
9
10
11
@Mapper(
unmappedTargetPolicy = ReportingPolicy.IGNORE, // 매핑되지 않은 필드 무시
nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE // null 값 무시
)
public interface MemberMapper {
MemberMapper INSTANCE = Mappers.getMapper(MemberMapper.class);
Member toMember(MemberRequestDTO request);
MemberResultDTO toMemberResultDTO(Member member);
}
위와 같이 MemberMapper
인터페이스에 @Mapper
어노테이션을 추가해주면 compile 시에 엔티티 <-> DTO를 매핑시켜주는 코드가 annotationPath( target/generated-sources/annotations/..) 하위에 생성된다. @Mapper
어노테이션에는 다양한 옵션을 설정할 수 있는데, 자세한 내용은 mapstruct 공식문서를 참고하면 된다. 위 코드에서는 엔티티와 DTO 간의 이름이 다른 필드들은 매핑하지 않는 옵션과, 필드 값이 null 인 필드는 매핑하지 않는 옵션만 추가하였다.
이제 MemberMapper
를 사용하여 MemberRequestDTO -> Member, Member -> MemberResultDTO 객체로의 매핑을 시켜보자.
3. Mapper를 사용한 매핑
위에서 MamberMapper
인터페이스를 잘 생성했다면 compile 후에 annotationPath에 MamberMapperImpl
자바파일이 생성되었을 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class MemberMapperImpl implements MemberMapper {
@Override
public Member toMember(MemberRequestDTO request) {
if ( request == null ) {
return null;
}
Member.MemberBuilder member = Member.builder();
member.name( request.getName() );
member.email( request.getEmail() );
return member.build();
}
@Override
public MemberResultDTO toMemberResultDTO(Member member) {
if ( member == null ) {
return null;
}
MemberResultDTO.MemberResultDTOBuilder memberResultDTO = MemberResultDTO.builder();
memberResultDTO.id( member.getId() );
memberResultDTO.name( member.getName() );
memberResultDTO.email( member.getEmail() );
return memberResultDTO.build();
}
}
이처럼 mapstruct 라이브러리를 활용하면 위와 같이 엔티티와 DTO를 매핑시켜주는 코드를 compile 시에 자동으로 생성할 수 있다.
이제 MemberMapper
를 사용하여 엔티티와 DTO를 매핑시켜보자.
1
2
3
4
5
6
public MemberResultDTO getMember(MemberRequestDTO request) {
MemberMapper mapper = MemberMapper.INSTANCE; // mapper 인스턴스 생성
Member member = memberRepository.save(mapper.toMember(request));
return mapper.toMemberResultDTO(member);
}
MemberMapper
인터페이스를 호출하여 MemberRequestDTO -> Member로 매핑 및 생성하여 jpa로 insert 하였고, Member -> MemberResultDTO로 변환하여 반환하는 작업이 간단하게 수행된 것을 확인할 수 있다. 이 후로는 필요 시 Mapper 인터페이스와 변환할 메소드만 정의해주어 사용하면 되므로 코드량도 줄일 수 있고 매핑도 간단해진다.
물론 mapstruct를 사용하지 않고 jackson 라이브러리의 ObjectMapper를 사용하여 변환할 수도 있지만, mapstruct에 비해 코드가 깔끔하지 않고, 객체간 변환이 많아질 수록 관리가 복잡해질 수 있다. 예시 코드처럼 엔티티와 요청/응답DTO 객체간 매핑이 많고 반드시 변환이 필요한 경우 엔티티 별로 Mapper 인터페이스를 생성하고 필요한 변환 메소드를 정의하여 사용하면 추후 사용 및 관리적인 측면에서도 유용하게 사용할 수 있다.