CodeStates/Section 03

[Spring] Spring MVC - 서비스 계층

NYinJP 2023. 2. 16. 00:21

 

 

어제까지 학습에서 API 계층에서 클라이언트와 서버가 요청, 응답을 Controller를 통해서 하는 것을 알았다. 

이제 API 계층과 서비스 계층을 연동해 보자. 서비스 계층은 API 계층에서 전달받은 클라이언트의 요청 데이터를 기반으로 실질적인 비즈니스 요구사항을 처리하는 계층이다.

Spring의 DI를 이용해서 API 계층과 비즈니스 계층을 연동하고, API 계층에서 전달받은 DTO객체를 비즈니스 계층에서

도메인 엔티티로 변환하여 전달하는 방법을 알아보자!

  • Spring의 DI 기능을 이용해서 API 계층과 서비스 계층을 연동할 수 있다.
  • API 계층에서 전달받은 DTO 객체를 서비스 계층의 도메인 엔티티(Entity) 객체로 변환할 수 있다.
API 계층과 서비스 계층을 연동?
API 계층에서 구현한 Controller 클래스가 서비스 계층의 Service 클래스와 메서드 호출을 통해 서로 상호작용 한다는 뜻

 

💬 Service의 의미

도메인(우리가 현실 세계에서 접하는 업무의 한 영역) 업무 영역을 구현하는 비즈니스 로직이다.
도메인 모델은 '빈약한 도메인 모델'과 '풍부한 도메인 모델'로 구분할 수 있다. 이러한 도메인 모델의 구분은 도메인 주도 설계(DDD, Domain Driven Design)과 관련 있습니다. 차후 배우게 될 Spring Data JDBC와 DDD가 밀접한 연관이 있습니다.


Service란 용어를 보면 "비즈니스 로직을 처리하는 서비스 계층의 서비스 클래스이구나"라고 생각.

 

1.
비즈니스 로직을 처리할 서비스 클래스 생성하기!

먼저 API 계층인 컨트롤러 클래스에서 클라이언트의 요구사항을 잘 전달받아야 합니다. 그리고 그 Controller를 기반으로 서비스 계층의 로직을 설계합니다.

Controller 클래스 안에 있는 핸들러 메서드를 살펴봅니다. MemberController의 핸들러 메서드들의 역할에는

  • postMember() : 1명의 회원 등록을 위한 요청을 전달받는다.
  • patchMember() : 1명의 회원 수정을 위한 요청을 전달받는다.
  • getMember() : 1명의 회원 정보 조회를 위한 요청을 전달받는다.
  • getMembers() : N명의 회원 정보 조회를 위한 요청을 전달받는다.
  • deleteMember() : 1명의 회원 정보 삭제를 위한 요청을 전달받는다.

결론 : 이 다섯 개의 핸들러 메서드가 전달받은 요청을 처리하는 메서드들을 Service 클래스 내부에서 구현하면 됩니다!

 

MemberService 클래스 내부의 클라이언트 요청을 처리할 메서드들. 클라이언트로부터 데이터를 전달받는 핸들러 메서드와 1:1 매치된다.

  • createMember() : 회원 정보 등록
  • updateMember() : 특정 회원 정보 수정
  • findMember() : 특정 회원 정보 조회
  • findMembers() : 모든 회원 조회
  • deleteMember() : 특정 회원 삭제

그런데! 이 MemberService 메서드들의 반환타입이 Member입니다!

처음에 MemberController의 핸들러 메서드가 클라이언트의 요청 데이터를 전달받을 때 DTO 클래스를 사용했습니다. 

 

DTO가 API 계층에서 클라이언트의 Request Body를 전달받고 클라이언트에게 되돌려 줄 응답 데이터를 담는 역할을 한다면, Member 클래스는 API 계층에서 전달받은 요청 데이터를 기반으로 서비스 계층에서 비즈니스 로직을 처리하기 위해 필요한 데이터를 전달받고, 비즈니스 로직을 처리한 후에는 결과 값을 다시 API 계층으로 리턴해주는 역할을 합니다.

 

Member 클래스처럼 데이터 액세스 계층과 연동하면서 비즈니스 로직을 처리하기 위해 필요한 데이터를 담는 역할을 하는 클래스를 도메인 엔티티(Entity) 클래스라고 부릅니다.

 

 

2.
도메인 엔티티 클래스와 서비스 클래스 구현하기!
package com.codestates.member.entity;

import lombok.*;

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

도메인 엔티티 클래스의 역할은 API 계층에서 전달받은 요청 데이터를 기반으로 서비스 계층에서 비즈니스 로직을 처리하기 위해 필요한 데이터를 전달받아서 비즈니스 로직을 처리한 후에는 결과값을 다시 API 계층으로 리턴해주는 역할을 합니다.

 

DTO 클래스에서 사용한 멤버변수들이 모두 포함되어 있습니다. MemberPostDto + MemberPatchDto

 

애너테이션(lombok 라이브러리에서 제공함) : lombok 라이브러리에서 제공! 잘 사용하면 아주 편리해!

  • @Getter / @Setter : 게터, 세터 메서드 자동 추가
  • @NoArgsConstructor : 기본 생성자 자동 추가
  • @AllArgsConstructor : 모든 멤버 변수를 파라미터로 갖는 생성자 자동 추가
package com.codestates.member.service;

// (클라이언트 요청)비즈니스 로직을 처리할 서비스 계층입니다.

import com.codestates.member.entity.Member;
import org.springframework.stereotype.Service;

import java.util.*;
@Service
public class MemberService {
    public Member createMember(Member member){
        Member createdMember = member;
        return createdMember;
    }
    public Member updateMember(Member member){
        Member updatedMember = member;
        return updatedMember;
    }
    public Member findMember(long memberId){
        Member member = new Member(memberId, "hgd@gmail.com","홍길동","010-1234-5678");
        return member;
    }
    public List<Member> findMembers(){
        List<Member> members=List.of(
                new Member(1,"hgd@gamil.com","홍길동","010-1234-5678"),
                new Member(2,"lml@gamil.com","이몽룡","010-1111-2222")
        );
        return members;
    }
    public void deleteMember(long memberId){

    }

}

아직 DB와 연동하지 않았기 때문에 지금 메서드는 그저 전달받은 Member객체를 그대로 되돌려주고 있다.

 

나의 부족한 개념

💬 List.of? Arrays.asList()?

둘 다 리스트를 생성해주는 메서드이다. ( ) 괄호 안에 들어오는 값을 리스트로 만들어준다.
public class Main {
    public static void main(String[] args){
        List<Integer> list1 = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
        List<Integer> list2 = List.of(10,11,12,13,14,15,16,17);

        list1.stream().forEach(System.out::print);
        System.out.println();
        list2.stream().forEach(System.out::print);
    }
}

 

그런데 그 둘은 차이점이 존재한다. asList로 만든 리스트를 1번 List.of로 만든 리스트를 2번이라 하겠다. 

1. set() 메서드 - 값 변경
list의 값을 인덱스와 변경할 값 두가지의 매개변수를 넣어서 변경해 주는
set 메서드를 쓸 때 1번은 가능. 2번은 불가능하다

2. null 값
1번은 허용. 2번은 허용하지 않는다.

3. 원본 데이터 소스의 변화
1번은 원본 데이터 값(예. 배열)이 바뀌면 반응하고 2번은 반응하지 않는다.

 

3 - 1.
API 계층과 서비스 계층 연동하기!

멤버 서비스 클래스에 넣어줄 데이터를 전달받는 Member 도메인 엔티티 클래스를 만들었습니다. 이제 멤버 Controller를 해당 기능을 사용할 수 있도록 바꿔보겠습니다!

 

어떤 클래스가 다른 클래스의 기능을 사용하기 위해서는

그 클래스의 인스턴스를 생성해서 인스턴스를 통해 클래스의 메서드를 호출해서 사용합니다. 

인스턴스의 생성은 new 연산자를 사용합니다!

 

복잡한 로직이 아니라고 합니다!

그저 MemberService 클래스의 기능(메서드)을 사용하기 위해 변경한 것일 뿐!

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

    public MemberController() {
        this.memberService = new MemberService(); // (1)
    }

    @PostMapping
    public ResponseEntity postMember(@Valid @RequestBody MemberPostDto memberDto) {
        // (2)
        Member member = new Member();
        member.setEmail(memberDto.getEmail());
        member.setName(memberDto.getName());
        member.setPhone(memberDto.getPhone());

        // (3)
        Member response = memberService.createMember(member);

        return new ResponseEntity<>(response, HttpStatus.CREATED);
    }

    @PatchMapping("/{member-id}")
    public ResponseEntity patchMember(
            @PathVariable("member-id") @Positive long memberId,
            @Valid @RequestBody MemberPatchDto memberPatchDto) {
        memberPatchDto.setMemberId(memberId);

        // (4)
        Member member = new Member();
        member.setMemberId(memberPatchDto.getMemberId());
        member.setName(memberPatchDto.getName());
        member.setPhone(memberPatchDto.getPhone());

        // (5)
        Member response = memberService.updateMember(member);

        return new ResponseEntity<>(response, HttpStatus.OK);
    }

    @GetMapping("/{member-id}")
    public ResponseEntity getMember(
            @PathVariable("member-id") @Positive long memberId) {
        // (6)
        Member response = memberService.findMember(memberId);
        return new ResponseEntity<>(response, HttpStatus.OK);
    }

    @GetMapping
    public ResponseEntity getMembers() {
        // (7)
        List<Member> response = memberService.findMembers();
        return new ResponseEntity<>(response, HttpStatus.OK);
    }

    @DeleteMapping("/{member-id}")
    public ResponseEntity deleteMember(
            @PathVariable("member-id") @Positive long memberId) {
        System.out.println("# delete member");

        // (8)
        memberService.deleteMember(memberId);

        return new ResponseEntity(HttpStatus.NO_CONTENT);
    }
}

 

코드 이해하기

  • postMember() 메서드

클라이언트의 요청 데이터를 저장한 memberPostDto 객체를 입력값으로 받습니다.

멤버 클래스의 객체를 new 연산자로 생성합니다. 앞에서 설명햇듯이 Member 클래스의 기능을 사용하기 위함입니다.

객체 member의 세터 메서드를 이용해 memberPostDto안에 저장된 클라이언트의 정보들을 게터 메서드로 가져와 저장합니다. 나머지도 동일합니다. 

멤버 객체인 response를 만들어서 멤버서비스클래스의 createMember메서드를 이용해 비즈니스 로직을 처리합니다.

이 지점이 API와 서비스 계층과의 연결 지점입니다. 

ResponseEntity로 감싸 클라이언트의 요청에 대한 응답을 합니다.

 

멤버컨트롤러클래스에서 멤버서비스클래스를 이용할 수 있는 이유는  멤버서비스 타입의 객체를 담을 참조변수를 final로 선언해 주었고 그 참조변수에 멤버 서비스 객체를 하나 생성자를 통해 할당해 주었습니다. 

 

💬 final로 선언한 이유

객체 변수에 final로 선언하면 그 변수에 다른 참조 값을 지정할 수 없습니다. 원시 타입과 동일하게 한번 쓰여진 변수는 재변경 불가합니다. 단, 객체 자체가 immutable 하다는 의미는 아닙니다. 객체의 변수는 변경 가능합니다.
재할당을 막아놓았다. 

아래와 같다. 

    private final MemberService memberService;

    public MemberController() {
        this.memberService =  new MemberService();
    }

 

  • deleteMember() 메서드

특정 memberId를 가진 회원을 삭제하는 메서드이다. 멤버서비스의 deleteMember()메서드 안에 memberId를 인자로 넣어준다. 멤버서비스클래스에 있는 메서드를 호출하는 이 지점이 API와 서비스 계층과의 연결 지점입니다. 

 

조회, 삭제와 같으 경우는 어렵지 않은데! 데이터 응답, 전달하는 부분에 있어서는 Member 클래스의 객체에 정보를 채워줘야 한다는 것을 기억하자. 

 

 

3 - 2.
API 계층과 서비스 계층 연동하기! - DI 적용하기

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

		// (1) MemberController의 변경 포인트
    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }
		...
}

이전에는 MemberController의 생성자에서 new 키워드를 사용하여 MemberService의 객체를 생성했습니다.

MemberController와 MemberService가 강하게 결합(Tight Coupling)되어 있는 상태입니다. 느슨한 결합으로 바꿔줍시다.

DI를 적용한 바뀐 코드에서는 MemberController의 생성자 파라미터로 MemberService의 객체를 주입(Injection) 받았습니다.(생성자 주입) Spring이 알아서 해준다고 하네요!

💬 생성자 주입

생성자 주입은 생성자의 호출 시점에 1회 호출되는 것이 보장된다. 그렇기 때문에 주입받은 객체가 변하지 않거나, 반드시 객체의 주입이 필요한 경우에 강제하기 위해 사용할 수 있다. 또한 Spring 프레임워크에서는 생성자 주입을 적극 지원하고 있기 때문에, 생성자가 1개만 있을 경우에 @Autowired를 생략해도 주입이 가능하도록 편의성을 제공하고 있다.

 

그런데 Spring에서 DI를 통해서 어떤 객체를 주입받기 위해서는

주입을 받는 클래스와 주입 대상 클래스 모두 Spring Bean이어야 합니다. 

 

MemberController에서는 @RestController,

MemberService에서는 @Service 애너테이션이 추가됨으로써

Spring Bean이 되어  Spring 컨테이너에 의해 관리하는 객체가 된답니다!

 

생성자 방식의 DI는 생성자가 하나일 경우에는 @Autowired 애너테이션을 추가하지 않아도 DI가 적용됩니다. 

DI(의존성 종속, Dependency Injection)
클래스 간의 의존관계를 스프링 컨테이너가 자동으로 연결해 주는 것

 

현재까지의 문제점

  • Controller 핸들러 메서드는 클라이언트의 요청을 서비스 클래스로 전달하고, 전달받은 것을 클라이언트에게 응답하는 단순한 역할만을 하는 것이 좋다. 
    • 현재는 핸들러 메서드가 서비스 클래스로 전달하기 위해 엔티티 객체에 값을 넣어주는 작업까지 하고 있습니다.
    • member.setEmail(memberPostDto.getEmail()); // "나의 일이 아니란다"
  • 엔티티 객체를 클라이언트의 응답으로 전송하고 있습니다.
    • 엔티티 객체는 다시 Controller로 돌아와 DTO로 변환된 뒤에 핸들러 메서드가 클라이언트에게 응답으로 전송해야 합니다. 
    • API 계층과 서비스 계층간의 역할 분리가 이루어지지 않았습니다. 
 

 

4.
매퍼(Mapper)를 이용해 DTO 클래스 ↔ Entity클래스 매핑

문제점

  • MemberController의 핸들러 메서드가 DTO 클래스를 엔티티(Entity) 클래스로 변환하는 작업까지 도맡아서 하고 있다.
  • 엔티티(Entity) 클래스의 객체를 클라이언트의 응답으로 전송함으로써 계층 간의 역할 분리가 이루어지지 않았다.

DTO 클래스를 엔티티 클래스로 변환하는 작업을 핸들러 메서드가 아니라 다른 클래스가 하게 합니다! 

또, 엔티티 클래스의 객체를 DTO 클래스의 객체로 변환한 뒤 핸들러 메서드가 응답하게 합니다!

 

결론 : DTO 클래스와 Entity 클래스를 서로 변환해 주는 매퍼(Mapper)가 필요하다! 

 

< 매퍼클래스 >

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

매퍼 클래스를 Spring Bean으로 등록하기 위해서 @Component 애너테이션을 추가해 줍니다.

그저 DTO 객체를 Entity 객체로 만들어주는 코드이다. 또 마지막은 반대로 해준다! Member클래스를 MemberResponseDto 클래스로 변환해 준다. 마지막에 있는 MemberResponseDto는 응답 데이터의 역할을 해주는 DTO 클래스입니다. 

 

Controller의 핸들러 메서드에 적용해 보겠습니다.

만들어놓은 Mapper 클래스를 사용하기 위해서 먼저 생성자 주입으로 DI 해줍니다. 

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

        Member response = memberService.createMember(member);

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

멤버를 등록해주는 메서드를 먼저 살펴보면, mapper 인스턴스의 memberPostDtoToMember 메서드를 이용해 memberDto에 저장된 클라이언트의 정보를 입력받아서 메서드 내부 동작으로 Member 클래스 형태로 만들어줍니다. 핸들러 메서드에서 진행하던 멤버 엔티티 클래스로의 변환을 Mapper 클래스가 하도록 하였습니다. 이제 이렇게 변환된 값을 서비스 계층으로 보낼 수 있습니다! createMember 메서드로 멤버를 저장합니다. response는 멤버 엔티티 객체입니다. 이 값을 클라이언트로 반환하려면 DTO 클래스로 만들어줘야 합니다. DTO 클래스로 클라이언트로 값을 전달받았던 것을 생각하면 돌려줄 때도 똑같이 한다고 생각하면 됩니다. 멤버 엔티티를 다시 DTO로 바꾸려면? 매퍼 클래스의 메서드를 사용합니다. 구현해 놓았던 memberToMemberResponseDto안에 멤버 객체를 넣어주면

public MemberResponseDto memberToMemberResponseDto(Member member) {
        return new MemberResponseDto(member.getMemberId(),
                member.getEmail(), 
                member.getName(), 
                member.getPhone());
    }

 이렇게 DTO객체로 만들어서 반환해줍니다. 반환해 줄 때도 DTO클래스로 반환! 기억해 둡시다!

 

Mapper 클래스를 사용함으로써 앞에서 살펴본 MemberController의 문제점이 아래와 같이 해결되었습니다.

  • MemberController의 핸들러 메서드가 DTO 클래스를 엔티티(Entity) 클래스로 변환하는 작업했던 문제
    • MemberMapper에게 DTO 클래스 → 엔티티(Entity) 클래스로 변환하는 작업을 위임함으로써 MemberController는 더 이상 두 클래스의 변환 작업을 신경 쓰지 않아도 됩니다.
    • 역할 분리로 인해 코드 자체가 깔끔해졌습니다.
  • 엔티티(Entity) 클래스의 객체를 클라이언트의 응답으로 전송하는 문제
    • MemberMapper가 엔티티(Entity) 클래스를 DTO 클래스로 변환해 주기 때문에 서비스 계층에 있는 엔티티(Entity) 클래스를 API 계층에서 직접적으로 사용하는 문제가 해결되었습니다.

 

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

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

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

        Member response = memberService.createMember(member);

				// (3) 매퍼를 이용해서 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) 매퍼를 이용해서 MemberPatchDto를 Member로 변환
        Member response = 
              memberService.updateMember(mapper.memberPatchDtoToMember(memberPatchDto));

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

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

				// (7) 매퍼를 이용해서 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);
    }
}

 

5.
Mapper 클래스를 누군가 자동으로 만들어준다면... MapStruct

앞에서 보았듯이 Mapper 클래스를 이용하면 DTO클래스와 엔티티 클래스의 변환 작업을 깔끔하게 처리할 수 있습니다. 그런데 도메인 업무 기능이 늘어날 때마다 Mapper클래스를 개발자가 일일이 수작업으로 구현하는 것은 상당히 번거로운 일입니다. 해결방법이 있습니다. MapStruct를 이용해 매퍼 클래스를 자동으로 구현해 줌으로써 개발자의 생산성을 향상할 수 있습니다.

💬 
MapStruct는 DTO 클래스처럼 Java Bean 규약을 지키는 객체들 간의 변환 기능을 제공하는
매퍼(Mapper) 구현 클래스를 자동으로 생성해 주는 코드 자동 생성기입니다.

매퍼 자동 생성을 위해서는 매퍼 인터페이스를 먼저 정의해야 합니다.

 

 

인텔리제이 오른쪽에 Gradle(그레이들) task를 확인할 수 있다. 

 

MemberMapperImpl 클래스는 언제, 어떻게 생성될까요? IntelliJ IDE의 오른쪽 상단의 [Gradle] 탭을 클릭한 후, [프로젝트 명 > Tasks 디렉터리 > build 디렉토리 > build task]를 더블 클릭하면 MapStruct로 정의된 인터페이스의 구현 클래스가 생성됩니다.

MemberMapperImpl 클래스는 어디에 생성될까요?
 IntelliJ IDE의 좌측에서 [Project 탭 > 프로젝트명 > build] 디렉터리 내의 MemberMapper 인터페이스가 위치한 패키지 안에 생성됩니다.

 


실습 100% 이해해 보기!

실습 내용은 CoffeeController클래스와 CoffeeService 클래스를 연동하는 것이다. 

 

1.

먼저 핸들러 메서드를 보면서 필요한 서비스 계층에 생성할 메서드 파악하기. 1:1 매칭된다.

각각의 핸들러 메서드 ➡ 서비스 계층에서 자신과 해당하는 메서드 가져와 사용 

  • 커피정보 등록에 필요한 정보를 전달받는
  • 하나의 커피 정보 조회 요청받는
  • 모든 커피 정보 조회 요청받는 
  • 커피 정보 수정에 필요한 정보를 전달받는
  • 커피 정보 삭제를 요청받는

이에 따라 서비스 클래스의 메서드를 구현하면 된다.

아직 DB와 연동하지 않았기 때문에 비즈니스 로직에서 크게 하는 일은 따로 없다. 그저 전달받은 값을 그대로 리턴해주거나 정보를 요청받는 경우에는 필요한 데이터도 DB에서 갖고 오는 게 아니라 Stub 데이터로 처리하였다. 어떤 요청을 보내든지 같은 값만 응답해 줄 것이다. 

 

@Service
public class CoffeeService {

    public Coffee createCoffee(Coffee coffee){
        Coffee createdCoffee= coffee;
        return createdCoffee;
    }
    public Coffee updateCoffee(Coffee coffee){
        Coffee updatedCoffee= coffee;
        return updatedCoffee;
    }
    public Coffee findCoffee(long coffeeId){
        Coffee findCoffee = new Coffee(
                coffeeId, "아메리카노","Americano",2500
        );
        return findCoffee;
    }
    public List<Coffee> findCoffees(){
        List<Coffee> coffees = Arrays.asList(
                new Coffee(1L, "아메리카노","Americano",2500),
                new Coffee(2L, "카라멜 라떼","Caramel Latte",5000)
        );
        return coffees;
    }
    public void deleteCoffee(long coffeeId){
        // not implement
    }
}

 

2.

Coffee 엔티티

서비스클래스의 데이터는 Coffee 엔티티 클래스 타입이다. 커피 도메인 엔티티 클래스를 구현할 때는 여태 쓰인 모든 DTO클래스의 필드들을 써주면 된다. 서비스 계층은 엔티티 클래스! 컨트롤러 클래스는 DTO 클래스! 생각하지 말고 받아들이자... 근데 서비스 계층에서 커피 엔티티로 만드는 게 아니라 이미 만들어진 커피 엔티티를 전달받아 그대로 커피 엔티티 타입으로 반환하고 있다. 이 일은 MapStruct가 해준다.  

 

3. DTO 클래스 

인터페이스로 구현해야 한다. 그전에 DTO클래스들을 먼저 잘 만들어둬야 한다?

클라이언트가 정보를 전달해 줄 때 postDto

클라이언트가 수정된 정보를 전달해줄때 patchDto

responseDto는 엔티티를 Dto로 만들어줄 때 필요한 Dto클래스이다. 

 

4. MapStruct 

MapStruct를 사용하기 위해 Mapper 클래스는 인터페이스로 구현한다. 나머지는 Spring이 알아서 만들어준다.

@Mapper(componentModel = "spring")

이 어노테이션을 꼭 붙여야 해당 인터페이스는 MapStruct의 매퍼 인터페이스로 정의가 된다.

 

@Mapper 애너테이션의 애트리뷰트로 componentModel = "spring"을 지정해 주면Spring의 Bean으로 등록이 된다는 사실을 꼭 기억할 것...! 그럼 이 인터페이스 안에는 뭐가 들어가느냐?! Mapper 클래스의 의미는 사용하는 이유는 핸들러 메서드가 비즈니스 로직에 데이터를 전달해 줄 때 도메인 엔티티 타입으로 바꿔주기 위해 직접 하던 일을 Mapper 클래스가 하도록 새로 생성한 클래스이다. 따라서 핸들러 메서드로부터 얻은 데이터들을 도메인 엔티티 타입으로 바꿔줘야 한다. 따라서 반환 타입은 엔티티 클래스 타입이 되고 안에 메서드 이름으로는 DtoToCoffee 형식, 매개변수로는 핸들러메서드가 전달받은 값(DTO)을 넣으면 되지 않을까?? 마지막에 커피 엔티티 클래스값을 DTO값으로 바꿔주는 것도 추가했다.

 

인터페이스 작성을 완료하면 Gradle의 build tasks를 실행시켜 주면 작성한 인터페이스를 기반으로 매퍼 구현 클래스를 자동으로 생성해 준다!

💬 인터페이스

인터페이스는 추상 메서드만을 가질 수 있고 구현메서드는 가질 수 없는 일종의 추상 클래스입니다. 추상화 정도가 높아서 추상메서드상수만을 멤버로 갖는다. 그 외에 어떠한 다른 요소도 가질 수 없다.   
위의 추상 클래스와 다른 점은 자식 클래스들의 기능 유사도인데요. 추상클래스는 서로 비슷한 기능을 하는 클래스들을 묶을 때 사용하며, 인터페이스는 서로 다른 기능을 하는 클래스들을 묶을 때 인터페이스를 사용합니다.

추상 메서드를 정의할 때는
public abstract 반환타입 메서드이름(매개변수목록);

public abstract가 앞에 붙는데 생략 가능하며 편의상 생략하는 경우가 많다. 인터페이스에 정의된 멤버에 예외 없이 적용되는 조건이기 때문에 컴파일러가 알아서 추가해 주기 때문이다. 

public abstract 생략 가능.

5.

커피 컨트롤러와 커피 서비스 연동하기

컨트롤러 클래스를 수정한다. 수정할 방향은

  • 매퍼와 커피서비스 DI 하기
  • DTO를 커피 엔티티로 바꿔주는 매퍼 사용
  • 매퍼로 변환된 값을 커피 서비스 메서드에 넣어주기

Order 도메인도 코드 구현하렴~!

 

결론

다른 클래스 메서드들을 객체 생성 없이 바로 쓰고 싶어서 클래스 타입 참조변수 하나 만들고 생성자 DI

어떤 클래스의 어떤 메서드? 어떤 입력값과 반환값?

을 생각하면 쉬운 거 같다.