티스토리 뷰

728x90
반응형

 

 

 

지난 글에선 작성한 API 계층을 기반으로 서비스 계층을 대략적으로 구현했다.

 

DI를 통한 계층 연동에는 크게 두 가지 정도의 문제가 있었는데, 잠깐 복습하면 아래와 같다.

 

  • MemberController 핸들러 메서드의 책임과 역할에 관한 문제

    • 핸들러 메서드는 요청 및 응답 데이터를 Service 클래스 - 클라이언트 사이에서 전송해주는 단순한 역할을 갖는 것이 좋음
    • 현재는 핸들러 메서드가 DTO 클래스를 엔티티(Entity) 객체로 변환하는 작업까지 맡고 있음
  • 서비스 계층에서 사용되는 엔티티(Entity) 객체를 클라이언트에 응답으로 전송하는 문제

    • DTO 클래스는 API 계층에서, 엔티티(Entity) 클래스는 서비스 계층에서만 데이터를 처리하는 역할을 해야 
    • 엔티티(Entity) 클래스의 객체를 클라이언트에 응답으로 전송함으로써 계층 간의 역할 분리가 이루어지지 않음

위 문제는 공통적으로 DTO 클래스와 엔티티(Entity) 클래스의 역할 분리를 요구하는데,

 

들어가기 전에 먼저 왜 분리가 필요한지를 살피고 가자.


DTO 클래스 / Entity 클래스의 역할 분리가 필요한 이유

 

  1. 계층별 관심사의 분리

    • 두 클래스는 사용되는 계층이 다르기 때문에 기능에 대한 관심사가 다름
    • DTO 클래스 - API 계층에서 데이터를 주고받는 역할을 함
    • Entity 클래스 - 서비스 계층에서 데이터 액세스 계층과 연동해 비즈니스 로직의 결과를 다루는 역할을 함
    • 하나의 클래스나 메서드에 두 기능을 몰아넣은 것은 객체지향적이지 않음
  2. 코드 구성의 단순화

    • DTO 클래스의 유효성 검사 애너테이션이 Entity 클래스에서 사용되면 JPA와 뒤섞여 유지보수가 힘들어짐
  3. REST API 스펙의 독립성 확보

    • Entity 클래스를 응답으로 그대로 전달하게 되면 원치 않는 데이터(ex - 비밀번호)까지 전송될 수 있음
    • DTO 클래스로 변환후 사용해 원치 않는 정보를 노출하지 않으면서 원하는 정보만 제공할 수 있음

 

계속해서 문제를 해결하는 방법을 살필 텐데, DTO 클래스 ↔ Entity 클래스 사이의 변환을 맡는 Mapper 클래스의 작성이 그것이다. 

 

하나씩 살펴보자.

 

Mapper 클래스 구현

먼저 member의 DTO 클래스 ↔ Member 클래스(Entity 클래스) 사이를 변환해주는 매퍼(Mapper) 클래스를 구현해보자.

package com.gnidinger.member;

import org.springframework.stereotype.Component;

@Component  // (1) Spring Bean 등록
public class MemberMapper {

    // (2) MemberPostDto → Member 변환
    public Member memberPostDtoToMember(MemberPostDto memberPostDto) {
        return new Member(0L,
                memberPostDto.getEmail(),
                memberPostDto.getName(),
                memberPostDto.getPhone());
    }

    // (3) MemberPatchDto → Member 변환
    public Member memberPatchDtoToMember(MemberPatchDto memberPatchDto) {
        return new Member(memberPatchDto.getMemberId(),
                null,
                memberPatchDto.getName(),
                memberPatchDto.getPhone());
    }

    // (4) Member → MemberResponseDto 변환
    public MemberResponseDto memberToMemberResponseDto(Member member) {
        return new MemberResponseDto(member.getMemberId(),
                member.getEmail(),
                member.getName(),
                member.getPhone());
    }
}

코드에 대한 설명은 위에 포함되어 있다.

 

마지막에 사용한 MemberResponseDto 클래스는 응답 데이터 역할을 하는 DTO 클래스이다.

 

추가로 작성해 주자.

package com.gnidinger.member;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class MemberResponseDto {
    private long memberId;

    private String email;

    private String name;

    private String phone;
}

 

 

MemberController의 핸들러 메서드에 Mapper 클래스 적용

 

작성한 Mapper 클래스를 MemberController에 적용해 보자.

package com.gnidinger.member;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import javax.validation.constraints.Positive;
import java.util.List;
import java.util.stream.Collectors;


@RestController
@RequestMapping("/v4/members")
@Validated
public class MemberController {
    private final MemberService memberService;
    private final MemberMapper mapper;

    // (1) MemberMapper 주입
    public MemberController(MemberService memberService, MemberMapper mapper) {
        this.memberService = memberService;
        this.mapper = mapper;
    }

    @PostMapping
    public ResponseEntity postMember(@Valid @RequestBody MemberPostDto memberDto) {
        // (2) Mapper를 이용해서 MemberPostDto → Member 변환
        Member member = mapper.memberPostDtoToMember(memberDto);

        Member response = memberService.createMember(member);

        // (3) 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);

        // (4) Mapper를 이용해서 MemberPatchDto → Member변환
        Member response =
                memberService.updateMember(mapper.memberPatchDtoToMember(memberPatchDto));

        // (5) 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);

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

    @GetMapping
    public ResponseEntity getMembers() {
        List<Member> members = memberService.findMembers();

        // (7) 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);
    }
}

(7)의 경우, 리턴되는 값이 List 이므로 Stream을 이용해 List 안의 Member 객체들을 하나씩 꺼내서

 

MemberResponseDto 객체로 변환해주고 있는 것을 확인할 수 있다.

 

위와 같이 Mapper을 작성, 적용함으로써 위에서 언급한 두 개의 문제가 해결되었다.

 

  • MemberController 핸들러 메서드의 책임과 역할에 관한 문제

    • 핸들러 메서드는 요청 및 응답 데이터를 Service 클래스 - 클라이언트 사이에서 전송해주는 단순한 역할을 갖는 것이 좋음
    • 현재는 핸들러 메서드가 DTO 클래스를 엔티티(Entity) 객체로 변환하는 작업까지 맡고 있음

→ Mapper 클래스에게 DTO 클래스 Entity 클래스 변환 작업 위임, 역할 문제 해결.

 

  • 서비스 계층에서 사용되는 엔티티(Entity) 객체를 클라이언트에 응답으로 전송하는 문제

    • DTO 클래스는 API 계층에서, 엔티티(Entity) 클래스는 서비스 계층에서만 데이터를 처리하는 역할을 해야 함
    • 엔티티(Entity) 클래스의 객체를 클라이언트에 응답으로 전송함으로써 계층 간의 역할 분리가 이루어지지 않음

→ Mapper 클래스가 Entity 클래스 DTO 클래스 변환. Entity 클래스를 API 계층에서 직접적으로 사용하는 문제 해결.

 

위와 같이 Mapper를 이용하면 DTO 클래스 ↔ Entity 클래스 간의 변환은 깨끗하게 해결된다.

 

하지만 처리해야 하는 DTO 클래스가 늘어난다면 어떨까?(ex - coffee 정보)

 

기능이 늘어날 때마다 수작업으로 Mapper를 작성하는 것은 비효율적인 일이 될 것이다.

 

이때 문제를 해결해주는 라이브러리가 바로 MapStruct이다.

 

계속해서 알아보자.

 

 

MapStruct를 이용한 Mapper 자동 생성

 

MapStruct는 간단하게 말하면 (Java Bean 규약을 지키는) 객체 간의 Mapper를 자동으로 생성해주는 라이브러리이다.

 

여기선 당연하게도 DTO와 Entity 클래스 사이의 변환을 지원하기 위해 사용한다.

 

시작하기 전에 MapStruct 의존 라이브러리를 build.gradle에 추가하자.

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

MapStruct 기반의 Mapper 인터페이스 정의

 

먼저 위에 작성했던 MemberMapper 클래스를 대신해 아래와 같은 인터페이스를 정의한다.

package com.gnidinger.member;

import org.mapstruct.Mapper;

@Mapper(componentModel = "spring")  // (1)
public interface MemberMapper {
    Member memberPostDtoToMember(MemberPostDto memberPostDto);
    Member memberPatchDtoToMember(MemberPatchDto memberPatchDto);
    MemberResponseDto memberToMemberResponseDto(Member member);
}

(1)의 @Mapper 애너테이션으로 MapStruct의 인터페이스임을 선언한다.

 

괄호 안의 componentModel = "spring" 속성은 스프링 빈 등록을 위한 것이다.

 

작성을 마치고 프로젝트 빌드를 하면, 자동 생성된 구현 클래스가 아래 위치에 생성된다.

 

내용은 아래와 같다.

package com.gnidinger.member;

import org.springframework.stereotype.Component;

@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;
        }
    }
}

위에서 수작업으로 작성했던 Mapper 클래스와 크게 다르지 않음을 확인할 수 있다.

 

 

MemberController의 핸들러 메서드에 MapStruct 적용

 

마지막으로 컨트롤러 클래스에 변경사항을 적용해 보자.

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

@RestController
@RequestMapping("/v5/members") // (2) URI 버전 변경
@Validated
public class MemberController {
		...
		...
}

코드는 전혀 손대지 않고 (1) 패키지 변경과 (2) URI 버전 변경만 한 것을 알 수 있다.

 

만약 모든 클래스와 인터페이스를 한 폴더에 만들었다면 (1) 역시 필요 없게 된다.

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/06   »
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
글 보관함