매퍼(Mapper)를 이용한 DTO 클래스 ↔ 엔티티(Entity) 클래스 매핑하는 과정을 정리해 보려 한다.

요그림에서 Controller 에서 DTO <-> Entity Class 요 과정을 매퍼라 한다!!

 

 

 

 

먼저 매퍼클래스를 구현해보자!

//MemberController에서 사용하는 DTO 클래스와 Member 간에 서로 타입을 변환해주는 매퍼(Mapper)클래스

import org.springframework.stereotype.Component;

@Component  // (1)스프링빈 등록을 위해 추가
public class MemberMapper {

    // (2) MemberPostDto를 Member로 변환
    public Member memberPostDtoToMember(MemberPostDto memberPostDto) {//메서드에DTO객체를 넣으면
        return new Member(0L,  //id(null),이메일,이름,폰번호를 넣은 멤버객체 리턴
                memberPostDto.getEmail(),
                memberPostDto.getName(),
                memberPostDto.getPhone());
    }

    // (3) MemberPatchDto를 Member로 변환
    public Member memberPatchDtoToMember(MemberPatchDto memberPatchDto) { //메서드에DTO객체를 넣으면
        return new Member(memberPatchDto.getMemberId(), //id,이메일(null),이름,폰번호를 넣은 멤버객체 리턴
                null,
                memberPatchDto.getName(),
                memberPatchDto.getPhone());
    }

    // (4) Member를 MemberResponseDto로 변환
    public MemberResponseDto memberToMemberResponseDto(Member member) { //멤버를 넣으면
        return new MemberResponseDto(member.getMemberId(), //멤버아이디,이메일,이름,폰을 넣은 DTo객체 리턴
                member.getEmail(),
                member.getName(),
                member.getPhone());
    }
}

 

 

member클래스는 다음과 같다.

@Getter
@Setter
@NoArgsConstructor 
@AllArgsConstructor
public class Member {
    private long memberId;
    private String email;
    private String name;
    private String phone;
}

 

 

 

memberController의 핸들러 메서드에 매퍼클래스를 적용해보자


@RestController
@RequestMapping("/v1/members")
@Validated
public class MemberController {
    //final을 하면 꼭 생성자를 만들어줘야함
    //final을 사용한 이유. memberRepository의 값이 필수로 들어와야만 오류가 안남.
    private final MemberService memberService; //멤버서비스를 사용하기 위해 선언
     private final MemberMapper mapper; //매퍼객체 사용 위해 선언
    //의존성 주입해줌
public MemberController(MemberService memberService,MemberMapper mapper){
    this.memberService = memberService;
    this.mapper = mapper;
    }
    @PostMapping
    public ResponseEntity postMember(@Valid @RequestBody MemberPostDto memberDto) {
    //원래는 이렇게 컨트롤러에서 일일히 멤버객체에 넣어줬음
//        Member member = new Member(); //멤버타입 새 객체 생성
//        member.setEmail(memberDto.getEmail()); //Dto에서 이메일 꺼내와 member에 넣어줌
//        member.setName(memberDto.getName());
//        member.setPhone(memberDto.getPhone());

       //이제는 매퍼를 이용해서 멤버PostDTO객체를 Member로 변환
        Member member = mapper.memberPostDtoToMember(memberDto);//client -> controller ->service

        Member response = memberService.createMember(member); //service->controller ->client
        
        //매퍼를 이용해서 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) {
        Member member = mapper.memberPatchDtoToMember(memberPatchDto);
        Member response = memberService.updateMember(member);

        //매퍼를 이용해서 Member를 MemberResponseDto로 변환
        return new ResponseEntity<>(mapper.memberToMemberResponseDto(response), HttpStatus.OK);
    }

    @GetMapping("/{member-id}")
    public ResponseEntity getMember(@PathVariable("member-id") @Positive long memberId) {
        System.out.println("# memberId: " + memberId);
        Member response = memberService.findMember(memberId);

        // (6) 매퍼를 이용해서 Member를 MemberResponseDto로 변환
        return new ResponseEntity<>(mapper.memberToMemberResponseDto(response),
                HttpStatus.OK);
    }

    @GetMapping
    public ResponseEntity getMembers() {
        System.out.println("# get Members");
        //멤버객체 <- 서비스로직
        List<Member> members = memberService.findMembers();

        //멤버객체 -> DTO
        //member를 담은 리스트를 stream으로 만들어서
        // mapper.ToMemberResponseDto(member)로 돌려준 후
        // 다시 리스트로 변환
        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("# deleted memberId: " + memberId);
        // (8)
        memberService.deleteMember(memberId);

        return new ResponseEntity<>(HttpStatus.NO_CONTENT);
    }

    // 생략
}

Mapper 클래스를 사용함으로써 이전 글의 MemberController의 문제점이 해결되었다.

 

 

그러나 도메인 업무 기능이 늘어날때 마다 개발자가 일일이 수작업으로 매퍼(Mapper) 클래스를 만드는 것은 비효율적임

 

따라서 이 매퍼 클래스를 자동 구현해주는 기능이 필요!

 

 

 

🔎MapStruct

 DTO 클래스처럼 Java Bean 규약을 지키는 객체들 간의 변환 기능을 제공하는

매퍼(Mapper) 구현 클래스를 자동으로 생성해주는 코드 자동 생성기

 

 

 

MapStruct 관련 의존 라이브러리를 Gradle의 build.gradle 파일에 아래와 같이 추가함으로써 사용할 수 있다.

dependencies {
	...
	...
	implementation 'org.mapstruct:mapstruct:1.5.3.Final'
	annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.3.Final'
}

 

 

매퍼(Mapper)를 자동으로 생성하기 위해서는 매퍼(Mapper) 인터페이스를 먼저 정의해야 한다.

//Member 클래스 간의 변환 기능을 제공
// MapStruct 기반의 MemberMapper 인터페이스 정의
//@Mapper 애너테이션의 애트리뷰트로
// (componentModel = "spring") -> Spring Bean으로 등록

@Mapper(componentModel = "spring")  // (1)
public interface MemberMapper {
    Member memberPostDtoToMember(MemberPostDto memberPostDto);

    Member memberPatchDtoToMember(MemberPatchDto memberPatchDto);

    MemberResponseDto memberToMemberResponseDto(Member member);
}

//MapStruct가 자동으로 생성해준 MemberMapper 인터페이스의 구현 클래스는 Gradle의 build task를 실행하면 자동으로 생성

해당 위치에 MemberMapperImpl이 자동 생성된다

 

 

자동 생성된 MamberMapperImpl 클래스의 코드

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2022-10-26T04:28:57+0900",
    comments = "version: 1.5.3.Final, compiler: IncrementalProcessingEnvironment from gradle-language-java-7.4.1.jar, environment: Java 11.0.16 (Azul Systems, Inc.)"
)
@Component
public class MemberMapperImpl implements MemberMapper {

    @Override
    public Member memberPostDtoToMember(MemberPostDto memberPostDto) {
        if ( memberPostDto == null ) {
            return null;
        }

        Member member = new Member();

        return member;
    }

    @Override
    public Member memberPatchDtoToMember(MemberPatchDto memberPatchDto) {
        if ( memberPatchDto == null ) {
            return null;
        }

        Member member = new Member();

        return member;
    }

    @Override
    public MemberResponseDto memberToMemberResponseDto(Member member) {
        if ( member == null ) {
            return null;
        }

        MemberResponseDto memberResponseDto = new MemberResponseDto();

        return memberResponseDto;
    }
}

이제 MapStruct 기반의 MemberMapper 인터페이스를 MemberController에 적용해 보자

//import com.codestates.member.mapper.MemberMapper;
import com.codestates.member.mapstruct.mapper.MemberMapper; // (1) 패키지 변경

/**
 * - DI 적용
 * - Mapstruct Mapper 적용
 */
@RestController
@RequestMapping("/v5/members") // (2) URI 버전 변경
@Validated
public class MemberController {
    ...
		...
		...
}

Matstruct 인터페이스가 위치한 인터페이스의 위치만 import문으로 알려주고, Controller URI의 버전 번호만 v4에서 v5로 바꿔주면 됨

MemberController 클래스의 내부는 손댈 필요 없이 사용하고자 하는 매퍼(Mapper)만 바꿔주면 됨.

 

 

 

 

 

👀DTO와 엔티티 클래스를 매핑해서 사용하는 이유

 

계층별 관심사의 분리

하나의 클래스나 메서드 내에서 한 개의 기능 구현

 

코드 구성의 단순화

유지보수가 쉬워짐

 

✔ REST API 스펙의 독립성 확보

데이터 액세스 계층에서 전달 받은 데이터로 채워진 Entity 클래스를 클라이언트의 응답으로 그대로 전달하게되면 원치 않는 데이터까지 클라이언트에게 전송될 수 있다.

 

ex) 로그인 패스워드 같은 정보를 클라이언트에게 노출하지 않고, 원하는 정보만 제공

복사했습니다!