매퍼는 DTO 클래스와 엔티티 클래스를 서로 변환해주는 클래스이다.
Mapper 클래스의 클래스 레벨에 @Component
애너테이션을 추가한다.
이는 Mapper 클래스를 Spring Bean으로 등록하고, 등록된 Bean은 Controller에서 사용한다.
@Component
public class MemberMapper {
// MemberPostDto 를 Member 엔티티로 변환
public Member memberPostDtoToMember(MemberPostDto memberPostDto) {
return new Member(
0L,
memberPostDto.getEmail(),
memberPostDto.getName(),
memberPostDto.getPhone()
);
}
// MemberPatchDto 를 Member 엔티티로 변환
public Member memberPatchDtoToMember(MemberPatchDto memberPatchDto) {
return new Member(
memberPatchDto.getMemberId(),
// 일반적으로 이메일은 고유한 데이터로 변경되지 않아
// null로 Patch가 불가능하도록 표기한 것
null,
memberPatchDto.getName(),
memberPatchDto.getPhone()
);
}
// Member 엔티티를 MemberResponseDto 로 변환
// MemberResponseDto 는 ResponseBody의 역할을 해주는 DTO 클래스
public MemberResponseDto memberToMemberResponseDto(Member member) {
return new MemberResponseDto(
member.getMemberId(),
member.getEmail(),
member.getName(),
member.getPhone()
);
}
}
DI로 주입하여 Spring Bean에 등록된 Mapper 객체를 Controller에서 사용한다.
@RestController
@RequestMapping("/v1/members/")
@Validated
public class MemberController {
private final MemberService memberService;
private final MemberMapper mapper;
public Controller(MemberService memberService, MemberMapper mapper) {
this.memberService = memberService;
this.mapper = mapper;
}
@PostMapping
public ResponseEntity postMember(@Valid @RequestBody MemberPostDto memberDto) {
// Mapper를 이용하여 MemberPostDto를 Member로 변환
Member member = mapper.memberPostDtoToMember(memberDto);
Member response = memberService.createMember(member);
// Mapper를 이용하여 Member를 MemberResponseDto로 변환
return new ResponseEntity<>(mapper.memberToMemberResponseDto(response), HttpStatus.CREATED);
}
@PatchMapping("/{member-id}")
public ResponseEntity patchMember(@PathVariable("member-id") @Positive long memberId,
@Valid @RequestBody MemberPatchDto memberPatchDto) {
memberPatchDto.setMemberId(memberId);
// Mapper를 이용하여 MemberPatchDto를 Member로 변환
Member response =
memberService.updateMember(mapper.memberPatchDtoToMember(memberPatchDto));
// Mapper를 이용하여 Member를 MemberResponseDto로 변환
return new ResponseEntity<>(mapper.memberToMemberResponseDto(response), HttpStatus.OK);
}
@GetMapping("/{member-id}")
public ResponseEntity getMember(@PathVariable("member-id") @Positive long memberId) {
Member response = memberService.findMember(memberId);
// Mapper를 이용하여 Member를 MemberResponseDto로 변환
return new ResponseEntity<>(mapper.memberToMemberResponseDto(response), HttpStatus.OK);
}
@GetMapping
public ResponseEntity getMembers() {
List<Member> members = memberService.findMembers();
// // Mapper를 이용하여 List<Member>를 MemberResponseDto로 변환
List<MemberResponseDto> response = members.stream()
.map(member -> mapper.memberToMemberResponseDto(member))
.collect(Collectors.toList());
return new ResponseEntity<>(response, HttpStatus.OK);
}
@DeleteMapping("/{member-id}")
public ResponseEntity deleteMember(@PathVariable("member-id") @Positive long memberId) {
System.out.println("# delete member");
memberService.deleteMember(memberId);
return new ResponseEntity(HttpStatus.NO_CONTENT);
}
}
MapStruct는 Mapper 구현 클래스를 자동으로 생성해주는 코드 자동 생성기이다.
도메인 업무 기능이 증가함에 따라 매퍼 클래스를 생성하는 것은 비효율적이기 때문에 MapStruct를 이용해 자동으로 구현함으로 생산성을 향상시켜줄 수 있다.
build.gradle 파일에 의존 라이브러리 추가 및 Gradle 프로젝트 Reload해야 한다.
dependencies {
...
...
implementation 'org.mapstruct:mapstruct:1.4.2.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.4.2.Final'
}
@Mapper
를 추가하여 해당 인터페이스가 MapStruct의 매퍼 인터페이스임을 정의해야 한다.
@Mapper(componentModel = "spring")
public interface MemberMapper {
Member memberPostDtoToMember(MemberPostDto memberPostDto);
Member memberPatchDtoToMember(MemberPatchDto memberPatchDto);
MemberResponseDto memberToMemberResponseDto(Member member);
}
Mapper 인터페이스의 구현 클래스는 Gradle의 build task를 실행하면 자동으로 생성된다.
@Component
public class MemberMapperImpl implements MemberMapper {
public MemberMapperImpl() {
}
public Member memberPostDtoToMember(MemberPostDto memberPostDto) {
if (memberPostDto == null) {
return null;
} else {
Member member = new Member();
member.setEmail(memberPostDto.getEmail());
member.setName(memberPostDto.getName());
member.setPhone(memberPostDto.getPhone());
return member;
}
}
public Member memberPatchDtoToMember(MemberPatchDto memberPatchDto) {
if (memberPatchDto == null) {
return null;
} else {
Member member = new Member();
member.setMemberId(memberPatchDto.getMemberId());
member.setName(memberPatchDto.getName());
member.setPhone(memberPatchDto.getPhone());
return member;
}
}
public MemberResponseDto memberToMemberResponseDto(Member member) {
if (member == null) {
return null;
} else {
long memberId = 0L;
String email = null;
String name = null;
String phone = null;
memberId = member.getMemberId();
email = member.getEmail();
name = member.getName();
phone = member.getPhone();
MemberResponseDto memberResponseDto =
new MemberResponseDto(memberId, email, name, phone);
return memberResponseDto;
}
}
}
MapStruct 인터페이스의 위치만 import문으로 전달한다.
//import com.codestates.member.mapper.MemberMapper;
import com.codestates.member.mapstruct.mapper.MemberMapper; // 패키지 변경
@RestController
@RequestMapping("/v1/members/")
@Validated
public class MemberController {
...
... // 변경 사항 없음으로 생략
}
Java에서 Object를 Mapping하는 라이브러리는 생각보다 많이 존재한다. 그 중에서 가장 많이 사용되는 Mapping 라이브러리에는 MapStruct와 ModelMapper가 있다.
ModelMapper가 여전히 많이 사용되고 있지만 ModelMapper는 Runtime 시 Java의 리플렉션 API를 이용해서 매핑을 진행하기 때문에 컴파일 타임에 이미 Mapper가 모두 생성되는 MapStruct보다 성능면에서 월등히 떨어진다.
따라서 ModelMapper의 대안으로 MapStruct가 많이 사용되고 있는 추세이다.
DTO 클래스에서 사용하는 유효성 검사 애너테이션이 Entity 클래스에서 사용되면 JPA에서 사용하는 애너테이션과 뒤섞인 상태로 유지보수하기 어려운 코드가 된다.
따라서 역할 분리를 통해 코드를 단순화할 필요가 있다.
DTO 클래스를 사용하여 클라이언트에게 원하는 정보만 선별하여 제공 가능하다.