1. DTO(Data Transfer Object)

  • DTO는 Data Transfer Object의 약자로 마틴 파울러(Martin Fowler)가 ‘Patterns of Enterprise Application Architecture’ 라는 책에서 처음 소개한 엔터프라이즈 애플리케이션 아키텍처 패턴이다
  • Transfer라는 의미에서 알 수 있듯이 데이터를 전송하기 위한 용도의 객체이다
  • 데이터 전송은 클라이언트에서 서버 쪽으로 전송하는 요청 데이터, 서버에서 클라이언트 쪽으로 전송하는 응답 데이터의 형식으로 클라이언트와 서버 간에 데이터 전송이 이루어진다

 

2. DTO를 사용하는 이유

 1) 코드의 간결성

public class MemberController {
    @PostMapping

    public /*String*/ResponseEntity postMember(@RequestHeader Map<String, String> headers,
                                               @RequestParam("email")String email,
                                               @RequestParam("name")String name,
                                               @RequestParam("phone")String phone) {

//        코드(1)
//        System.out.println("# email: " + email);
//        System.out.println("# name: " + name);
//        System.out.println("# phone: " + phone);

//        JSON문자열 응답 타입을 수작업 코드에서 Map객체로 변경->produce애트리뷰트를 삭제할 수 있다
        //MAp 객체를 리턴하면 내부적으로 응답 데이터를 JSON데이터로 자동 변환해야 한다고 인식한다
//            Map<String, String> map = new HashMap<>();
//            map.put("email", email);
//            map.put("name", name);
//            map.put("phone", phone);

        //코드(2)
        //@RequestHeader Map을 사용하여 전체 헤더 정보를 받아 온다
        for (Map.Entry<String, String> entry : headers.entrySet()){
            System.out.println("key: " + entry.getKey() +
                    ", value: " + entry.getValue());
        }
        //리턴 값을 변경된 ResponseEntity로 대체
        //ResponseEntity 객체를 생성하고 생성자 파라미터로 map과 HttpStatus.CREATED를 반환한다
        //HttpStatus.CREATED 는 201, created를 의미한다

//        코드(1)
//            return new ResponseEntity<>(map, HttpStatus.CREATED);

        //코드(2)
        //@RequestHeader Map 을 사용함으로 리턴 값의 코드가 변경된다
        return new ResponseEntity<>(new Member(email, name, phone), HttpStatus.CREATED);
  • 코드(1)에서 작성한 MemberController의 postMember() 핸들러 메서드에서 개선해야 될 부분을 찾아본다
    - 코드(1)에서는 회원 정보를 저장하기 위해서 총 세 개의 @RequestParam 애너테이션을 사용하고 있다
    - 요청 데이터는 회원의 주소 정보, 로그인 패스워드, 패스워드 확인 정보 등 추가 내용이 회원 정보에 포함될 수 있다
    - postMember()에 파라미터로 추가되는 @RequestParam의 개수는 늘어날 수 밖에 없다
  • 클라이언트의 요청 데이터를 하나의 객체로 모두 전달 받을 수 있다면 코드 자체가 간결해질 수 있다
    - DTO 클래스는 요청 데이터를 하나의 객체로 전달 받는 역할을 한다
//    public /*String*/ResponseEntity postMember(@RequestHeader Map<String, String> headers,
//                                               @RequestParam("email")String email,
//                                               @RequestParam("name")String name,
//                                               @RequestParam("phone")String phone) {
    //코드(3)
    public class ResponseEntity postMember(MemberDto memberDto){
    
    로 변경하면
    
    //코드(2)
        //@RequestHeader Map 을 사용함으로 리턴 값의 코드가 변경된다
//        return new ResponseEntity<>(new Member(email, name, phone), HttpStatus.CREATED);

        //코드(3)
        return new ResponseEntity<MemberDto>(memberDto, HttpStatus.CREATED);
        
        으로 변경된다
  • postMember()에서 @RequestParam을 사용하는 부분이 사라지고 MemberDto memberDto 를 추가한다
  • 비즈니스 로직이 없는 상태이지만
    - @RequestParam을 통해 전달 받은 요청 데이터들을 Map에 추가하는 로직을 삭제하고
    - MemberDto 객체를 ResponseEntity 클래스의 생성자 파라미터로 전달하도록 변경하였다
  • 일단, 코드는 매우 간결해졌다

 2) 데이터 유효성(Validation) 검증의 단순화

  • 지금까지 작성한 Controller의 핸들러 메서드는 클라이언트의 요청 데이터에 대한 유효성 검증 작업을 거치지 않았다
  • 클라이언트 쪽에서 회원 정보의 email 주소를 ‘1111@gmail.com’ 같은 이메일 주소 형식이 아닌 ‘1111’ 같은 단순 문자열로 전송해도 정상적으로 핸들러 메서드 쪽에서 전달 받을 수 있다
  • 서버 쪽에서 유효한 데이터를 전달 받기 위해 데이터를 검증하는 것을 유효성(Validation)검증이라고 한다
@RestController
@RequestMapping("/no-dto-validation/v1/members")
public class MemberController {
    @PostMapping
    public ResponseEntity postMember(@RequestParam("email") String email,
                                     @RequestParam("name") String name,
                                     @RequestParam("phone") String phone) {
				// (1) email 유효성 검증
        if (!email.matches("^[a-zA-Z0-9_!#$%&'\\*+/=?{|}~^.-]+@[a-zA-Z0-9.-]+$")) {
            throw new InvalidParameterException();
        }
        Map<String, String> map = new HashMap<>();
        map.put("email", email);
        map.put("name", name);
        map.put("phone", phone);

        return new ResponseEntity<Map>(map, HttpStatus.CREATED);
    }
		...
		...
}
  • (1)은 정규 표현식을 사용해서 이메일 주소의 유효성을 검증하는 로직으로 핸들러 메서드 내에 직접적으로 포함되어 있다
  • name, phone에 대한 유효성 검증도 필요하면 핸들러 내의 코드는 유효성을 검증하는 로직들로 코드가 복잡해 진다
  • HTTP 요청을 전달 받는 핸들러 메서드는 요청을 전달 받는 것이 주 목적이기 때문에 최대한 간결하게 작성되는 것이 좋다
  • 핸들러 메서드 내부에 있는 유효성 검사 로직을 외부로 뺄 수 있다면 핸들러 메서드의 간결함을 유지할 수 있을 것이다
  • DTO 클래스를 사용하면 유효성 검증 로직을 DTO 클래스로 빼내어 핸들러 메서드의 간결함을 유지할 수 있다
import javax.validation.constraints.Email;
public class MemberDto {
    //MemberDto의 email 멤버 변수에 유효성 검증을 적용한다
    //email 멤버 변수에 @Email을 추가하여 클라이언트의 요청 데이터에 유효한 이메일 주소가 포함되어 있지 않을 경우
    //유효성 검증에 실패하므로 클라이언트의 요청은 거부(reject)된다
    //MemberDto 클래스에서 이메일에 대한 유효성 검증을 진행하므로
    //MemberController의 postMember() 핸들러 메서드는 코드(5)와 같이 간결해진다
    @Email  //'org.springframework.boot:spring-boot-starter-validation'을 추가해 주어야 한다
    private String email;
    private String name;
    private String phone;

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }
}
  • 코드(5)가 적용된 MemberController class 코드이다
@RestController
@RequestMapping("v1/members"/*,produce=MediaType.APPLICATION_JSON_VALUE*/)

public class MemberController {
    @PostMapping
//  코드(1)
//    public /*String*/ResponseEntity postMember(@RequestHeader Map<String, String> headers,
//                                               @RequestParam("email")String email,
//                                               @RequestParam("name")String name,
//                                               @RequestParam("phone")String phone) {
    //코드(3)
//    public class ResponseEntity postMember(MemberDto memberDto){

    //코드(5)
    public ResponseEntity postMember (@Valid/*'org.springframework.boot:spring-boot-starter-validation'을 추가해 주어야 한다*/MemberDto memberDto) {

//        코드(1)
//        System.out.println("# email: " + email);
//        System.out.println("# name: " + name);
//        System.out.println("# phone: " + phone);

//        JSON문자열 응답 타입을 수작업 코드에서 Map객체로 변경->produce애트리뷰트를 삭제할 수 있다
        //MAp 객체를 리턴하면 내부적으로 응답 데이터를 JSON데이터로 자동 변환해야 한다고 인식한다
//            Map<String, String> map = new HashMap<>();
//            map.put("email", email);
//            map.put("name", name);
//            map.put("phone", phone);

        //코드(4)
        //email 유효성 검증 : 코드(1) @RequestParam 과 함게 사용 시 에러 없음
//        if (!email.matches("^[a-zA-Z0-9_!#$%&'\\*+/=?{|}~^.-]+@[a-zA-Z0-9.-]+$")) {
//            throw new InvalidParameterException();
//        }
//        Map<String, String> map = new HashMap<>();
//        map.put("email", email);
//        map.put("name", name);
//        map.put("phone", phone);

        //코드(2)
        //@RequestHeader Map을 사용하여 전체 헤더 정보를 받아 온다
//        for (Map.Entry<String, String> entry : headers.entrySet()){
//            System.out.println("key: " + entry.getKey() +
//                    ", value: " + entry.getValue());
//        }
        //리턴 값을 변경된 ResponseEntity로 대체
        //ResponseEntity 객체를 생성하고 생성자 파라미터로 map과 HttpStatus.CREATED를 반환한다
        //HttpStatus.CREATED 는 201, created를 의미한다

//        코드(1)
//            return new ResponseEntity<>(map, HttpStatus.CREATED);

        //코드(2)
        //@RequestHeader Map 을 사용함으로 리턴 값의 코드가 변경된다
//        return new ResponseEntity<>(new Member(email, name, phone), HttpStatus.CREATED);

        //코드(3)
        return new ResponseEntity<MemberDto>(memberDto, HttpStatus.CREATED);

//        코드(1)
//        String reponse =
//                "{\"" +
//                    "email\":\""+email+"\"," +
//                    "\"name\":\""+name+"\",\"" +
//                    "phone\":\"" + phone+
//                "\"}";
//        return reponse;
    }
  • @Valid 애너테이션은 MemberDto 객체에 유효성 검증을 적용하게 해주는 애너테이션이다
    - @Valid를 사용하기 위해서는 build.gradle에 아래 코드를 적용해야 한다
implementation 'org.springframework.boot:spring-boot-starter-validation'
Spring boot 2.3 version 이상부터는 spring-boot-starter-web 의존성이 분리되서
@validation 사용을 하기 위해 추가 해줘야 한다.
  • DTO 클래스를 사용하는 가장 중요한 목적은
    - 비용이 많이 드는 작업인 HTTP 요청의 수를 줄이고
    - 도메인 객체와 분리하는 것이다

 

3. HTTP 요청/응답 데이터에 DTO 적용하기

  • MemberController의 핸들러 메서드에 DTO 클래스를 적용해 본다
  • 지금까지는 HTTP Request Body가 JSON 형식이 아닌 ‘x-www-form-urlencoded’ 형식의 데이터였다
  • 프런트엔드와 함께 프로젝트를 진행 또는 하나의 제품이나 서비스를 제작할 경우
    - 프런트엔드의 웹앱과 백엔드의 애플리케이션 간 기본 API 통신 프로토콜은 대부분 JSON 형식으로 한다
  • 클라이언트에서 전달되는 요청 데이터 중에서 바디에 해당되는 데이터를 Request Body라고 한다
  • @RequestBody 애너테이션의 추가에 따라 Request Body를 전달 받는 방식이 JSON 형식으로 변경된다

 1) DTO 클래스 적용을 위한 코드 리팩토링 절차

  • 회원 정보를 전달 받을 DTO 클래스를 생성한다
    - MemberCotroller에서 현재 회원 정보로 전달 받는 각 데이터 항목(email, name, phone)들을 DTO 클래스의 멤버 변수로 추가한다
  • 클라이언트 쪽에서 전달하는 요청 데이터를 @RequestParam 애너테이션으로 전달 받는 핸들러 메서드를 찾는다
    - Request Body가 필요한 핸들러는 HTTP POST, PATCH, PUT 같이 리소스의 추가나 변경이 발생하는 경우이다
    - HTTP GET은 리소스를 조회하는 용도이기 때문에 Request Body는 필요가 없다
    - @PostMapping, @PatchMapping 애너테이션이 붙은 핸들러 메서드를 찾는것과 동일하다
  • @RequestParam의 코드를 DTO 클래스의 객체로 수정한다
  • Map 객체로 작성되어 있는 Response Body를 DTO 클래스의 객체로 변경한다
import javax.validation.constraints.Email;
public class MemberDto {
    @Email
    private String email;
    private String name;
    private String phone;
    private long memberId;

    public String getEmail() {
        return email;
    }
    public String getName() {
        return name;
    }
    public String getPhone() {
        return phone;
    }
    //--------------------------------- 회원정보 Dto class 작성
    //--------------------------------- RequestBody를 전달받기 위한 Dto class 작성
    public void setEmail(String email) {
        this.email = email;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }
    public long getMemberId(){
        return memberId;
    }
    public void getMemberId(long memberId) {
        this.memberId = memberId;
    }
}
  • 위의 코드는 회원 정보 등록과 수정 시 Request Body를 전달 받기 위한 Dto 클래스이다
  • DTO 클래스를 만들 때에는 멤버 변수 이외에 각 멤버 변수에 해당하는 getter 메서드가 있어야 한다
    - getter 메서드가 없으면 Response Body에 해당 멤버 변수의 값이 포함되지 않는 문제가 발생한다
    - setter 메서드는 개발자의 필요에 의해서 생성할 수 있다

 

 

Generate constructors and accessor methods - IntelliJ IDEA Guide

You can use ⌘N (macOS), or Alt+Insert (Windows/Linux) for the Generate menu and then select Constructor, Getter, Setter or Getter and Setter.

www.jetbrains.com

lombok 라이브러리를 등록하면 getter/setter 메서드가 내부에서 자동으로 만들어진다

 

  • @RequestParam을 사용했던 코드를 DTO 클래스를 사용하여 수정하였다
    - postMember()에서는 MemberPostDto 클래스의 객체를
    - patchMember()에서는 MemberPatchDto 클래스의 객체를 통해 Request Body를 한번에 전달 받을 수 있도록 하였다
  • @RequestParam을 사용했던 코드가 간결해진 것을 확인할 수 있다
@RestController
@RequestMapping("v1/members"/*,produce=MediaType.APPLICATION_JSON_VALUE*/)

@RestController
@RequestMapping("v1/members"/*,produce=MediaType.APPLICATION_JSON_VALUE*/)

public class MemberController {
    @PostMapping
    public ResponseEntity postMember (@RequestBody MemberPostDto memberPostDto) {
        return new ResponseEntity<>(memberPostDto, HttpStatus.CREATED);
    }

    //추가 코드 : patchMember
    @PatchMapping("/{member-id}")
    public ResponseEntity patchMember (@PathVariable("member-id")long memberId, @RequestBody MemberPatchDto memberPatchDto) {
        memberPatchDto.setMemberId(memberId);
        memberPatchDto.setName("홍길동");
        // No need Business logic
        return new ResponseEntity<>(memberPatchDto, HttpStatus.OK);
    }

    @GetMapping("/{member-id}")
    public ResponseEntity getMember(@PathVariable("member-id")long memberId, MemberGetDto memberGetDto) {
        System.out.println("# memberId: " + memberId);  //터미널에 출력되는 코드이다
        // not implementation
        return new ResponseEntity<>(memberGetDto, HttpStatus.OK);
    }

    @GetMapping //별도의 URI를 지정해주지 않았기 때문에 클래스 레벨의 URI(“/v1/members”)에 매핑된다
    public ResponseEntity getMembers(MemberGetDto memberGetDto) {
        System.out.println("# get Members");  //터미널에 출력되는 코드이다
        // not implementation
        return new ResponseEntity<>(memberGetDto, HttpStatus.OK);
    }

    //추가 코드 : getDelete
    @DeleteMapping("/{member-id}")
    public ResponseEntity deleteMember(@PathVariable("member-id")long memberId) {
        // No need business logic
        return new ResponseEntity<>(HttpStatus.NO_CONTENT);
    }
}
  • postman을 통해 post를 보내보면 아래와 같이 데이터가 입력되는 것을 확인할 수 있다

 

4. Annotation

 1) @RequestBody

  • JSON 형식의 Request Body를 Dto 클래스의 객체로 변환을 시켜주는 역할을 한다
    - 클라이언트에서 전송하는 Request Body가 JSON 형식이어야 하기 때문이다
  • JSON 형식이 아닌 다른 형식의 데이터를 전송하면 Spring 내부에서 ‘Unsupported Media Type’ 과 같은 에러 메시지를 포함한 응답을 전달한다

 

 2) @ResponseBody

  • JSON 형식의 Response Body를 클라이언트에게 전달하기 위해 DTO 클래스의 객체를 Response Body로 변환한다
  • postMember(), patchMember()에는 @ResponseBody 를 사용하는 곳이 없다
    - postMember(), patchMember() 핸들러 메서드의 리턴 값이 ResponseEntity 클래스의 객체이기 때문이다
  • Spring MVC에서는 핸들러 메서드에 @ResponseBody 애너테이션이 붙거나 핸들러 메서드의 리턴 값이 ResponseEntity일 경우, 내부적으로 HttpMessageConverter가 동작하게 되어 응답 객체(여기서는 DTO 클래스의 객체)를 JSON 형식으로 바꿔준다

 

 3) JSON 직렬화(Serialization)와 역직렬화(Deserialization)

  • 클라이언트에서 JSON 형식의 데이터를 서버 쪽으로 전송하면 서버의 웹 애플리케이션은 전달 받은 JSON 형식의 데이터를 DTO 같은 Java의 객체로 변환하는 기능을 역직렬화(Deserialization)이라고 한다
  • 서버에서 클라이언트에게 응답 데이터를 전송하기 위해 DTO 같은 Java의 객체를 JSON 형식으로 변환하는 것을 직렬화(Serialization)라고 한다
  • JSON 직렬화(Serialization) : Java 객체 → JSON
  • JSON 역직렬화(Deserialization) : JSON → Java 객체

 

 

1. 서비스 계층 연동

  • 예제 코드를 활용하여 서비스 계층의 연동을 진행한다
  • API계층과 서비스계층의 연동은 API계층의 클래스가 서비스계층의 클래스와 메서드를 호출하여 상호 작용한다는 의미이다

 1) 예제 코드

 2) 학습 방향

  • Spring의 DI(Dependency Injection)를 이용해서 API 계층과 비즈니스 계층을 연동한다
  • API 계층에서 전달받은 DTO 객체를 비즈니스 계층의 도메인 엔티티(Entity) 객체로 변환해서 전달하는 방법을 알아본다
  • Controller에 많은 개선이 일어나기 때문에 Controller의 요청 URI가 “/v1/xxxx”에서 “/v2/xxxx”와 같이 지속적으로 API 버전이 바뀌는 점에 유의한다
  • 서비스 계층에서 샘플 응답 데이터를 클라이언트에 전송하는 로직을 확인할 수 있다

2. 서비스(Service)

  • 애플리케이션에서 Service는 도메인 업무 영역을 구현하는 비지니스 로직과 연관이 있다
  • 비지니스 로직을 처리하기 위한 서비스 계층은 도메인 모델을 포함하고 있다
  • 도메인 모델은 DDD(Domain Driven Design / 도메인 주도 설계)와 관련성이 높다
    - 빈약한 도메인 모델(anemic domain model) 과 풍부한 도메인 모델(roch domain model)로 구분된다

3. Service 클래스 작성

 1) Controller 클래스와 연동하는 Service 클래스의 틀을 작성한다

package com.codestates.member;

import java.lang.reflect.Member;
import java.util.List;

public class MemberService {

    //MemberController 클래스의 postMember
    public Member saveMember(Member member) {
        return null;
    }

    //MemberController 클래스의 patchMember
    public Member updateMember(Member member) {
        return null;
    }

    //MemberController 클래스의 getMember
    public Member findMember(Member member) {
        return null;
    }

    //MemberController 클래스의 getMembers
    public List<Member> findMember() {
        return null;
    }

    //MemberController 클래스의 deleteMember
    public void deleteMember(long memberId) {

    }
}
  • MemberController 클래스에는 핸들러 메서드가 5개 있다
    - postMember() , patchMember() , getMember() , getMembers() , deleteMember()
package com.codestates.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;


@RestController
@RequestMapping("/v1/members")
@Validated
public class MemberController {

	//postMember() : 1명의 회원 등록을 위한 요청을 전달 받는다.
    @PostMapping    
    public ResponseEntity postMember(@Valid @RequestBody MemberPostDto memberDto) {
        return new ResponseEntity<>(memberDto, HttpStatus.CREATED);
    }

	//patchMember() : 1명의 회원 수정을 위한 요청을 전달 받는다.
    @PatchMapping("/{member-id}")
    public ResponseEntity patchMember(@PathVariable("member-id") @Positive long memberId,
                                      @Valid @RequestBody MemberPatchDto memberPatchDto) {
        memberPatchDto.setMemberId(memberId);

        // No need Business logic

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

	//getMember() : 1명의 회원 정보 조회를 위한 요청을 전달 받는다.
    @GetMapping("/{member-id}")
    public ResponseEntity getMember(@PathVariable("member-id") @Positive long memberId) {
        System.out.println("# memberId: " + memberId);

        // not implementation
        return new ResponseEntity<>(HttpStatus.OK);
    }

	//getMembers() : N명의 회원 정보 조회를 위한 요청을 전달 받는다.
    @GetMapping
    public ResponseEntity getMembers() {
        System.out.println("# get Members");

        // not implementation

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

	//deleteMember() : 1명의 회원 정보 삭제를 위한 요청을 전달 받는다.
    @DeleteMapping("/{member-id}")
    public ResponseEntity deleteMember(@PathVariable("member-id") @Positive long memberId) {
        System.out.println("# deleted memberId: " + memberId);
        // No need business logic

        return new ResponseEntity<>(HttpStatus.NO_CONTENT);
    }
  • MemberController의 핸들러 메서드에서 클라이언트의 요청 데이터(Request Body)를 전달 받을 때 MemberPostDtoMemberPatchDto 같은 DTO 클래스를 사용한다
    - DTO가 API 계층에서 클라이언트의 Request Body를 전달 받고 클라이언트에게 되돌려 줄 응답 데이터를 담는 역할을 한다
  • MemberService에서는 saveMember() 메서드와 updateMember() 메서드의 파라미터와 리턴값에 Member라는 타입을 사용했다
    - API 계층에서 전달 받은 요청 데이터를 기반으로 서비스 계층에서 비즈니스 로직을 처리하기 위해 필요한 데이터를 전달 받고, 비즈니스 로직을 처리한 후에는 결과 값을 다시 API 계층으로 리턴해주는 역할을 한다

 2) Member 클래스 구현

  • 아노테이션을 적용하면 자동으로 import 된다
package com.codestates.Member;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor

public class Member {
    private long memberId;
    private String email;
    private String name;
    private String phone;
}

 

 3) MemberService 클래스 세부 구현

package com.codestates.Member;

import java.lang.reflect.Member;
import java.util.List;

public class MemberService {

    //MemberController 클래스의 postMember
    public Member createMember(Member member) {
        // TODO should business logic

        // TODO member 객체는 나중에 DB에 저장 후, 되돌려 받는 것으로 변경 필요.
        Member createdMember = member;
        // DB 데이터가 없으므로 Member 객체를 그대로 리턴
        return createdMember;
    }

    //MemberController 클래스의 patchMember
    public Member updateMember(Member member) {
        // TODO should business logic

        // TODO member 객체는 나중에 DB에 저장 후, 되돌려 받는 것으로 변경 필요.
        Member updatedMember = member;
        // DB 데이터가 없으므로 Member 객체를 그대로 리턴
        return updatedMember;
    }

    //MemberController 클래스의 getMember
    public Member findMember(Member member) {
        // TODO should business logic

        // TODO member 객체는 나중에 DB에 저장 후, 되돌려 받는 것으로 변경 필요.
        // 현재는 stub 데이터로 사용
        Member member =
                new Member(memberId, "hgd@gmail.com", "홍길동", "010-1234-5678");
        return member;
    }

    //MemberController 클래스의 getMembers
    public List<Member> findMember() {
        // TODO should business logic

        // TODO member 객체는 나중에 DB에 저장 후, 되돌려 받는 것으로 변경 필요.
        List<Member> members = List.of(
                new Member(1, "hgd@gmail.com", "홍길동", "010-1234-5678"),
                new Member(2, "lml@gmail.com", "이몽룡", "010-1111-2222");
        );
        return members;
    }

    //MemberController 클래스의 deleteMember
    public void deleteMember(long memberId) {
        // TODO should business logic
    }
}

 

 4) DI(Dependency Injection)없이 비즈니스 계층과 API 계층 연동

  • 어떤 클래스가 다른 클래스의 기능을 사용하고자 한다면 객체를 새로 생성하여 다른 클래스의 메서드를 호출하면 된다
  • 객체의 생성은 new 키워드를 사용한다
  • MemberController에서 MemberService의 기능을 사용하도록 MemberController를 수정해 본다
package com.codestates.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;

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

    private class MemberService memberService;
    //에러로 인하여 생긴 class - 질문 요함...???

    public MemberController() {
        this.memberService = new MemberService();
        //MembeService 클래스를 사용하기 위하여 객체를 생성한다
    }

    //postMember() : 1명의 회원 등록을 위한 요청을 전달 받는다.
    @PostMapping
    public ResponseEntity postMember(
            @Valid @RequestBody MemberPostDto memberDto) {

        //클라이언트에서 전달 받은 DTO 클래스의 정보를
        // MemberService의 updateMember() 메서드의 파라미터로 전달하기 위해
        // MemberPostDto 클래스의 정보를 Member 클래스에 채워준다

        Member member = new Member();
        member.setEmail(memberDto.getEmail());
        member.setName(memberDto.getName());
        member.setPhone(memberDto.getPhone());

        //회원 정보 등록을 위해 MemberService 클래스의 createMember() 메서드를 호출한다
        Member response = memberService.createMember(member);

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

    //patchMember() : 1명의 회원 수정을 위한 요청을 전달 받는다.
    @PatchMapping("/{member-id}")
    public ResponseEntity patchMember(
            @PathVariable("member-id") @Positive long memberId,
            @Valid @RequestBody MemberPatchDto memberPatchDto) {
        memberPatchDto.setMemberId(memberId);

        //클라이언트에서 전달 받은 DTO 클래스의 정보를
        //MemberService의 createMember() 메서드의 파라미터로 전달하기 위해
        //MemberPatchDto 클래스의 정보를 Member 클래스에 채워준다
        Member member = new Member();
        member.setMemberId(memberPatchDto.getMemberId());
        member.setName(memberPatchDto.getName());
        member.setPhone(memberDto.getPhone());

        //회원 정보 수정을 위해 MemberService 클래스의 updateMember() 메서드를 호출한다
        Member response = memberService.updateMember(member);

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

    //getMember() : 1명의 회원 정보 조회를 위한 요청을 전달 받는다.
    @GetMapping("/{member-id}")
    public ResponseEntity getMember(
            @PathVariable("member-id") @Positive long memberId) {

        //한 명의 회원 정보 조회를 위해 MemberService 클래스의 findMember() 메서드를 호출한다
        //특정 회원의 정보를 조회하는 기준인 memberId를 파라미터로 넘겨준다
        Member response = memberService.fineMember(memberId);

        System.out.println("# memberId: " + memberId);

        // not implementation
        return new ResponseEntity<>(HttpStatus.OK);
    }

    //getMembers() : N명의 회원 정보 조회를 위한 요청을 전달 받는다.
    @GetMapping
    public ResponseEntity getMembers() {

        //모든 회원의 정보를 조회하기 위해 MemberService 클래스의 findMembers() 메서드를 호출한다
        List<Member> response = memberService.fineMembers();

        System.out.println("# get Members");

        // not implementation

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

    //deleteMember() : 1명의 회원 정보 삭제를 위한 요청을 전달 받는다.
    @DeleteMapping("/{member-id}")
    public ResponseEntity deleteMember(
            @PathVariable("member-id") @Positive long memberId) {
        System.out.println("# deleted memberId: " + memberId);

        //한 명의 회원 정보를 삭제하기 위해 MemberService 클래스의 deleteMember() 메서드를 호출한다
        //특정 회원의 정보를 삭제하는 기준인 memberId를 파라미터로 넘겨준다
        memberService.deleteMember(memberId);
        // No need business logic

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

 

 5) DI를 적용한 비즈니스 계층과 API 계층 연동

  • MemberController v2 코드는 Spring에서 지원하는 DI 기능을 사용하지 않았기 때문에 MemberController와 MemberService가 강하게 결합(Tight Coupling)되어 있는 상태이다
  • Spring의 DI를 사용하면 클래스 간의 결합을 느슨한 결합(Loose Coupling)으로 만들 수 있다
  • Spring의 DI를 사용하도록 MemberController를 수정해 본다
 //DI를 적용하기 위한 코드 수정, 비어있던 (파라미터)에 객체를 추가한다
    public MemberController(MemberService memberService) {
        // Spring이 애플리케이션 로드시, ApplicationContext에 있는 MemberService 객체를 주입하여 준다
        this.memberService = memberService();
        //DI를 적용하여 MembeService 클래스를 사용하기 위하여 객체를 지정한다
    }
  • Spring에서 DI를 통해서 어떤 객체를 주입 받기 위해서는 주입을 받는 클래스와 주입 대상 클래스 모두 Spring Bean이어야 한다
  • MemberService 클래스에 @Service 애너테이션을 추가하여 MemberService 클래스를 Spring Bean으로 변경한다
@Service

public class MemberService {
  • 생성자가 하나 이상일 경우에는 DI를 적용하기 위해 생성자에 반드시 @Autowired 애너테이션을 붙여야 한다

 

 6) 현재까지 작성한 코드의 문제점

  1. Controller 핸들러 메서드의 책임과 역할에 관한 문제
    - 핸들러 메서드의 역할은 클라이언트로부터 전달 받은 요청 데이터를 Service 클래스로 전달하고, 응답 데이터를 클라이언트로 다시 전송해주는 단순한 역할만을 하는 것이 좋다
    - 현재의 MemberController에서는 핸들러 메서드가 DTO 클래스를 엔티티(Entity) 객체로 변환하는 작업까지 하고 있다
  2. Service 계층에서 사용되는 엔티티(Entity) 객체를 클라이언트의 응답으로 전송하고 있다
    - DTO 클래스는 API 계층에서만 데이터를 처리하는 역할을 하고 엔티티(Entity) 클래스는 서비스 계층에서만 데이터를 처리하는 역할을 해야 한다
    - 엔티티(Entity) 클래스의 객체를 클라이언트의 응답으로 전송함으로써 계층 간의 역할 분리가 되어있지 않다

 7) 적용한 아노테이션

  • @Valid
    - 필드에 사용된 모든 아노테이션에 대한 유효성을 검증할 때 사용한다
  • @REquestBody
    - @REquestBody를 사용하면 파라메터에 HTTP요청 본문이 그대로 전달된다
    - 일반적으로 GET/POST 요청 파라미터는 @REquestBody를 사용하지 않는다
    -  xml 또는 Json 파라미터를 요청할 경우에 @REquestBody를 사용한다
  • @ResponseBody
    - @ResponseBody를 사용하면 HTTP요청의 Body 내용으로 변환하여 전송한다
  • @Getter, @Setter
    - lombok 라이브러리에서 제공하는 애너테이션이다
    - DTO 클래스를 작성하면서 각 멤버 변수에 해당하는 getter/setter 메서드를 일일이 작성하지 않아도 된다
  • @AllArgsConstructor
    - 현재 Member 클래스에 추가된 모든 멤버 변수를 파라미터로 갖는 Member 생성자를 자동으로 생성해 준다
  • @NoArgsConstructor
    - 파라미터가 없는 기본 생성자를 자동으로 생성해 준다

@Getter, @Setter, @AllArgsConstructor, @NoArgsConstructor, @Data, @ToString

 

 

 

 

※ 참조 링크

▶ DDD : https://ko.wikipedia.org/wiki/%EB%8F%84%EB%A9%94%EC%9D%B8_%EC%A3%BC%EB%8F%84_%EC%84%A4%EA%B3%84

 

도메인 주도 설계 - 위키백과, 우리 모두의 백과사전

 

ko.wikipedia.org

https://ko.wikipedia.org/wiki/%EB%B9%88%EC%95%BD%ED%95%9C_%EB%8F%84%EB%A9%94%EC%9D%B8_%EB%AA%A8%EB%8D%B8

 

빈약한 도메인 모델 - 위키백과, 우리 모두의 백과사전

 

ko.wikipedia.org

https://martinfowler.com/eaaCatalog/domainModel.html

 

P of EAA: Domain Model

 

martinfowler.com

https://martinfowler.com/tags/domain%20driven%20design.html

 

domain driven design

When programming, I often find it's useful to represent things as a compound. A 2D coordinate consists of an x value and y value. An amount of money consists of a number and a currency. A date range consists of start and end dates, which themselves can be

martinfowler.com

Lombok : https://projectlombok.org/features/all

 

Stable

 

projectlombok.org

 

+ Recent posts