1. DTO 유효성 검증(Validation)

 

 1) DTO 유효성 검증(Validation) 필요성

  • 애플리케이션과 REST API 통신을 하는 프런트엔드(Frontend) 쪽 웹 앱이 아래 그림과 같다
  • 일반적으로 프런트엔드의 웹 사이트에서는 자바스크립트를 통해서 사용자가 입력하는 입력 폼 필드의 값에 대한 유효성 검증을 진행한다
  • 프런트엔드에서 검증에 통과되었다고 그 값이 반드시 유효한 값이라고 보장할 수는 없다
    - 유효성 검사를 통과한 뒤 [등록] 버튼을 누르면 서버 쪽으로 HTTP Request 요청이 전송된다
    - 자바스크립트로 전송되는 데이터는 브라우저의 개발자 도구를 사용하면 브레이크 포인트(break point)를 이용해서 값을 조작할 수 있다
    - 프런트엔드에서 유효성 검사를 진행해도 서버에서 한번 더 유효성 검사를 진행해야 한다
    - 프런트엔드에서 진행하는 유효성 검사는 사용자 편의성을 위해 진행하는 절차이다
 

2. DTO 클래스에 유효성 검증 적용

  • MemberController에서 사용된 MemberPostDto 클래스와 MemberPatchDto 클래스에 유효성 검증을 적용해 본다

 1) 유효성 검증을 위한 의존 라이브러리 추가

DTO 클래스에 유효성 검증을 적용하기 위해서는 Spring Boot에서 지원하는 Starter가 필요하다

build.gradle 파일의 dependencies 항목에 'org.springframework.boot:spring-boot-starter-validation’을 추가한다

//Spring boot 2.3 version 이상부터는 spring-boot-starter-web 의존성이 분리되서 @validation 사용을 하기 위해 추가 해줘야 한다.
implementation 'org.springframework.boot:spring-boot-starter-validation'

 

 2) MemberPostDto 유효성 검증 제약 사항

  • email (이메일 주소)
    - 값이 비어있지 않거나 공백이 아니어야 한다
    - 유효한 이메일 주소 형식이어야 한다
  • name (이름)
    - 값이 비어있지 않거나 공백이 아니어야 한다
  • phone (휴대폰 번호)
    - 값이 비어있지 않거나 공백이 아니어야 한다
    - 010으로 시작하는 11자리 숫자와 ‘-’로 구성된 문자열이어야 한다
      : 010-1234-5678
  • 아래는 유효성 검증을 위해 제약사항을 적용한 코드이다
    - 회원 등록을 위해 클라이언트에서 전달 받는 Request Body의 데이터인 emil, name, phone 정보에 유효성 검증을 위한 애너테이션을 추가하였다
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;

public class MemberPostDto {
    @NotBlank(message = "공백없이 작성해 주시기 바랍니다")
    @Email
    private String email;
    @NotBlank(message = "공백없이 작성해 주시기 바랍니다")
    private String name;

    @NotBlank(message = "공백없이 작성해 주시기 바랍니다")
    @Pattern(regexp = "^010-\\d{3,4}-\\d{4}$", message = "010으로 시작하는 11자리 숫자를 '-'을 넣어서 작성해 주시기 바랍니다")
    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 setMemberId(long memberId) {
        this.memberId = memberId;
    }
}

 

 2-1) MemberPostDto의 멤버 변수에 적용된 유효성 검증 내용

  • email
    - @NotBlank
      : 이메일 정보가 비어있지 않은지를 검증한다
      : null 값이나 공백(””), 스페이스(” “) 같은 값들을 허용하지 않는다
      : 유효성 검증에 실패하면 에러 메시지를 콘솔에 출력한다

    - @Email
      : 유효한 이메일 주소인지 검증한다
      : 유효성 검증에 실패하면 내장된 디폴트 에러 메시지를 콘솔에 출력한다
  • name
    - @NotBlank
      : 이름 정보가 비어있지 않은지를 검증한다
      : null 값이나 공백(””), 스페이스(” “) 같은 값들을 모두 허용하지 않는다
      : 유효성 검증에 실패하면 message 애트리뷰트에 지정한 문자열 에러 메시지를 콘솔에 출력한다
  • phone
    - @Pattern
      : 휴대폰 정보가 정규표현식(Reqular Expression)에 매치되는 유효한 번호인지를 검증한다
      : 유효성 검증에 실패하면 내장된 디폴트 에러 메시지를 콘솔에 출력한다
  • 요청으로 전달 받는 MemberPostDto 클래스의 각 멤버 변수에 유효성 검증을 위한 애너테이션을 추가하여 MemberController의 핸들러 메서드에 별도의 유효성 검증을 추가하지 않고 유효성 검증 로직을 분리하였다

 

 2-2) Controller에 유효성 검증 적용

  • 유효성 검증 애너테이션이 추가된 DTO 클래스에서 유효성 검증 로직이 실행되게 하기 위해서 postMember()와 같이 DTO 클래스에 @Valid 애너테이션을 추가한다
@PostMapping
//Dto에 유효성 검증 조건을 적용하였으므로 @Valid 를 적용하여 준다
public ResponseEntity postMember (@Valid @RequestBody MemberPostDto memberPostDto) {
    return new ResponseEntity<>(memberPostDto, HttpStatus.CREATED);
}

postman에서 유효하지 않은 정보를 전송하면 Error이 발생한다

  • Spring MVC에서 출력한 로그를 IntelliJ IDE에서 확인하면 아래와 같이 출력된다
    - email의 오류를 출력한다
    - name는 공백이 없으므로 정상적으로 전송된다
    - phone은 '010'이 아니므로 오류 메시지를 출력한다

 

3) MemberPatchDto 유효성 검증 제약 사항

  • name (이름)
    - 값이 비어있지 않거나 공백이 아니어야 한다
  • phone (휴대폰 번호)
    - 값이 비어있지 않거나 공백이 아니어야 한다
    - 010으로 시작하는 11자리 숫자와 ‘-’로 구성된 문자열이어야 한다
      : 010-1234-5678
  • 아래는 유효성 검증을 위해 제약사항을 적용한 코드이다
    - 회원 정보 수정을 위해 클라이언트에서 전달 받는 Request Body의 데이터인 name, phone 정보에 유효성 검증을 위한 애너테이션을 추가하였다
import javax.validation.constraints.Email;
import javax.validation.constraints.Pattern;

public class MemberPatchDto {
    @Email
    private String email;

    @Pattern(regexp = "^(?=\\s*\\S).*$", message = "공백이 아니어야 합니다")
    private String name;

    @Pattern(regexp = "^(?=\\s*\\S).*$", message = "공백이 아니어야 합니다")
    @Pattern(regexp = "^010-\\d{3,4}-\\d{4}$", message = "010으로 시작하는 11자리 숫자를 '-'을 넣어서 작성해 주시기 바랍니다")
    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 setMemberId(long memberId) {
        this.memberId = memberId;
    }
}

 3-1) MemberPatchDto 클래스의 멤버 변수에 적용된 유효성 검증 내용

  • memberId
    - Request Body에 포함되는 데이터가 아니므로 유효성 검증이 필요하지 않다
  • name
    - @Pattern
      : 정규 표현식으로 다음 내용을 체크한다
        -> 이름 정보가 비어있으면 검증에 성공
        -> 이름 정보가 비어 있지 않고, 공백 문자열이라면 검증에 실패
  • phone
    - @Pattern
      : 정규 표현식으로 다음 내용을 체크한다
        -> 휴대폰 정보가 비어있으면 검증에 성공
        -> 휴대폰 정보가 비어 있지 않고, 010으로 시작하는 11자리 숫자와 ‘-’로 구성된 문자열이 아니라면 검증에 실패
  • MemberPostDto 클래스와 달리 MemberPatchDto 에서는 모두 정규 표현식을 사용했다
  • 웹 브라우저에서 회원 가입 후에 이름(또는 닉네임)만 수정하거나 휴대폰 번호만 수정할 수도 있고 모두 수정할 수도 있다
  • 이름이나 휴대폰 번호의 값으로 공백 문자열(”” 또는 “ “)만 포함이 되어 있을 경우를 검증해야할 수 있다

 3-2) Controller 클래스에 patchMember() 핸들러 메서드의 코드 적용

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

 

3. 쿼리 파라미터(Query Parameter 또는 Query String) 및 @Pathvariable에 대한 유효성 검증

  • Request Body의 유효성 검사 검증 대상에서 빠진 항목이 있다
  • patchMember() 핸들러 메서드의 URI path에서 사용되는 @PathVariable("member-id") long memberId 변수이다
  • 일반적으로 수정이 필요한 데이터의 식별자는 0이상의 숫자로 표현을 한다
  • patchMember() 핸들러 메서드에서 사용되는 memberId에 "1 이상의 숫자여야 한다"라는 조건을 부여하도록 한다
    - 1 이상의 숫자일 경우에만 유효성 검증에 통과하도록 @Min(1) 이라는 검증 애너테이션을 추가한다
    @PatchMapping("/{member-id}")
    public ResponseEntity patchMember (@PathVariable("member-id") @Min(1) long memberId,
                                       @Valid @RequestBody MemberPatchDto memberPatchDto) {
        memberPatchDto.setMemberId(memberId);
//        memberPatchDto.setName("홍길동");
        // No need Business logic
        return new ResponseEntity<>(memberPatchDto, HttpStatus.OK);
    }
  • 위와 같이 수정하여도 Postman으로 URI path에 유효하지 않은 값을 전송하면 유효성 검증이 정상적으로 수행되지 않는다
  • 유효성 검증이 정상적으로 수행되려면 클래스 레벨에 @Validated 애너테이션을 붙여주어야 한다
@RestController
@RequestMapping("v1/members"/*,produce=MediaType.APPLICATION_JSON_VALUE*/)
@Validated
  • postman으로 실행해 보면 응답 결과는 Response Satus가 500인 ‘Internal Server Error’를 전달 받는다
  • @Request Body에 대한 유효성 검증 실패 메시지와 조금 다르지만 유효성 검증은 정상적으로 이루어진 것이다

 

4. Jakarta Bean Validation

  • DTO 클래스의 유효성 검증을 위해서 사용한 애너테이션은 Jakarta Bean Validation라는 유효성 검증을 위한 표준 스펙에서 지원하는 내장 애너테이션이다
  • Jakarta Bean Validation은 라이브러리처럼 사용할 수 있는 API가 아닌 스펙(사양, Specification) 자체이다
    - Jakarta Bean Validation은 일종의 기능 명세와 같다
    - Jakarta Bean Validation 스펙을 구현한 구현체가 Hibernate Validator이다
  • Jakarta Bean Validation의 애너테이션을 DTO 클래스에만 사용할 수 있는 것은 아니다
    - Java Bean 스펙을 준수하는 Java 클래스라면 Jakarta Bean Validation의 애너테이션으로 유효성 검증을 할 수 있다

 

5. Custom Validator를 사용한 유효성 검증

  • DTO 클래스에 유효성 검증 시 Jakarta Bean Validation에 내장된 애너테이션 중에 목적에 맞는 애너테이션이 없을 수 있다
    - 목적에 맞는 애너테이션을 직접 만들어서 유효성 검증에 적용할 수 있다
  • MemberPatchDto 클래스의 name과 phone 멤버 변수에서 공백 여부를 검증하는 @Pattern(regexp = "^(?=\\s*\\S).*$") 애너테이션을 Custom Validator를 사용하여 바꿔 본다
  • 정규 표현식(Regular Expression)은 성능적인 면에서 때로는 비싼 비용을 치뤄야 될 가능성이 있다
    - 모든 로직을 정규표현식 위주로 작성하는 것은 좋은 개발 방식이 아니다

 

 1) Custom Validator를 구현하기 위한 절차

  • Custom Validator를 사용하기 위한 Custom Annotation을 정의한다
    - 공백을 허용하지 않는 Custom Annotation 'Notspace'를 생성한다
    - 중요한 부분은 NotSpace 애너테이션을 멤버 변수에 추가할 경우 동작하는 Custom Validator를 추가하는 것이다
     : @Constraint(validatedBy = {NotSpaceValidator.class})
    - 유효성 검증 실패 시 표시되는 디폴트 메시지를 애너테이션에서 정의해 준다
     : String message() default "공백이 아니어야 합니다"
     : 애너테이션에서 정의한 디폴트 메시지는 적용 시 공통적으로 사용된다
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;


@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {NotSpaceValidator.class}) // (1)
public @interface Notspace {
    String message() default "공백이 아니어야 합니다"; // (2)
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}
  • 정의한 Custom Annotation에 바인딩 되는 Custom Validator를 구현한다
    - CustomValidator를 구현하기 위해서는 ConstraintValidator 인터페이스를 먼저 구현해야 한다
    - ConstraintValidator<NotSpace, String>에서
     : NotSpace는 CustomValidator와 매핑된 Custom Annotation을 의미한다
     : String은 Custom Annotation으로 검증할 대상 멤버 변수의 타입을 의미한다
import org.springframework.util.StringUtils;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class NotSpaceValidator implements ConstraintValidator<Notspace, String> {

    @Override
    public void initialize(Notspace constraintAnnotation) {
        ConstraintValidator.super.initialize(constraintAnnotation);
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        return value == null || StringUtils.hasText(value);
    }
}
  • 유효성 검증이 필요한 DTO 클래스의 멤버 변수에 Custom Annotation을 추가한다
    - name과 phone 등의 멤버 변수에서 적용된 @Pattern(regexp = "^(?=\\s*\\S).*$") 애너테이션 대신 Custom Annotation인 @NotSpace를 추가한다
    - 애플리케이션을 재시작하고 patchMember() 핸들러 메서드에 해당되는 URI로 요청을 전송하여 확인한다
public class MemberPostDto {
//    @NotBlank(message = "공백이 아니어야 합니다")
    @Notspace(message = "공백이 아니어야 합니다")
    @Email
    private String email;
//    @NotBlank(message = "공백이 아니어야 합니다")
    @Notspace(message = "공백이 아니어야 합니다")
    private String name;

//    @NotBlank(message = "공백이 아니어야 합니다")
    @Notspace(message = "공백이 아니어야 합니다")
    @Pattern(regexp = "^010-\\d{3,4}-\\d{4}$", message = "010으로 시작하는 11자리 숫자를 '-'을 넣어서 작성해 주시기 바랍니다")
    private String phone;
public class MemberPatchDto {
    @Email
    private String email;

//    @Pattern(regexp = "^(?=\\s*\\S).*$", message = "공백이 아니어야 합니다")
    @Notspace(message = "공백이 아니어야 합니다")
    private String name;

//    @Pattern(regexp = "^(?=\\s*\\S).*$", message = "공백이 아니어야 합니다")
    @Notspace(message = "공백이 아니어야 합니다")
    @Pattern(regexp = "^010-\\d{3,4}-\\d{4}$", message = "010으로 시작하는 11자리 숫자를 '-'을 넣어서 작성해 주시기 바랍니다")
    private String phone;

 

 

■ 참조 링크

정규 표현식 관련 자료

 : https://www.w3schools.com/java/java_regex.asp 

 

Java Regular Expressions

W3Schools offers free online tutorials, references and exercises in all the major languages of the web. Covering popular subjects like HTML, CSS, JavaScript, Python, SQL, Java, and many, many more.

www.w3schools.com

 : https://developer.mozilla.org/ko/docs/Web/JavaScript/Guide/Regular_Expressions

 

정규 표현식 - JavaScript | MDN

정규 표현식, 또는 정규식은 문자열에서 특정 문자 조합을 찾기 위한 패턴입니다. JavaScript에서는 정규 표현식도 객체로서, RegExp의 exec()와 test() 메서드를 사용할 수 있습니다. String의 match(), matchA

developer.mozilla.org

정규 표현식 모범 사례 자료

 : https://learn.microsoft.com/ko-kr/dotnet/standard/base-types/best-practices 

 

.NET의 정규식에 대한 모범 사례

.NET에서 효율적이고 효과적인 정규식을 만드는 방법을 알아봅니다.

learn.microsoft.com

 Jakarta Bean Validation Specification

 : https://beanvalidation.org/2.0/spec/ 

 

Jakarta Bean Validation specification

BeanNode, PropertyNode and ContainerElementNode host getContainerClass() and getTypeArgumentIndex(). If the node represents an element that is contained in a container such as Optional, List or Map, the former returns the declared type of the container and

beanvalidation.org

Jakarta Bean Validation Built-in Constraint definitions

 : https://beanvalidation.org/2.0/spec/#builtinconstraints 

 

Jakarta Bean Validation specification

BeanNode, PropertyNode and ContainerElementNode host getContainerClass() and getTypeArgumentIndex(). If the node represents an element that is contained in a container such as Optional, List or Map, the former returns the declared type of the container and

beanvalidation.org

Hibernate Validator

 : https://docs.jboss.org/hibernate/validator/6.2/reference/en-US/html_single/#preface 

 

Hibernate Validator 6.2.5.Final - Jakarta Bean Validation Reference Implementation: Reference Guide

Validating data is a common task that occurs throughout all application layers, from the presentation to the persistence layer. Often the same validation logic is implemented in each layer which is time consuming and error-prone. To avoid duplication of th

docs.jboss.org

Java Bean 관련 자료

 : https://ko.wikipedia.org/wiki/%EC%9E%90%EB%B0%94%EB%B9%88%EC%A6%88 

 

자바빈즈 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 자바빈즈(JavaBeans)는 자바로 작성된 소프트웨어 컴포넌트이다. 자바빈즈의 사양은 썬 마이크로시스템즈에서 다음과 같이 정의되었다. "빌더 형식의 개발도구에

ko.wikipedia.org

 

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. 클라이언트(Client)와 서버(Server)의 관계

클라이언트(Client)와 서버(Server)의 관계

  • WebBrower는 Frontend 서버에 content를 요청하면 Forntend 서버는 content를 찾아서 제공한다
  • Frontend 서버는 WenBrower에서 요청한 content를 필요에 따라 Backend 서버에 요청하여 제공받는다
  • 최종 Client는 WebBrower이지만 Frontend 서버는 필요에 따라 Client가 되기도 하고 Server가 되기도 한다
  • Backend도 다중의 Server를 사용하게 되면 Request Server가 Client가 되기도 한다

2. Rest API

  • Rest API 서버에 HTTP 요청을 보낼 수 있는 클라이언트 툴 또는 라이브러리를 의미한다
  • postman과 같은 프로그램은 UI가 있는 Rest API이다
  • UI가 없는 Rest API일 경우에는 Rest Client Library를 사용하면 된다
    - Server와 Server 간의 요청이 대표적이다

▶ IBM REST API : https://www.ibm.com/docs/en/integration-bus/10.0?topic=apis-rest

 

3. RestTemplate

  • Java에서 사용할 수 있는 HTTP Client 라이브러리
    - java.net.HttpURLConnection
    - Apache HttpComponents
    - OkHttp 3
    - Netty
  • Spring에서는 HTTP Client 라이브러리 중 하나를 이용해서 원격지에 있는 다른 Backend 서버에 HTTP 요청을 보낼 수 있는 RestTemplate이라는 Rest Client API를 제공한다
  • RestTemplate이라는 템플릿 클래스를 이용하여 HTTP Client 라이브러리 중 하나를 유연하게 사용할 수 있다

 

4. RestTemplate 객체 생성

  • Spring Initializr를 사용하여 프로젝트를 생성한다
package main;

public class RestClientExample01{
    public static void main(String[] args) {

        /*기본적으로 RestTemplate의 객체를 생성하기 위해서는
        RestTemplate의 생성자 파라미터로 HTTP Client 라이브러리의 구현 객체를 전달해야 한다*/

        RestTemplate restTemplate = new RestTemplate(

                /*HttpComponentsClientHttpRequestFactory 클래스를 통해 Apache HttpComponents를 전달한다*/

                new HttpComponentsClientHttpRequestFactory());
    }
}
  • 위의 코드는 객체 에러로 인하여 실행할 수 없다
  • Apache HttpComponents를 사용하기 위해서는 builde.gradle의 dependencies 항목에 아래와 같이 의존 라이브러리를 추가해야 한다
dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'

	//추가한 코드
	implementation 'org.apache.httpcomponents:httpclient'
}
  • dependencies를 추가하면  객체를 import 할 수 있다
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;

public class RestClientExample01{
    public static void main(String[] args) {

        /*기본적으로 RestTemplate의 객체를 생성하기 위해서는
        RestTemplate의 생성자 파라미터로 HTTP Client 라이브러리의 구현 객체를 전달해야 한다*/

        RestTemplate restTemplate = new RestTemplate(

                /*HttpComponentsClientHttpRequestFactory 클래스를 통해 Apache HttpComponents를 전달한다*/

                new HttpComponentsClientHttpRequestFactory());
    }
}

 

5. URI 생성

  • RestTemplate 객체를 생성한 후 HTTP Request를 전송할 Rest 엔드포인트 URI를 지정한다
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;

import java.net.URI;

public class RestClientExample01{
    public static void main(String[] args) {

        /*기본적으로 RestTemplate의 객체를 생성하기 위해서는
        RestTemplate의 생성자 파라미터로 HTTP Client 라이브러리의 구현 객체를 전달해야 한다*/

        RestTemplate restTemplate = new RestTemplate(

                /*HttpComponentsClientHttpRequestFactory 클래스를 통해 Apache HttpComponents를 전달한다*/

                new HttpComponentsClientHttpRequestFactory());

        //URI 생성
        /*
        UriComponentsBuilder 클래스를 이용해서 UriComponents 객체를 생성하고
        UriComponents 객체를 이용해서 HTTP Request를 요청할 엔드포인트의 URI를 생성한다
        */
        UriComponents uriComponents = UriComponentsBuilder

                .newInstance() //UriComponentsBuilder 객체를 생성한다
                .scheme("http") //URI의 scheme을 설정한다
                .host("worldtimeapi.org") //호스트 정보를 입력한다
                .port(80)  //디폴트 값은 80이므로 80 포트를 사용하는 호스트라면 생략 가능하다
                .path("api/timezone/{continents}/{city}")
                /*
                URI의 경로(path)를 입력한다
                URI의 path에서 {continents}, {city} 두 개의 템플릿 변수를 사용하고 있다
                두 개의 템플릿 변수는 uriComponents.expand("Asia", "Seoul").toUri(); 에서
                expand() 메서드 파라미터의 문자열로 채워진다
                빌드 타임에 {continents}는 ‘Asia’, {city}는 ‘Seoul’로 변환됩니다.
                 */
                .encode()
                /*URI에 사용된 템플릿 변수들을 인코딩 한다
                 non-ASCII 문자와 URI에 적절하지 않은 문자를 Percent Encoding 한다는 의미이다
                 */
                .build();
                //UriComponents 객체를 생성한다

        URI uri = uriComponents.expand("Asia","Seoul").toUri();
        /* expand 메서드는 파라미터로 입력한 값을 URI 템플릿 변수의 값으로 대체한다
           toUri 메서드는 객체를 생성한다
         */
    }

 

6. Request 전송

  • 엔드포인트로 Request를 전송한다
  • getForObject(URI uri, Class<T> responseType)
    - getForObject() 메서드는 HTTP Get 요청을 통해 서버의 리소스를 조회한다
    - URI uri : Request를 전송할 엔드포인트의 URI 객체를 지정한다
    - Class responseType : 응답으로 전달 받을 클래스의 타입을 지정한다
package com.dreamfactory.restAPI;

import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;

import java.net.URI;

public class RestClientExample01{
    public static void main(String[] args) {

        /*기본적으로 RestTemplate의 객체를 생성하기 위해서는
        RestTemplate의 생성자 파라미터로 HTTP Client 라이브러리의 구현 객체를 전달해야 한다*/

        RestTemplate restTemplate = new RestTemplate(

                /*HttpComponentsClientHttpRequestFactory 클래스를 통해 Apache HttpComponents를 전달한다*/

                new HttpComponentsClientHttpRequestFactory());

        //URI 생성
        /*
        UriComponentsBuilder 클래스를 이용해서 UriComponents 객체를 생성하고
        UriComponents 객체를 이용해서 HTTP Request를 요청할 엔드포인트의 URI를 생성한다
        */
        UriComponents uriComponents = UriComponentsBuilder

                .newInstance() //UriComponentsBuilder 객체를 생성한다
                .scheme("http") //URI의 scheme을 설정한다
                .host("worldtimeapi.org") //호스트 정보를 입력한다
                .port(80)  //디폴트 값은 80이므로 80 포트를 사용하는 호스트라면 생략 가능하다
                .path("api/timezone/{continents}/{city}")
                /*
                URI의 경로(path)를 입력한다
                URI의 path에서 {continents}, {city} 두 개의 템플릿 변수를 사용하고 있다
                두 개의 템플릿 변수는 uriComponents.expand("Asia", "Seoul").toUri(); 에서
                expand() 메서드 파라미터의 문자열로 채워진다
                빌드 타임에 {continents}는 ‘Asia’, {city}는 ‘Seoul’로 변환됩니다.
                 */
                .encode()
                /*URI에 사용된 템플릿 변수들을 인코딩 한다
                 non-ASCII 문자와 URI에 적절하지 않은 문자를 Percent Encoding 한다는 의미이다
                 */
                .build();
                //UriComponents 객체를 생성한다

        URI uri = uriComponents.expand("Asia","Seoul").toUri();
        /* expand 메서드는 파라미터로 입력한 값을 URI 템플릿 변수의 값으로 대체한다
           toUri 메서드는 객체를 생성한다
         */

        //Request 전송

        //getForObject() 메서드로 HTTP Get 요청을 통해 서버의 리소스를 조회한다
        //응답 데이터를 문자열로 받을 수 있도록 String.class로 지정한다
       String result = restTemplate.getForObject(uri, String.class);
        System.out.println(result);

    }
}
  • 코드를 최종적으로 실행하면 아래와 같이 출력된다

 

 1) getForObject()를 이용하여 커스텀 클래스 타입으로 원하는 정보만 응답으로 전달 받기

  • 전달 받고자하는 응답 데이터의 JSON 프로퍼티 이름과 클래스의 멤버변수 이름이 동일해야 한다
  • 해당 멤버 변수에 접근하기 위한 getter 메서드 역시 동일한 이름이어야 한다
package com.dreamfactory.restAPI;

import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;

import java.net.URI;

public class RestClientExample02 {
    public static void main(String[] args) {
        RestTemplate restTemplate = new RestTemplate(
                new HttpComponentsClientHttpRequestFactory()
        );
        UriComponents uriComponents = UriComponentsBuilder
                .newInstance()
                .scheme("http")
                .host("worldtimeapi.org")
                        .port(80)
                        .path("/api/timezone/{continents}/{city}")
                        .encode()
                        .build();
        URI uri = uriComponents.expand("Asia", "Seoul").toUri();
        //WorldTime 클래스를 사용해서 전체 응답 데이터가 아니라 datetime과 timezone 정보만 전달 받는다
        WorldTime worldTime = restTemplate.getForObject(uri, WorldTime.class);

        System.out.println("# datatime: " + worldTime.getDatetime());
        System.out.println("# timezone: " + worldTime.getTimezone());
        System.out.println("# day_of_week: " + worldTime.getDay_of_week());
    }
}
  • WorldTime에 대한 클래스를 새로 생성해야 한다
package com.dreamfactory.restAPI;

public class WorldTime {
    private String datetime;
    private String timezone;
    private String day_of_week;

    public String getDatetime() {
        return datetime;
    }
    public String getTimezone() {
        return timezone;
    }
    public String getDay_of_week() {
        return day_of_week;
    }
}
  • 실행하면 지정된 데이터만 응답 전송되는 것을 확인할 수 있다

 

 

 2) getForEntity()를 사용한 Response Body(바디, 컨텐츠) + Header(헤더) 정보 전달 받기

  • 응답 데이터는 ResponseEntity 클래스로 래핑되어서 전달 된다
  • getBody(), getHeaders() 메서드 등을 이용해서 바디와 헤더 정보를 얻을 수 있다
  • 응답으로 전달 되는 모든 헤더 정보를 보고 싶다면 getHeaders().entrySet() 메서드를 이용해서 확인할 수 있다
package com.dreamfactory.restAPI;

import org.springframework.http.ResponseEntity;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;

import java.net.URI;

public class RestClientExample02 {
    public static void main(String[] args) {
        RestTemplate restTemplate = new RestTemplate(
                new HttpComponentsClientHttpRequestFactory()
        );
        UriComponents uriComponents = UriComponentsBuilder
                .newInstance()
                .scheme("http")
                .host("worldtimeapi.org")
                        .port(80)
                        .path("/api/timezone/{continents}/{city}")
                        .encode()
                        .build();
        URI uri = uriComponents.expand("Asia", "Seoul").toUri();
        //WorldTime 클래스를 사용해서 전체 응답 데이터가 아니라 datetime과 timezone 정보만 전달 받는다
//        WorldTime worldTime = restTemplate.getForObject(uri, WorldTime.class);

//        System.out.println("# datatime: " + worldTime.getDatetime());
//        System.out.println("# timezone: " + worldTime.getTimezone());
//        System.out.println("# day_of_week: " + worldTime.getDay_of_week());
        
        //getForEntity()를 사용한 Response Body(바디, 컨텐츠) + Header(헤더) 정보 전달 받기
        ResponseEntity<WorldTime> response = restTemplate.getForEntity(uri, WorldTime.class);
        System.out.println("# datatime: " + response.getBody().getDatetime());
        System.out.println("# timezone: " + response.getBody().getTimezone());
        System.out.println("# day_of_week: " + response.getBody().getDay_of_week());
        System.out.println("# HTTP Status Code: " + response.getStatusCode());
        System.out.println("# HTTP Status Value: " + response.getStatusCodeValue());
        System.out.println("# Content Type: " + response.getHeaders().getContentType());
        System.out.println(response.getHeaders().entrySet());

    }
}
  • 실행하면 아래와 같은 결과 값이 출력된다

 

 3) exchange() 를 사용한 응답 데이터 받기

  • exchange() 메서드를 사용한 방식은 일반적인 HTTP Request 방식입니다.
  • HTTP Method나 HTTP Request, HTTP Response 방식을 개발자가 직접 지정해서 유연하게 사용할 수 있다
  • exchange(URI uri, HttpMethod method, HttpEntity<?> requestEntity, Class<T> responseType)
    - HTTP Method, RequestEntity, ResponseEntity를 직접 지정해서 HTTP Request를 전송할 수 있는 가장 일반적인 방식이다
    URI uri : Request를 전송할 엔드포인트의 URI 객체를 지정한다
    - HttpMethod method : HTTP Method 타입을 지정한다
    - HttpEntity<?> requestEntity :
    HttpEntity 객체를 지정하며, HttpEntity 객체를 통해 헤더 및 바디, 파라미터 등을 설정해줄 수 있다
    -  Class<T> responseType : 응답으로 전달 받을 클래스의 타입을 지정한다
package com.dreamfactory.restAPI;

import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;

import java.net.URI;

public class RestClientExample02 {
    public static void main(String[] args) {
        RestTemplate restTemplate = new RestTemplate(
                new HttpComponentsClientHttpRequestFactory()
        );
        UriComponents uriComponents = UriComponentsBuilder
                .newInstance()
                .scheme("http")
                .host("worldtimeapi.org")
                        .port(80)
                        .path("/api/timezone/{continents}/{city}")
                        .encode()
                        .build();
        URI uri = uriComponents.expand("Asia", "Seoul").toUri();
        //WorldTime 클래스를 사용해서 전체 응답 데이터가 아니라 datetime과 timezone 정보만 전달 받는다
//        WorldTime worldTime = restTemplate.getForObject(uri, WorldTime.class);

//        System.out.println("# datatime: " + worldTime.getDatetime());
//        System.out.println("# timezone: " + worldTime.getTimezone());
//        System.out.println("# day_of_week: " + worldTime.getDay_of_week());

        //getForEntity()를 사용한 Response Body(바디, 컨텐츠) + Header(헤더) 정보 전달 받기
//        ResponseEntity<WorldTime> response = restTemplate.getForEntity(uri, WorldTime.class);
//        System.out.println("# datatime: " + response.getBody().getDatetime());
//        System.out.println("# timezone: " + response.getBody().getTimezone());
//        System.out.println("# day_of_week: " + response.getBody().getDay_of_week());
//        System.out.println("# HTTP Status Code: " + response.getStatusCode());
//        System.out.println("# HTTP Status Value: " + response.getStatusCodeValue());
//        System.out.println("# Content Type: " + response.getHeaders().getContentType());
//        System.out.println(response.getHeaders().entrySet());

        //exchange() 를 사용한 응답 데이터 받기
        ResponseEntity<WorldTime> response = restTemplate.exchange(
                uri, HttpMethod.GET, null, WorldTime.class);
        System.out.println("# datatime: " + response.getBody().getDatetime());
        System.out.println("# timezone: " + response.getBody().getTimezone());
        System.out.println("# day_of_week: " + response.getBody().getDay_of_week());
        System.out.println("# HTTP Status Code: " + response.getStatusCode());
        System.out.println("# HTTP Status Value: " + response.getStatusCodeValue());
    }
}
  • 코드를 실행하면 아래와 같이 출력된다

 

 

※ 참조 링크

URI scheme : https://en.wikipedia.org/wiki/List_of_URI_schemes

 

List of URI schemes - Wikipedia

From Wikipedia, the free encyclopedia Jump to navigation Jump to search Namespace identifier assigned by IANA This article lists common URI schemes. A Uniform Resource Identifier helps identify a source without ambiguity. Many URI schemes are registered wi

en.wikipedia.org

Percent Encoding : https://ko.wikipedia.org/wiki/%ED%8D%BC%EC%84%BC%ED%8A%B8_%EC%9D%B8%EC%BD%94%EB%94%A9

 

퍼센트 인코딩 - 위키백과, 우리 모두의 백과사전

 

ko.wikipedia.org

RestTemplate API : https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#rest-client-access

 

Integration

As a lightweight container, Spring is often considered an EJB replacement. We do believe that for many, if not most, applications and use cases, Spring, as a container, combined with its rich supporting functionality in the area of transactions, ORM and JD

docs.spring.io

RestTEmplate API Docs : https://docs.spring.io/spring-framework/docs/current/javadoc-api/

 

Spring Framework 5.3.22 API

 

docs.spring.io

Open API 서비스 제공 사이트

https://www.data.go.kr/dataset/3043385/openapi.do

 

공공데이터 포털

국가에서 보유하고 있는 다양한 데이터를『공공데이터의 제공 및 이용 활성화에 관한 법률(제11956호)』에 따라 개방하여 국민들이 보다 쉽고 용이하게 공유•활용할 수 있도록 공공데이터(Datase

www.data.go.kr

https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api

 

Kakao Developers

카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.

developers.kakao.com

https://developers.naver.com/products/intro/plan/plan.md

 

네이버 오픈 API 목록 - INTRO

네이버 오픈 API 목록 NAVER Developers - API 소개 네이버 오픈API 목록 및 안내입니다. 네이버 오픈 API 목록 API명 설명 호출제한 검색 네이버 블로그, 이미지, 웹, 뉴스, 백과사전, 책, 카페, 지식iN 등 검

developers.naver.com

https://console.cloud.google.com/getting-started?pli=1 

 

Google Cloud console

계정이 일시적으로 차단되었습니다. Google이 네트워크에서 악성 소프트웨어, 브라우저 플러그인 또는 자동화된 요청을 전송하는 스크립트에 의해 전송되었을 수 있는 요청을 감지한 경우 계정

console.cloud.google.com

https://aiopen.etri.re.kr/

 

공공 인공지능 오픈 API·DATA 서비스 포털

과학기술정보통신부의 R&D 과제를 통해 개발한 다양한 인공지능 기술 및 데이터를 누구나 사용할 수 있도록 제공

aiopen.etri.re.kr

 

1. HTTP헤더(Header)

  • General headers
      - 메시지 전체에 적용되는 헤더이며, body를 통해 전송되는 데이터와는 관련이 없는 헤더이다
  • Response headers
      - 위치 또는 서버 자체에 대한 정보(이름, 버전 등)와 같이 응답에 대한 부가적인 정보를 갖는 헤더이다
      - Vary, Accept-Ranges와 같이 상태 줄에 넣기에는 공간이 부족했던 추가 정보를 제공한다
  • Representation headers
      - Entity headers라고 부르기도 한다
      - body에 담긴 리소스의 정보(콘텐츠 길이, MIME 타입 등)를 포함하는 헤더이다

 

2. Spring MVC에서 HTTP Header의 사용

 1) 클라이언트와 서버 관점에서의 대표적인 HTTP 헤더

  • 클라이언트와 서버의 관점에서 내부적으로 가장 많이 사용되는 헤더 정보로 ‘Content-Type’이 있다
    - 클라이언트와 서버가 주고 받는 HTTP 메시지 body의 데이터 형식을 알려준다
    - 클라이언트와 서버는 Content-Type의 데이터 형식에 맞는 데이터들을 주고 받는다
  • 샘플로 작성하고 있는 커피주문 애플리케이션의 Content-Type은 ‘application/json’이다
    - Spring MVC - API 계층 - Controller (MVC 개요/핸들러 메서드) 에서는 prosuce 값으로 사용했지만, Spring MVC - API 계층 -Controller (ResponseEntity 적용) 에서 Map 객체를 사용하면서 Content-Type을 삭제했다

 

 2) 대표적인 HTTP 헤더 예시

  • 개발자가 직접 코드 레벨에서 HTTP 헤더를 컨트롤 해야될 경우 사용하는 대표적인 HTTP 헤더이다
  • Authorization 헤더
    - 클라이언트가 적절한 자격 증명을 가지고 있는지를 확인하기 위한 정보이다
    - REST API 기반 애플리케이션의 경우 클라이언트와 서버 간의 로그인(사용자 ID/비밀번호) 인증(Authenticatioin)에 통과한 클라이언트들은 ‘Authorization’ 헤더 정보를 기준으로 인증에 통과한 클라이언트가 맞는지 확인하는 인가 절차를 거친다
  • User-Agent 헤더
    - 애플리케이션을 구현 과정에서 여러가지 유형의 클라이언트가 하나의 서버 애플리케이션에 요청을 전송하는 경우가 많다
    - 사용자에 따라 데스크탑이나 노트북의 웹 브라우저를 사용하거나 스마트폰, 태블릿 등 모바일에서 서버에 요청을 보낸다
    - 보내오는 요청의 클라이언트를 구분해서 응답 데이터를 다르게 보내줘야 되는 경우가 있다
    - 모바일, 데스크탑, 노트북의 화면 크기에 따라 더 많은 정보를 보여주기 위해 각각 데이터의 종류와 크기가 다를 수 있다
    - User-Agent 헤더 정보를 이용해서 클라이언트의 요청 브라우져를 구분해서 처리할 수 있다

3. 샘플 어플리케이션에 HTTP Header 적용

 1) @RequestHeader 로 개별 헤더 정보 받기

  • CoffeeController에 적용하여 실행한다
package com.dreamfactory.exam_controller.coffee;

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

import java.lang.reflect.Member;
import java.sql.SQLOutput;
import java.util.HashMap;
import java.util.Map;


@RestController
@RequestMapping(value="v1/coffees"/*, produces = MediaType.APPLICATION_JSON_VALUE*/)
//v1은 버젼을 의미한다. coffees는 데이터 post, get 의 조회 위치를 의미한다

public class CoffeeController {
    @PostMapping

    //postCoffee() 메서드는 커피 정보를 등록해 준다
    public /*String*/ResponseEntity postCoffee(//@RequestParam("coffee")String coffee,
                                               @RequestHeader("user-agent")String userAgent,
                                                 @RequestParam("coffeeId")String coffeeId,
                                                 @RequestParam("korName")String korName,
                                                 @RequestParam("engName")String engName,
                                                 @RequestParam("price")int price) {
//        System.out.println("# coffee:" + coffee);
//        System.out.println("# coffeeId:" + coffeeId);
//        System.out.println("# korName:" + korName);
//        System.out.println("# engName:" + engName);
//        System.out.println("# price:" + price);

        //@RequestHeader 로 개별 정보 받아오기
        System.out.println("user-agent: " + userAgent);
        return new ResponseEntity<>(new Coffee(korName, engName, price),
        HttpStatus.CREATED);

        //Map객체로 변경
//            Map<String, String> map = new HashMap<>();
//            map.put("coffee", coffee);
//            map.put("coffeeId", coffeeId);
//            map.put("korName", korName);
//            map.put("engName", engName);
//            map.put("price", String.valueOf(price));
//
//            //return 값을 ResponseEntity로 변경
//        return new ResponseEntity<>(map, HttpStatus.CREATED);

//        String reponse =
//                "{\"" +
//                    "coffee\":\""+coffee+"\"," +
//                    "\"coffeeId\":\""+coffeeId+"\"," +
//                    "\"korName\":\""+korName+"\"," +
//                    "\"engName\":\""+engName+"\"," +
//                    "\"price\":\""+price+
//                "\"}";
//        return reponse;
    }

    @GetMapping("/{coffee-id}")

    //getCoffee() 메서드는 커피 정보을 클라이언트에게 제공하는 핸들러 메서드이다
    public /*String*/ResponseEntity getCoffee(@PathVariable("coffee-id")long coffeeId) {
        System.out.println("# coffeeId: " + coffeeId);
        return new ResponseEntity<>(HttpStatus.OK);
        }

    @GetMapping //별도의 URI를 지정해주지 않았기 때문에 클래스 레벨의 URI(“/v1/coffees”)에 매핑된다

    //getCoffees() 메서드는 커피 목록을 클라이언트에게 제공하는 핸들러 메서드이다
    public /*String*/ResponseEntity getCoffees() {
        System.out.println("# get Coffees");
        return new ResponseEntity<>(HttpStatus.OK);
    }
}
  • 실행하면 error가 발생한다
  • new Coffee에 대한 신규 class를 생성하여 문제를 해결한다
package com.dreamfactory.exam_controller.coffee;

public class Coffee {
    public Coffee(String korName, String engName, int price) {
    }
}

 

 2) @RequestHeader 로 전체 헤더 정보 받기

  • MemberController에 적용하여 실행한다
package com.dreamfactory.exam_controller.member;

import ch.qos.logback.classic.util.LogbackMDCAdapter;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.sql.SQLOutput;

import java.util.HashMap;
import java.util.Map;

@RestController

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

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) {
       
//        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);

        //@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를 의미한다
//            return new ResponseEntity<>(map, HttpStatus.CREATED);

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

//        String reponse =
//                "{\"" +
//                    "email\":\""+email+"\"," +
//                    "\"name\":\""+name+"\",\"" +
//                    "phone\":\"" + phone+
//                "\"}";
//        return reponse;
    }

    @GetMapping("/{member-id}")

    public /*String*/ResponseEntity getMember(@PathVariable("member-id")long memberId) {
       
        System.out.println("# memberId: " + memberId);
        //리턴 값을 변경된 ResponseEntity로 대체
        //HttpStatus.OK 는 200, OK를 의미한다
        return new ResponseEntity<>(HttpStatus.OK);
        //not implementation
//        return null;
    }

    @GetMapping //별도의 URI를 지정해주지 않았기 때문에 클래스 레벨의 URI(“/v1/members”)에 매핑된다

    //getMembers() 메서드는 회원 목록을 클라이언트에게 제공하는 핸들러 메서드이다
    public /*String*/ResponseEntity getMembers() {
        System.out.println("# get Members");
        //리턴 값을 변경된 ResponseEntity로 대체
        //HttpStatus.OK 는 200, OK를 의미한다
        return new ResponseEntity<>(HttpStatus.OK);
        //not implementation
//        return null;
    }
}
  • 위의 코드를 실행하면 error가 발생한다
  • Member class를 생성하라는 메시지가 확인된다
  • new Member에 대한 신규 class를 생성해 주면 해결된다
package com.dreamfactory.exam_controller.member;

public class  Member {
    public Member(String email, String name, String phone) {
    }
}
  • postman으로 get/post를 실행하면 아래와 같은 CLI 메시지가 출력된다

 

 3) HttpServletRequest 객체로 헤더 정보 얻기

  • HttpServletRequest 객체를 이용하면 Request 헤더 정보에 다양한 방법으로 접근이 가능하다
  • HttpServletRequest는 다양한 API를 지원하지만 특정 헤더 정보에 접근하고자 한다면 @RequestHeader 가 더 용이하다
  • orderController에 HttpservletRequest 적용하기
package com.dreamfactory.exam_controller.order;

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

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping(value="v1/orders"/*, produces = MediaType.APPLICATION_JSON_VALUE*/)

public class OrderController {
    @PostMapping

    //postOrder() 메서드는 커피 주문 정보를 등록한다
    public /*String*/ResponseEntity postOrder(HttpServletRequest httpServletRequest,
                                              @RequestParam("memberId") String memberId,
                                              @RequestParam("coffeeId") String coffeeId) {

////        Map<String, String> map = new HashMap<>();
////        map.put("memberId", memberId);
////        map.put("coffeeId", coffeeId);
//
//        return new ResponseEntity(map, HttpStatus.CREATED);

        System.out.println("user-agent: " + httpServletRequest.getHeader("user-agent"));
        return new ResponseEntity<>(new Order(memberId, coffeeId),
                HttpStatus.CREATED);
//        System.out.println("# memberId:" + memberId);
//        System.out.println("# coffeeId:" + coffeeId);
//
//        String reponse =
//                "{\"" +
//                        "memberId\":\"" + memberId + "\"," +
//                        "\"coffeeId\":\"" + coffeeId +
//                        "\"}";
//        return reponse;
    }

    @GetMapping("/{order-id}")
    public /*String*/ResponseEntity getOrder(@PathVariable("order-id") long orderId) {
        System.out.println("# orderId: " + orderId);
        return new ResponseEntity(HttpStatus.OK);
    }

    @GetMapping //별도의 URI를 지정해주지 않았기 때문에 클래스 레벨의 URI(“/v1/orders”)에 매핑된다

    //getOrders() 메서드는 주문 목록을 클라이언트에게 제공하는 핸들러 메서드이다
    public /*String*/ResponseEntity getOrders() {
        System.out.println("# get Orders");
        return new ResponseEntity(HttpStatus.OK);
    }
}
  • 실행하면 error가 발생한다
  • new Order에 대한 신규 class를 생성하면 해결된다
package com.dreamfactory.exam_controller.order;

public class Order {
    public  Order (String memberId, String CoffeeId){

    };
}
  • postman으로 post/get를 실행하면 아래와 같이 CLI가 출력된다

 

 

 

※ 참조 링크

▶ HTTP Header : https://developer.mozilla.org/ko/docs/Web/HTTP/Headers

 

HTTP 헤더 - HTTP | MDN

HTTP 헤더는 클라이언트와 서버가 요청 또는 응답으로 부가적인 정보를 전송할 수 있도록 해줍니다. HTTP 헤더는 대소문자를 구분하지 않는 이름과 콜론 ':' 다음에 오는 값(줄 바꿈 없이)으로 이루

developer.mozilla.org

 HttpServletRequest : https://docs.oracle.com/javaee/7/api/index.html?javax/servlet/http/HttpServletRequest.html 

 

Java(TM) EE 7 Specification APIs

 

docs.oracle.com

https://docs.oracle.com/javaee/7/api/index.html?javax/servlet/http/HttpServletResponse.html 

 

Java(TM) EE 7 Specification APIs

 

docs.oracle.com

User Agent의 유형 : https://gist.github.com/pzb/b4b6f57144aea7827ae4

 

user-agents.txt

GitHub Gist: instantly share code, notes, and snippets.

gist.github.com

https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent

 

User-Agent - HTTP | MDN

The User-Agent request header is a characteristic string that lets servers and network peers identify the application, operating system, vendor, and/or version of the requesting user agent.

developer.mozilla.org

▶ 백엔드 서비스에 커스텀 헤더 만들기 : https://cloud.google.com/load-balancing/docs/https/custom-headers?hl=ko 

 

백엔드 서비스에 커스텀 헤더 만들기  |  부하 분산  |  Google Cloud

전역 외부 HTTP(S) 부하 분산기(기본)에서 사용하는 백엔드 서비스의 커스텀 헤더를 구성합니다.

cloud.google.com

HTTP 헤더 및 Application Load Balancer : https://docs.aws.amazon.com/ko_kr/elasticloadbalancing/latest/application/x-forwarded-headers.html

 

HTTP 헤더 및 Application Load Balancer - Elastic Load Balancing

HTTP 요청 및 HTTP 응답은 헤더 필드를 사용하여 HTTP 메시지에 대한 정보를 전송합니다. HTTP 헤더가 자동으로 추가됩니다. 헤더 필드는 콜론으로 구분된 이름-값 페어이며 CR(캐리지 리턴) 및 LF(줄

docs.aws.amazon.com

 

■ 이번 페이지는 https://coding-mid-life.tistory.com/60?category=1287134  에서 작성된 Controller 예제의 개선을 진행한다

 

Spring MVC - Controller (MVC 개요/핸들러 메서드)

1. Controller 클래스 설계 및 구조 생성 API 계층을 Spring MVC 기반의 코드로 구현해 본다  1) 애플리케이션 경계 설정 커피 주문 애플리케이션으로 설정  2) 애플리케이션 기능 요구사항 수집 기능적

coding-mid-life.tistory.com

 

1. MemberComtroller 개선

  • ResponseEntity를 사용한다
    - ResponseEntity는 HttpEntity의 확장 클래스이다
    - HttpStatus 상태 코드를 추가한 전체 HTTP응답을 표현한다(상태코드, 헤더, 본문)
    - @Controller @RestController 애너테이션이 붙은 Controller 클래스의 핸들러 메서의 요청에 대한 응답을 표현한다
    - RestTemplate에서 외부 API통신에 대한 응답을 전달받아서 표현할 경우에 사용한다

  • Map 과 HashMap을 사용한다

 1) 기존 작성 코드

package com.dreamfactory.exam_controller.member;

import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;

import java.sql.SQLOutput;

@RestController

@RequestMapping(value ="v1/members", produces = MediaType.APPLICATION_JSON_VALUE)

public class MemberController {
    @PostMapping
    public String postMember(@RequestParam("email")String email,
                             @RequestParam("name")String name,
                             @RequestParam("phone")String phone) {        
        System.out.println("# email: " + email);
        System.out.println("# name: " + name);
        System.out.println("# phone: " + phone);
       
        //---> code change <---
        String reponse =
                "{\"" +
                    "email\":\""+email+"\"," +
                    "\"name\":\""+name+"\",\"" +
                    "phone\":\"" + phone+
                "\"}";
        return reponse;
    }

    @GetMapping("/{member-id}")    
    public String getMember(@PathVariable("member-id")long memberId) {
        
        System.out.println("# memberId: " + memberId);

        //not implementation
        return null;
    }

    @GetMapping 
    public String getMembers() {
        System.out.println("# get Members");

        //not implementation
        return null;
    }
}
  • 위의 코드에서 다음 부분이 개선이 우선된다
    - JSON 형식으로 응답을 받기 위해 작성된 코드이다
String reponse =
                "{\"" +
                    "email\":\""+email+"\"," +
                    "\"name\":\""+name+"\",\"" +
                    "phone\":\"" + phone+
                "\"}";
        return reponse;
  • 위의 수동 코드를 Map 메서드를 사용하여 개선하였다
//JSON문자열 응답 타입을 수작업 코드에서 Map객체로 변경->produce애트리뷰트를 삭제할 수 있다
//MAp 객체를 리턴하면 내부적으로 응답 데이터를 JSON데이터로 자동 변환해야 한다고 인식한다
Map<String, String> map = new HashMap<>();
map.put("email", email);
map.put("name", name);
map.put("phone", phone);
//리턴 값을 변경된 ResponseEntity로 대체
//ResponseEntity 객체를 생성하고 생성자 파라미터로 map과 HttpStatus.CREATED를 반환한다
//HttpStatus.CREATED 는 201, created를 의미한다
return new ResponseEntity<>(map, HttpStatus.CREATED)
  • 전체적으로 개선된 코드는 아래와 같다
  • getMember()와 getMembers() 핸들러 메서드도 ResponseEntity 객체를 리턴하고 HttpStatus.OK의 응답을 전송하는 것으로 수정되었다
package com.dreamfactory.exam_controller.member;

import ch.qos.logback.classic.util.LogbackMDCAdapter;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.sql.SQLOutput;
//@애터테이션에 의해 자동으로 import 된다

import java.util.HashMap;
import java.util.Map;


@RestController
//Spring MVC에서는 특정 클래스에 @RestController 를 추가하면 해당 클래스가 REST API의 리소스(자원, Resource)를
  처리하기 위한 API 엔드포인트로 동작함을 정의한다
//@RestController가 추가된 클래스는 애플리케이션 로딩 시, Spring Bean 으로 등록 해 준다
//REST API란 REST 방식을 통해서 리소스에 접근하기 위한 서비스 API를 지칭한다
//REST(Representational State Transfer)는 HTTP 네트워크 상의 리소스(Resource, 자원)를 정의하고 
  해당 리소스를 URI라는 고유의 주소로 접근하는 접근 방식을 의미한다

@RequestMapping("v1/members"/*,produce=MediaType.APPLICATION_JSON_VALUE*/)
//produces애트리뷰트(Attribute)는 응답 데이터를 어떤 미디어 타입으로 클라이언트에게 전송할 지를 설정한다


public class MemberController {
    @PostMapping

    //postMember() 메서드는 회원 정보를 등록해주는 핸들러 메서드이다
    //클라이언트의 요청 데이터(request body)를 서버에 생성할 때 사용하는 애너테이션이다
    //클라이언트에서 요청 전송 시 HTTP Method 타입을 동일하게 맞춰 주어야 한다(POST)
    public /*String*/ResponseEntity postMember(@RequestParam("email")String email,
                                                  @RequestParam("name")String name,
                                                  @RequestParam("phone")String phone) {
        //@RequestParam() 핸들러 메서드의 파라미터 종류 중 하나이다
        /*클라이언트에서 전송하는 요청 데이터를 쿼리 파라미터(Query Parmeter 또는 Query String),
        폼 데이터(form-data), x-www-form-urlencoded 형식으로 전송하면 서버 쪽에서 전달 받을 때
        사용하는 애너테이션이다*/

//        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);
            //리턴 값을 변경된 ResponseEntity로 대체
            //ResponseEntity 객체를 생성하고 생성자 파라미터로 map과 HttpStatus.CREATED를 반환한다
            //HttpStatus.CREATED 는 201, created를 의미한다
            return new ResponseEntity<>(map, HttpStatus.CREATED);


        //클라이언트 쪽에서 JSON 형식의 데이터를 전송 받아야 하기 때문에
        //응답 문자열을 JSON 형식에 맞게 작성한다
//        String reponse =
//                "{\"" +
//                    "email\":\""+email+"\"," +
//                    "\"name\":\""+name+"\",\"" +
//                    "phone\":\"" + phone+
//                "\"}";
//        return reponse;
    }

    @GetMapping("/{member-id}")
    //@GetMapping은 클라이언트가 서버에 리소스를 조회할 때 사용하는 애너테이션이다
    //@GetMapping 애너테이션의 괄호 안에는 몇 가지 애트리뷰트(Attribute)를 사용할 수 있다
    //여기서는 전체 HTTP URI의 일부를 지정했다
    //클라이언트에서 getMember() 핸들러 메서드에 요청을 보낼 경우, 최종 URI는 형태는 아래와 같다
    // /v1/members/{member-id}
    //{member-id}는 회원 식별자를 의미한다
    //클라이언트가 요청을 보낼 때 URI로 어떤 값을 지정하느냐에 따라서 동적으로 바뀌는 값이다

    public /*String*/ResponseEntity getMember(@PathVariable("member-id")long memberId) {
        //getMember() 메서드는 특정 회원의 정보를 클라이언트 쪽에 제공하는 핸들러 메서드이다

        //@PathVariable역시 핸들러 메서드의 파라미터 종류 중 하나이다
        //@PathVariable의 괄호 안에 입력한 문자열 값은 @GetMapping("/{member-id}") 처럼 중괄호({ })
          안의 문자열과 동일해야 한다
        //여기서는 두 문자열 모두 “member-id” 로 동일하게 지정했다
        //두 문자열이 다르면 MissingPathVariableException이 발생한다

        System.out.println("# memberId: " + memberId);
        //리턴 값을 변경된 ResponseEntity로 대체
        //HttpStatus.OK 는 200, OK를 의미한다
        return new ResponseEntity<>(HttpStatus.OK);
        //not implementation
//        return null;
    }

    @GetMapping //별도의 URI를 지정해주지 않았기 때문에 클래스 레벨의 URI(“/v1/members”)에 매핑된다

    //getMembers() 메서드는 회원 목록을 클라이언트에게 제공하는 핸들러 메서드이다
    public /*String*/ResponseEntity getMembers() {
        System.out.println("# get Members");
        //리턴 값을 변경된 ResponseEntity로 대체
        //HttpStatus.OK 는 200, OK를 의미한다
        return new ResponseEntity<>(HttpStatus.OK);
        //not implementation
//        return null;
    }
}
  • 수정된 코드를 실행한 후 postman에서 post로 request한 결과는 아래와 같다
  • map에 의한 파라미터 값과 HttpStatus.CREATED의 결과값 201 Created가 반환된다
  • ResponseEntity를 사용하지 않았던 코드의 결과값 200 OK 가 변경된 결과이다

  • CoffeeController 와 OrderController 도 개선된 코드로 수정한다
  • CoffeeController 
package com.dreamfactory.exam_controller.coffee;

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

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping(value="v1/coffees", produces = MediaType.APPLICATION_JSON_VALUE)
//v1은 버젼을 의미한다. coffees는 데이터 post, get 의 조회 위치를 의미한다

public class CoffeeController {
    @PostMapping

    //postCoffee() 메서드는 커피 정보를 등록해 준다
    public /*String*/ResponseEntity postCoffee(@RequestParam("coffee")String coffee,
                                                 @RequestParam("coffeeId")String coffeeId,
                                                 @RequestParam("korName")String korName,
                                                 @RequestParam("engName")String engName,
                                                 @RequestParam("price")int price) {
//        System.out.println("# coffee:" + coffee);
//        System.out.println("# coffeeId:" + coffeeId);
//        System.out.println("# korName:" + korName);
//        System.out.println("# engName:" + engName);
//        System.out.println("# price:" + price);

        //Map객체로 변경
            Map<String, String> map = new HashMap<>();
            map.put("coffee", coffee);
            map.put("coffeeId", coffeeId);
            map.put("korName", korName);
            map.put("engName", engName);
            map.put("price", String.valueOf(price));

            //return 값을 ResponseEntity로 변경
        return new ResponseEntity<>(map, HttpStatus.CREATED);

//        String reponse =
//                "{\"" +
//                    "coffee\":\""+coffee+"\"," +
//                    "\"coffeeId\":\""+coffeeId+"\"," +
//                    "\"korName\":\""+korName+"\"," +
//                    "\"engName\":\""+engName+"\"," +
//                    "\"price\":\""+price+
//                "\"}";
//        return reponse;
    }

    @GetMapping("/{coffee-id}")

    //getCoffee() 메서드는 커피 정보을 클라이언트에게 제공하는 핸들러 메서드이다
    public /*String*/ResponseEntity getCoffee(@PathVariable("coffee-id")long coffeeId) {
        System.out.println("# coffeeId: " + coffeeId);
        return new ResponseEntity<>(HttpStatus.OK);
        }

    @GetMapping //별도의 URI를 지정해주지 않았기 때문에 클래스 레벨의 URI(“/v1/coffees”)에 매핑된다

    //getCoffees() 메서드는 커피 목록을 클라이언트에게 제공하는 핸들러 메서드이다
    public /*String*/ResponseEntity getCoffees() {
        System.out.println("# get Coffees");
        return new ResponseEntity<>(HttpStatus.OK);
    }
}

 

  • OrderController
package com.dreamfactory.exam_controller.order;

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

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping(value="v1/orders"/*, produces = MediaType.APPLICATION_JSON_VALUE*/)

public class OrderController {
    @PostMapping

    //postOrder() 메서드는 커피 주문 정보를 등록한다
    public /*String*/ResponseEntity postOrder(@RequestParam("memberId") String memberId,
                                              @RequestParam("coffeeId") String coffeeId) {

        Map<String, String> map = new HashMap<>();
        map.put("memberId", memberId);
        map.put("coffeeId", coffeeId);

        return new ResponseEntity(map, HttpStatus.CREATED);
//        System.out.println("# memberId:" + memberId);
//        System.out.println("# coffeeId:" + coffeeId);
//
//        String reponse =
//                "{\"" +
//                        "memberId\":\"" + memberId + "\"," +
//                        "\"coffeeId\":\"" + coffeeId +
//                        "\"}";
//        return reponse;
    }

    @GetMapping("/{order-id}")
    public /*String*/ResponseEntity getOrder(@PathVariable("order-id") long orderId) {
        System.out.println("# orderId: " + orderId);
        return new ResponseEntity(HttpStatus.OK);
    }

    @GetMapping //별도의 URI를 지정해주지 않았기 때문에 클래스 레벨의 URI(“/v1/orders”)에 매핑된다

    //getOrders() 메서드는 주문 목록을 클라이언트에게 제공하는 핸들러 메서드이다
    public /*String*/ResponseEntity getOrders() {
        System.out.println("# get Orders");
        return new ResponseEntity(HttpStatus.OK);
    }
}

 

 

※ 참조 링크

▶ ResponseEntity : https://itvillage.tistory.com/44

 

ResponseEntity 알아보기

ResponseEntity란? ResponseEntity는 HttpEntity의 확장 클래스로써 HttpStatus 상태 코드를 추가한 전체 HTTP 응답(상태 코드, 헤더 및 본문)을 표현하는 클래스입니다. ResponseEntity를 어디에 사용할 수 있나..

itvillage.tistory.com

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/http/ResponseEntity.html

 

ResponseEntity (Spring Framework 5.3.21 API)

Extension of HttpEntity that adds an HttpStatus status code. Used in RestTemplate as well as in @Controller methods. In RestTemplate, this class is returned by getForEntity() and exchange(): ResponseEntity entity = template.getForEntity("https://example.co

docs.spring.io

 

1. Controller 클래스 설계 및 구조 생성

  • API 계층을 Spring MVC 기반의 코드로 구현해 본다

 1) 애플리케이션 경계 설정

  • 커피 주문 애플리케이션으로 설정

 2) 애플리케이션 기능 요구사항 수집

  • 기능적으로 요구사항을 정리해 본다

 3) 패키지 구조 생성

  • 기능 기반 패키지 구조(package-by-feature)
    - 애플리케이션에서 구현해야 하는 기능을 기준으로 패키지를 구성하는 것을 말한다
    - 패키지를 커피 / 회원 / 주문 등 기능으로 구분할 수 있다
  • 계층 기반 패키지 구조(package-by-layer)
    - 패키지를 하나의 계층(Layer)으로 보고 클래스들을 계층별로 묶어서 관리하는 구조를 말한다
    - controller / model / repository / service 등 계층으로 구분할 수 있다
  • 테스트와 리팩토링이 용이하고, 향후에 마이크로 서비스 시스템으로의 분리가 상대적으로 용이기능 기반 패키지 구조 사용을 권장한다

 4) 애플리케이션의 기능 요구사항 및 클래스 정리

  • REST API 기반의 애플리케이션에서는 일반적으로 애플리케이션이 제공해야 될 기능을 리소스(Resource, 자원)로 분류한다
  • 리소스에 해당하는 Controller 클래스를 작성한다

 5) 엔트리포인트(Entrypoint) 클래스 작성

  • Spring Boot 기반의 애플리케이션이 정상적으로 실행되기 위해서 가장 먼저 해야될 일은 main() 메서드가 포함된 애플리케이션의 엔트리포인트(Entrypoint, 애플리케이션 시작점)를 작성하는 것이다
  • 부트스트랩(Bootstrap)은 애플리케이션이 실행되기 전에 여러가지 설정 작업을 수행하여 실행 가능한 애플리케이션으로 만드는 단계를 의미한다
  • 엔트리포인트는 spring initializr를 사용하여 프로젝트를 생성하면 자동으로 만들어 진다

  • exam_controller > src > main > java > com.dreamfactory.exam_controller > ExamControllerApplication 으로 자동 생성된다
//엔트리포인트 클래스

package com.dreamfactory.exam_controller;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
//자동구성을 활성화 해 준다
//애플리케이션 패키지 내에서 @Component가 붙은 클래스를 검색한 후(scan), Spring Bean으로 등록하는 기능을 
  활성화 해 준다
//@Configuration 이 붙은 클래스를 자동으로 찾아주고, 추가적으로 Spring Bean을 등록하는 기능을 활성화 해 준다

public class ExamControllerApplication {

	public static void main(String[] args) {
		SpringApplication.run(ExamControllerApplication.class, args);
	}
	//SpringApplication.run : Spring 애플리케이션을 부트스트랩하고, 실행하는 역할을 한다
}

 

 6) 애플리케이션의 Controller 구조 작성

  • 회원관리를 위한 MemberController 구조 작성
package com.dreamfactory.exam_controller.member;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
//@애터테이션에 의해 자동으로 import 된다

@RestController
//Spring MVC에서는 특정 클래스에 @RestController 를 추가하면 해당 클래스가 REST API의 리소스(자원, Resource)를
  처리하기 위한 API 엔드포인트로 동작함을 정의한다
//@RestController가 추가된 클래스는 애플리케이션 로딩 시, Spring Bean 으로 등록 해 준다

@RequestMapping
//@RequestMapping 은 클라이언트의 요청과 클라이언트 요청을 처리하는 핸들러 메서드(Handler Method)를 매핑해주는
  역할을 한다
//Controller 클래스 레벨에 추가하여 클래스 전체에 사용되는 공통 URL(Base URL)을 설정할 수 있다

public class MemberController {

}
  • 커피정보를 위한 CoffeeController 구조 작성
package com.dreamfactory.exam_controller.coffee;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/v1/coffees")
//v1은 버젼을 의미한다. coffees는 데이터 post, get 의 조회 위치 URI를 지정한다
//Controller 클래스 레벨에 추가하여 클래스 전체에 사용되는 공통 URL(Base URL)을 설정할 수 있다.

public class CoffeeController {

}
  • 주문관리를 위한 OrderController 구조 작성
package com.dreamfactory.exam_controller.order;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping

public class OrderController {

}

 

[참조 자료]

※ REST API란?

  • REST(Representational State Transfer)는 HTTP 네트워크 상의 리소스(Resource, 자원)를 정의하고 해당 리소스를 URI라는 고유의 주소로 접근하는 접근 방식을 의미한다
  • REST API란 REST 방식을 통해서 리소스에 접근하기 위한 서비스 API를 지칭한다

 REST에서 의미하는 리소스

  • REST에서 의미하는 자원은 데이터베이스에 저장된 데이터, 문서, 이미지, 동영상 등 HTTP 통신을 통해 주고 받을 수 있는 모든 것을 의미한다

 URI(Uniform Resource Identifier)와 URL(Uniform Resource Locator)

  • URI는 네트워크 상에 있는 특정 리소스를 식별하는 통합 자원 식별자(Uniform Resource Identifier)를 의미한다
  • URL은 인터넷에 있는 리소스를 나타내는 통합 리소스 식별자를 의미한다
  • 일상적인 웹 상의 주소는 URL을 의미한다
  • URI는 URL의 상위 개념으로 볼 수 있다
  • URI는 리소스를 식별하는 식별자 역할을 하고, URL은 리소스의 위치를 가리킨다
    - http://www.itivllage.tistory.com/manage? id = 1 에서
    - http://www.itivllage.tistory.com/manage'까지는 리소스의 위치를 가리키는 URL이고
    - http://www.itivllage.tistory.com/manage? id = 1는 리소스를 식별하기 위한 'id = 1'이라는 고유 식별자가 붙었으므로 URI이다

※ HTTP 에서 REST API 서비스를 만드는 경우의 REST API URI 작성 규칙

  • URI의 마지막이 '/' 로 끝나지 않게 한다
    - http://www.dreamfactory.com/coffees   (good)
    - http://www.dreamfactory.com/coffees/ (bad)
  • 동사 보다는 명사를 사용한다
    - http://www.dreamfactory.com/coffees             (good)
    - http://www.dreamfactory.com/coffees/update (bad) 
  • 단수형 보다는 복수형 명사를 사용한다
    - http://www.dreamfactory.com/coffees   (good)
    - http://www.dreamfactory.com/coffee     (bad)
  • URI는 기본 소문자를 사용한다
  • 언더스코어( _ ) 대신에 하이픈(-)을 사용한다
  • 파일 확장자는 URI에 포함하지 않는다

 

※ 참조 링크

 

RESTful API의 URL 작성 규칙

REST API란? REST(Representational State Transfer)는 HTTP 네트워크 상의 리소스(Resource, 자원)를 정의하고 해당 리소스를 URI라는 고유의 주소로 접근하는 접근 방식을 의미하며, REST API란 REST 방식을 통..

itvillage.tistory.com

 

@SpringBootApplication 의 역할

[코드 1-1] @SpringBootApplication public class CoffeeApplication { public static void main(String[] args) { SpringApplication.run(CoffeeApplication.class, args); } } [코드 1-1]과 같이 Spring Boot Ap..

itvillage.tistory.com

Spring Boot 애플리케이션의 부트스트랩(Bootstrap) 과정: https://itvillage.tistory.com/37

 

Spring Boot 애플리케이션의 부트스트랩(Bootstrap) 과정 알아보기

Spring Boot에서의 부트스트랩이란? 일반적으로 부트스트랩(Bootstrap)이란 어떠한 과정이 시작되어 알아서 진행되는 일련의 과정을 의미합니다. 컴퓨터의 부팅 과정을 생각해보면 이해가 쉬울것입

itvillage.tistory.com

 

2. 핸들러 메서드(Handler Method)

postman 에서 request 결과

  • MemberController에 클라이언트의 요청을 처리할 핸들러 메서드(Handler Method)가 아직 없기 때문에 localhost를 요청해도 에러가 발생한다
  • 작업 중인 애플리케이션은 REST API 기반 애플리케이션이기 때문에 응답 메시지는 JSON 형식으로 클라이언트에게 전달된다

 1) MemberController의 핸들러 메서드(Handler Method) 작성

  • 회원 이메일 주소: email
  • 회원 이름: name
  • 회원 전화번호: phoneNumber
  • 위의 정보를 기준으로 작성한다
package com.dreamfactory.exam_controller.member;

import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;

import java.sql.SQLOutput;
//@애터테이션에 의해 자동으로 import 된다

@RestController

@RequestMapping(value ="v1/members", produces = MediaType.APPLICATION_JSON_VALUE)
//produces애트리뷰트(Attribute)는 응답 데이터를 어떤 미디어 타입으로 클라이언트에게 전송할 지를 설정한다
//JSON 형식의 데이터를 응답 데이터로 전송하기 위해 MediaType.APPLICATION_JSON_VALUE 값을 설정했다
//이 값을 설정하지 않으면 현재 코드에서는 JSON 형식의 데이터를 응답으로 전송하지 않고, 문자열 자체를 전송한다


public class MemberController {
    @PostMapping

    //postMember() 메서드는 회원 정보를 등록해주는 핸들러 메서드이다
    //클라이언트의 요청 데이터(request body)를 서버에 생성할 때 사용하는 애너테이션이다
    //클라이언트에서 요청 전송 시 HTTP Method 타입을 동일하게 맞춰 주어야 한다(POST)
    public String postMember(@RequestParam("email")String email,
                             @RequestParam("name")String name,
                             @RequestParam("phone")String phone) {
        //@RequestParam() 핸들러 메서드의 파라미터 종류 중 하나이다
        /*클라이언트에서 전송하는 요청 데이터를 쿼리 파라미터(Query Parmeter 또는 Query String),
        폼 데이터(form-data), x-www-form-urlencoded 형식으로 전송하면 서버 쪽에서 전달 받을 때
        사용하는 애너테이션이다*/

        System.out.println("# email: " + email);
        System.out.println("# name: " + name);
        System.out.println("# phone: " + phone);

        //클라이언트 쪽에서 JSON 형식의 데이터를 전송 받아야 하기 때문에
        //응답 문자열을 JSON 형식에 맞게 작성한다
        String reponse =
                "{\"" +
                    "email\":\""+email+"\"," +
                    "\"name\":\""+name+"\",\"" +
                    "phone\":\"" + phone+
                "\"}";
        return reponse;
    }

    @GetMapping("/{member-id}")
    //@GetMapping은 클라이언트가 서버에 리소스를 조회할 때 사용하는 애너테이션이다
    //@GetMapping 애너테이션의 괄호 안에는 몇 가지 애트리뷰트(Attribute)를 사용할 수 있다
    //여기서는 전체 HTTP URI의 일부를 지정했다
    //클라이언트에서 getMember() 핸들러 메서드에 요청을 보낼 경우, 최종 URI는 형태는 아래와 같다
    // /v1/members/{member-id}
    //{member-id}는 회원 식별자를 의미한다
    //클라이언트가 요청을 보낼 때 URI로 어떤 값을 지정하느냐에 따라서 동적으로 바뀌는 값이다

    public String getMember(@PathVariable("member-id")long memberId) {
        //getMember() 메서드는 특정 회원의 정보를 클라이언트 쪽에 제공하는 핸들러 메서드이다

        //@PathVariable역시 핸들러 메서드의 파라미터 종류 중 하나이다
        //@PathVariable의 괄호 안에 입력한 문자열 값은 @GetMapping("/{member-id}") 처럼 중괄호({ }) 안의 문자열과 동일해야 한다
        //여기서는 두 문자열 모두 “member-id” 로 동일하게 지정했다
        //두 문자열이 다르면 MissingPathVariableException이 발생한다

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

        //not implementation
        return null;
    }

    @GetMapping //별도의 URI를 지정해주지 않았기 때문에 클래스 레벨의 URI(“/v1/members”)에 매핑된다

    //getMembers() 메서드는 회원 목록을 클라이언트에게 제공하는 핸들러 메서드이다
    public String getMembers() {
        System.out.println("# get Members");

        //not implementation
        return null;
    }
}
  • 쿼리 파라미터(Query Parameter 또는 QueryString)
    - 요청 URL에서 ‘?’를 기준으로 붙는 key/value 쌍의 데이터를 말한다
    - ex) http://localhost:8080/coffees/1?page=1&size=10
  • RestController
    - Spring MVC에서 웹 요청을 하기 위한 준비가 완료되었음을 의미한다
  • RequestMapping
    - RequestMapping(value="파일경로", method={HTTP 요청값}) 의 형식으로 작성한다
    - HTTP 값을 지정하지 않으면 모든 HTTP요청에 매핑된다
    - Request에 대한 헤더 값을 지정하면 매핑 범위를 좁힐 수 있다
헤더 값을 지정하여 범위를 좁힌 예)

@RequestMapping(value = "/ex/dream", headers = "key=val", method = GET)
@ResponseBody
public String getDreamWithHeader() {
    return "Get some Dream with Header";
}
HTTP POST에 요청된 예)

@RequestMapping(value = "/ex/dream", method = POST)
@ResponseBody
public String postDream() {
    return "Post some Dream";
}
headers속성을 사용하여 헤더를 여러개 지정할 수 있다 

@RequestMapping(
  value = "/ex/dream", 
  headers = { "key1=val1", "key2=val2" }, method = GET)
@ResponseBody
public String getDreamWithHeaders() {
    return "Get some Dream with Header";
}

 

※ MemberController : postMember() 요청 및 응답

  • HTTP POST Method와 요청 URI를 입력한다
  • [Body] 탭에서 ‘x-www-form-urlencoded’ 형식의 요청 데이터를 KEY/VALUE 형태로 입력한다
  • JSON 형식의 응답 데이터를 전달 받을 수 있다
  • 우측 중간을 보면 ‘200’, ‘OK’ 라는 값을 통해서 클라이언트가 정상적으로 응답을 전달 받았음을 알 수 있다

 

 2) CoffeeController의 핸들러 메서드(Handler Method) 작성

  • 커피 식별자: coffee
  • 식별자: coffeeId
  • 커피명(영문): engName
  • 커피명(한글): korName
  • 가격: price
package com.dreamfactory.exam_controller.coffee;

import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping(value="v1/coffees", produces = MediaType.APPLICATION_JSON_VALUE)
//v1은 버젼을 의미한다. coffees는 데이터 post, get 의 조회 위치를 의미한다

public class CoffeeController {
    @PostMapping

    //postCoffee() 메서드는 커피 정보를 등록해 준다
    public String postCoffee(@RequestParam("coffee")String coffee,
                             @RequestParam("coffeeId")String coffeeId,
                             @RequestParam("korName")String korName,
                             @RequestParam("engName")String engName,
                             @RequestParam("price")int price) {
        System.out.println("# coffee:" + coffee);
        System.out.println("# coffeeId:" + coffeeId);
        System.out.println("# korName:" + korName);
        System.out.println("# engName:" + engName);
        System.out.println("# price:" + price);

        String reponse =
                "{\"" +
                    "coffee\":\""+coffee+"\"," +
                    "\"coffeeId\":\""+coffeeId+"\"," +
                    "\"korName\":\""+korName+"\"," +
                    "\"engName\":\""+engName+"\"," +
                    "\"price\":\""+price+
                "\"}";
        return reponse;
    }

    @GetMapping("/{coffee-id}")

    //getCoffee() 메서드는 커피 정보을 클라이언트에게 제공하는 핸들러 메서드이다
    public String getCoffee(@PathVariable("coffee-id")long coffeeId) {
        System.out.println("# coffeeId: " + coffeeId);
        return null;
        }

    @GetMapping //별도의 URI를 지정해주지 않았기 때문에 클래스 레벨의 URI(“/v1/coffees”)에 매핑된다

    //getCoffees() 메서드는 커피 목록을 클라이언트에게 제공하는 핸들러 메서드이다
    public String getCoffees() {
        System.out.println("# get Coffees");
        return null;
    }
}

 

postman - v1/coffees post

 3) OrderController의 핸들러 메서드(Handler Method) 작성

  • 회원 식별자: memberId
  • 커피 식별자: coffeeId
package com.dreamfactory.exam_controller.order;

import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping(value="v1/orders", produces = MediaType.APPLICATION_JSON_VALUE)

public class OrderController {
    @PostMapping

    //postOrder() 메서드는 주문 정보를 등록한다
    public String postOrder(@RequestParam("memberId")String memberId,
                             @RequestParam("coffeeId")String coffeeId) {
        System.out.println("# memberId:" + memberId);
        System.out.println("# coffeeId:" + coffeeId);

        String reponse =
                "{\"" +
                    "memberId\":\""+memberId+"\"," +
                    "\"coffeeId\":\""+coffeeId+
                "\"}";
        return reponse;
    }

    @GetMapping("/{order-id}")
    public String getOrder(@PathVariable("order-id")long orderId) {
        System.out.println("# orderId: " + orderId);
        return null;
    }

    @GetMapping
    public String getOrders() {
        System.out.println("# get Orders");
        return null;
    }
}

※ 식별자(Identifier)

  • 어떤 데이터를 식별할 수 있는 고유값을 의미한다
  • 기본키(Primary key)는 대표적인 식별자 중 하나이다
  •  API 계층에서 사용하는 memberId, coffeeId 등은 데이터베이스의 테이블에 저장되는 로우(row)의 식별자인 기본키(Primary key)가 된다

 

참조 링크

▶ 핸들러메서드 파라미터 : https://itvillage.tistory.com/41

 

Controller 핸들러 메서드의 Argument 알아보기

Controller의 핸들러 메서드는 다양한 유형의 Argument(인수)를 지원합니다. 그 중에서 REST API 애플리케이션에서 자주 사용되는 유형의 Argument를 간단히 살펴보도록 하겠습니다. Method Argument 설명 @Reque

itvillage.tistory.com

 미디어 타입(Media Type) : https://ko.wikipedia.org/wiki/%EB%AF%B8%EB%94%94%EC%96%B4_%ED%83%80%EC%9E%85

 

미디어 타입 - 위키백과, 우리 모두의 백과사전

 

ko.wikipedia.org

▶ MIME 타입 : https://developer.mozilla.org/ko/docs/Web/HTTP/Basics_of_HTTP/MIME_Types

 

MIME 타입 - HTTP | MDN

MIME 타입이란 클라이언트에게 전송된 문서의 다양성을 알려주기 위한 메커니즘입니다: 웹에서 파일의 확장자는 별  의미가 없습니다. 그러므로, 각 문서와 함께 올바른 MIME 타입을 전송하도

developer.mozilla.org

https://developer.mozilla.org/ko/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types

 

MIME 타입의 전체 목록 - HTTP | MDN

다음은 일반적인 확장자로 정렬된, 문서 타입과 관련된 MIME 타입의 포괄적인 목록입니다.

developer.mozilla.org

 HTTP POST Method의 요청 데이터 형식 : https://developer.mozilla.org/ko/docs/Web/HTTP/Methods/POST

 

POST - HTTP | MDN

HTTP POST 메서드는 서버로 데이터를 전송합니다. 요청 본문의 유형은 Content-Type 헤더로 나타냅니다.

developer.mozilla.org

HTTP 응답 상태(Response Status) : https://developer.mozilla.org/ko/docs/Web/HTTP/Status

 

HTTP 상태 코드 - HTTP | MDN

HTTP 응답 상태 코드는 특정 HTTP 요청이 성공적으로 완료되었는지 알려줍니다. 응답은 5개의 그룹으로 나누어집니다: 정보를 제공하는 응답, 성공적인 응답, 리다이렉트, 클라이언트 에러, 그리고

developer.mozilla.org

 

1. Spring MVC

 1) 개요

  • 소프트웨어 설계에서 세 가지 구성 요소인 모델(Model), 뷰(View), 컨트롤러(Controller)를 이용한 설계 방식을 말한다
  • 웹 계층을 담당하는 몇가지 모듈 중에서 서블릿(Servlet) API를 기반으로 클라이언트의 요청을 처리하는 모듈을 spring-webmvc라고 한다
  • Spring Web MVC를 줄여서 'Spring MVC' 라고 한다
  • Spring MVC가 웹 프레임워크의 한 종류이기 때문에 'Spring MVC 프레임워크'라고도 한다
  • 클라이언트의 요청을 편리하게 처리해주는 프레임워크이다.

※ 서블릿(Servlet)

  • 클라이언트의 요청을 처리하도록 특정 규약에 맞추어서 Java 코드로 작성하는 클래스 파일이다
  • 아파치 톰캣(Apache Tomcat)은 서블릿들이 웹 애플리케이션으로 실행이 되도록 해주는 서블릿 컨테이너(Servlet Container) 중 하나이다
  • Spring MVC 내부에서는 서블릿을 기반으로 웹 애플리케이션이 동작한다

 2) 모델(MODEL)

  • Spring MVC 기반의 웹 애플리케이션은 클라이언트의 요청을 전달 받으면 요청 사항을 처리하기 위한 작업을 한다
  • 작업 완료 후 클라이언트에게 응답으로 돌려주는 처리한 작업의 결과 데이터를 Model이라고 한다
  • 클라이언트의 요청 사항을 구체적으로 처리하는 영역을 서비스 계층(Service Layer)이라고 한다
  • 요청 사항을 처리하기 위해 Java 코드로 구현한 것을 비즈니스 로직(Business Logic)이라고 한다

 3) 뷰(View)

  • Model 데이터를 이용해서 웹브라우저 같은 클라이언트 애플리케이션의 화면에 보여지는 리소스(Resource)를 제공하는 역할을 한다
  • View의 형태별 종류
    ▶ HTML 페이지의 출력
     - 클라이언트 애플리케이션에 보여지는 HTML 페이지를 직접 렌더링해서 클라이언트 측에 전송하는 방식이다
     - 기본적인 HTML 태그로 구성된 페이지에 Model 데이터를 채워서 최종 HTML 페이지를 만든 후 클라이언트 측에 전송한다
     - Spring MVC에서 지원하는 HTML 페이지 출력 기술에는 Thymeleaf, FreeMarker, JSP + JSTL, Tiles 등이 있다

     PDF, Excel 등의 문서 형태로 출력
     -  Model 데이터를 가공해서 PDF 문서나 Excel 문서를 만들어서 클라이언트 측에 전송하는 방식이다
     -  문서 내에서 데이터가 동적으로 변경되어야 하는 경우 사용할 수 있다

    ▶ XML, JSON 등 특정 형식의 포맷으로의 변환
     - Model 데이터를 특정 프로토콜 형태로 변환해서 클라이언트 측에 전송하는 방식이다
     - 특정 형식의 데이터만 전송하고, 프런트엔드 측에서 이 데이터를 기반으로 HTML 페이지를 만드는 방식이다
     - 프런트엔드 영역과 백엔드 영역이 명확하게 구분되므로 개발 및 유지보수가 상대적으로 용이하다
     - 프런트엔드 측에서 비동기 클라이언트 애플리케이션을 만드는 것이 가능해진다

※ JSON(JavaScript Object Notation)

  • JSON은 Spring MVC에서 클라이언트 애플리케이션과 서버 애플리케이션이 주고 받는 데이터 형식이다
  • 과거에는 XML 형식의 데이터가 많이 사용되었다
  • 현재는 XML보다 상대적으로 가볍고, 간결한 JSON 형식을 사용하고 있는 추세이다
  • JSON은 {"속성", "값"} 의 형태로 작성된다

 

 4) 컨트롤러(Controller)

  • 클라이언트 측의 요청을 직접적으로 전달 받는 엔드포인트(Endpoint)이다
  • Model 과 View 중간에서 상호 작용을 해주는 역할을 한다
  • 클라이언트 측의 요청을 전달 받아서 비즈니스 로직을 거친 후에 Model 데이터가 만들어지면 View로 전달하는 역할을 한다

 

2. Spring MVC의 동작 방식

Client가 요청 데이터 전송 → Controller가 요청 데이터 수신 → 비즈니스 로직 처리 → Model 데이터 생성

→ Controller에게 Model 데이터 전달 → Controller가 View에게 Model 데이터 전달 → View가 응답 데이터 생성

 

  • DispatcherServlet이 역활이 많아 보이지만 요청에 대한 처리는 다른 구성 요소들에게 위임(Delegate)하고 있다
  • DispatcherServlet이 애플리케이션의 가장 앞단에 배치되어 다른 구성요소들과 상호작용하면서 클라이언트의 요청을 처리하는 패턴을 Front Controller Pattern이라고 한다

 

 

 

 

※ 참조 링크

▶ JSON : https://ko.wikipedia.org/wiki/JSON

 

JSON - 위키백과, 우리 모두의 백과사전

 

ko.wikipedia.org

JOSN을 JAVA로 변환 : https://json2csharp.com/code-converters/json-to-pojo

 

JSON to POJO Object Online Converter - Json2CSharp Toolkit

 

json2csharp.com

동작방식 추가 : https://itvillage.tistory.com/entry/Spring-MVC%EC%9D%98-%EB%8F%99%EC%9E%91-%EB%B0%A9%EC%8B%9D-%EC%B6%94%EA%B0%80-%EC%84%A4%EB%AA%85

 

Spring MVC의 동작 방식 추가 설명

Handler 용어의 의미 핸들(Handle) 이라고 하면 일반적으로 자동차의 핸들을 제일 먼저 떠올릴 수 있는데, 자동차의 핸들은 운전자가 직접 핸들을 움직이면서 직접적으로 자동차의 주행을 처리하는

itvillage.tistory.com

 

1. Mock

  • 일반적으로 사용하는 목업(Mock-up)의 의미와 비슷하며, 진짜인 것 처럼 보이도록 만드는 유사한 상황이나 물건, 물질 등을 의미한다
  • 테스트 계통에서의 Mock는 가짜 객체를 의미한다
  • 단위 테스트나 슬라이스 테스트 등에 Mock 객체를 사용하는 것을 Mocking 이라고 한다

2. Mock 객체를 사용하는 이유

  • 테스트의 단위는 적을수록 효율적이다
  • Mock 객체를 사용하지 않으면 Request는 서비스계층을 거쳐 데이터 액세스 계을과 데이터베이스까지 순회한 후 되돌아 온다
  • Mock 객체를 사용하면 Request가 원하는 응답을 상관없는 계층을 제외하고 결과값을 전송할 수 있다

3. Mockito

  • 여러 오픈 소스 라이브러리를 통해 Mock 객체를 생성하고 진짜처럼 동작하게 한다
  • Spring Framework 자체적으로도 지원하고 있는 Mocking 라이브러리가 Mockito이다
  • Mocking 기능을 이용하면 테스트하고자 하는 대상에서 다른 영역(다른 계층 또는 외부 통신이 필요한 서비스 등)을 단절시켜 테스트 대상에만 집중할 수 있다

4. Mockito 예제

 1) MemberController의 postMember() 테스트에 Mockito 적용

package com.codestates.slice.mock;

import com.codestates.member.dto.MemberDto;
import com.codestates.member.entity.Member;
import com.codestates.member.mapper.MemberMapper;
import com.codestates.member.service.MemberService;
import com.codestates.stamp.Stamp;
import com.google.gson.Gson;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultActions;

import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
class MemberControllerMockTest {
    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private Gson gson;

    //@MockBean 애너테이션은 Application Context에 등록되어 있는 Bean에 대한 Mockito Mock 객체를 생성하고
    //주입해주는 역할을 한다
    //@MockBean 애너테이션을 필드에 추가하면 해당 필드의 Bean에 대한 Mock 객체를 생성한 후, 필드에 주입(DI)한다
    @MockBean
    //MemberService 빈에 대한 Mock 객체를 생성해서 memberService 필드에 주입한다
    private MemberService memberService;

    //MemberMapper를 DI 받는 이유는 MockMemberService(가칭)의 createMember()에서 리턴하는 Member 객체를
    //생성하기 위함이다
    @Autowired
    private MemberMapper mapper;

    @Test
    void postMemberTest() throws Exception {
        // given
        MemberDto.Post post = new MemberDto.Post("hgd@gmail.com",
                "홍길동", "010-1234-5678");

        //MemberMapper를 이용해 post(MemberDto.Post 타입) 변수를 Member 객체로 변환하고 있다
        //MemberMapper를 사용하지 않고 new Member() 와 같이 Member 객체를 생성해도 되지만
        //여기서는 post 변수를 재사용하기 위해 MemberMapper로 변환을 했다
        Member member = mapper.memberPostToMember(post);

        //실제 MemberService의 createMember()에서 회원 정보 등록 시, Stamp 정보도 등록되며,
        //createMember() 의 리턴 값(Member 객체)에 Stamp 정보가 포함된다
        //MockMemberService(가칭)의 createMember()에서도 리턴 값으로 Stamp 정보가 포함된 Member 객체를
        //리턴하도록 (4)와 같이 Stamp 객체를 추가해  준다
        member.setStamp(new Stamp());
        //Stamp 객체를 추가해주지 않으면 MemberResponseDto 클래스 객체가 JSON으로 변환되는 과정 중에
        //Stamp에 대한 정보가 없다는 예외가 발생한다


        //Mockito에서 지원하는 Stubbing 메서드이다
        given(memberService.createMember(Mockito.any(Member.class)))
        /*given()은 Mock 객체가 특정 값을 리턴하는 동작을 지정하는데 사용한다
          Mockito에서 지원하는 when()과 동일한 기능을 한다
          Mock 객체인 memberService 객체로 createMember() 메서드를 호출하도록 정의 하였다*/
        /*createMember()의 파라미터인 Mockito.any(Member.class) 는 Mockito에서 지원하는
          변수 타입 중 하나이다
          MockMemberService(가칭)가 아닌 실제 MemberService 클래스에서 createMember()의
          파라미터 타입이 Member이므로 Mockito.any()에 Member.class를 타입으로 지정해 준다*/

        //.willReturn(member)은 MockMemberService(가칭)의 createMember() 메서드가 리턴할
        //Stub 데이터이다
               .willReturn(member);

        String content = gson.toJson(post);

        // when
        ResultActions actions =
                mockMvc.perform(
                        post("/v11/members")
                                .accept(MediaType.APPLICATION_JSON)
                                .contentType(MediaType.APPLICATION_JSON)
                                .content(content)
                );

        // then
        MvcResult result = actions
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.data.email").value(post.getEmail()))
                .andExpect(jsonPath("$.data.name").value(post.getName()))
                .andExpect(jsonPath("$.data.phone").value(post.getPhone()))
                .andReturn();

//        System.out.println(result.getResponse().getContentAsString());
    }
}
  • 실행하면 아래와 같은 결과가 출력된다

  • 실행 결과에서 MockMemberService(가칭)의 createMember() 메서드가 호출되므로, 데이터 액세스 계층쪽의 로직은 실행이 되지 않는다
  • MockMemberService(가칭) 클래스는 테스트하고자 하는 Controller의 테스트에 집중할 수 있도록 다른 계층과의 연동을 끊어주는 역할을 한다
  • MemberService의 클래스 쪽의 createMember()가 호출되지 않고, Mockito가 생성한 MockMemberService(가칭)의 createMember()가 호출되는지 확인해 본다
    - MemberService의 createMember() 메서드 내에 디버깅 용 breakpoint를 추가해서 MemberControllerMockTest 클래스의 실행이 breakpoint에서 멈추는지 확인해 본다
    -  breakpoint 확인을 위한 실행은 Debug 실행으로 해야한다
    - 실행이 멈추지 않고 정상적으로 실행이 된다면 MockMemberService(가칭)쪽의 로직이 실행된다고 유추해 볼 수 있다
  • MemberController의 postMember() 핸들러 메서드 내에서  아래의 코드에 breakpoint를 추가해 본다
 @PostMapping
    public ResponseEntity postMember(@Valid @RequestBody MemberDto.Post requestBody) {
        Member member = mapper.memberPostToMember(requestBody);
        member.setStamp(new Stamp()); // homework solution 추가

  여기에 breakpoint ->     Member createdMember = memberService.createMember(member);

        return new ResponseEntity<>(
                new SingleResponseDto<>(mapper.memberToMemberResponse(createdMember)),
                HttpStatus.CREATED);
    }
  • 디버그 모드에서 MemberControllerMockTest 클래스를 실행해 본다

  • 위와 같이 breakpoint에서 실행이 일시 중지된다
    - IntelliJ IDE의 Debug 창을 확인해 보면 memberService 객체가 Mockito의 Mock 객체인 것을 확인할 수 있다
    - Mockito를 이용하면 의존하는 다른 메서드 호출이나 외부 서비스의 호출을 단절 시켜 원하는 테스트의 범위를 최대한 좁힐 수 있다

▶ IntelliJ debug : https://www.jetbrains.com/help/idea/debug-tool-window.html

 

Debug tool window | IntelliJ IDEA

 

www.jetbrains.com

 

 2) MemberService에 Mockito 적용

  • Mockito를 통해서 DB에서 회원 정보를 조회하지 않고 Member 객체를 제공할 수 있다는 것을 확인해 본다
package com.codestates.slice.mock;

import com.codestates.exception.BusinessLogicException;
import com.codestates.member.entity.Member;
import com.codestates.member.repository.MemberRepository;
import com.codestates.member.service.MemberService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.Optional;

import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.BDDMockito.given;

//Spring을 사용하지 않고, Junit에서 Mockito의 기능을 사용하기 위해서는 
  @ExtendWith(MockitoExtension.class)를 추가해야 한다
@ExtendWith(MockitoExtension.class)
public class MemberServiceMockTest {

    //@Mock 애너테이션을 추가하면 해당 필드의 객체를 Mock 객체로 생성한다
    @Mock
    private MemberRepository memberRepository;

    //@InjectMocks 애너테이션을 추가한 필드에 MemberRepository Mock 객체를 주입해 준다
    //memberService 객체는 주입 받은 memberRepository Mock 객체를 포함하고 있다
    @InjectMocks
    private MemberService memberService;

    @Test
    public void createMemberTest() {
        // given
        Member member = new Member("hgd@gmail.com", "홍길동", "010-1111-1111");

        //memberRepository Mock 객체로 Stubbing을 하고 있다
        //memberRepository.findByEmail(member.getEmail())의 리턴 값으로 Optional.of(member)를
          지정했기 때문에 테스트 케이스를 실행하면 결과는 “passed”이다
        //Optional.of(member) 의 member 객체에 포함된 이메일 주소가
          memberRepository.findByEmail(member.getEmail()) 에서 파라미터로 전달한 이메일 주소와
          동일하기 때문에 검증 결과가 “passed”이다
        given(memberRepository.findByEmail(member.getEmail()))
                .willReturn(Optional.of(member)); // (5)

        // when / then (6)
        assertThrows(BusinessLogicException.class, () -> memberService.createMember(member));
    }
}

 

 

 

※ 참조 링크

▶ mockito : https://site.mockito.org/

 

Mockito framework site

Intro Why How More Who Links Training Why drink it? Mockito is a mocking framework that tastes really good. It lets you write beautiful tests with a clean & simple API. Mockito doesn’t give you hangover because the tests are very readable and they produc

site.mockito.org

 https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html

 

Mockito - mockito-core 4.6.1 javadoc

Latest version of org.mockito:mockito-core https://javadoc.io/doc/org.mockito/mockito-core Current version 4.6.1 https://javadoc.io/doc/org.mockito/mockito-core/4.6.1 package-list path (used for javadoc generation -link option) https://javadoc.io/doc/org.m

javadoc.io

 

1. DDD(Domain Driven Design)

  • 도메인 위주의 설계 기법을 의미한다

 1) 도메인(Domain)

  • 애플리케이션 개발에서 흔하게 사용하는 도메인이란 용어는 주로 비즈니스적인 어떤 업무 영역과 관련이 있다 
  • 애플리케이션 개발에서 필요한 업무 로직에 대한 지식을 도메인 지식(Domain Knowledge)이라고 한다
  • 도메인 지식이 많을수록 서비스 계층에서 비즈니스 로직으로 구현해야 하는 것들을 퀄리티가 높게 표현할 수 있다

 2) 애그리거트(Aggregate)

  • 비슷한 업무 도메인들의 묶음이다
  • 비슷한 범주의 연관된 업무들을 하나로 묶어 그룹화한 것이다

 3) 애그리거트 루트(Aggregate Root)

  • 하나의 애그리거트를 대표하는 도메인을 말한다
  • 애그리거트 루트(Aggregate Root)의 기본키 정보를 다른 도메인들이 외래키 형태로 가지고 있다

 4) 애그리거트 간의 관계

  • 하나의 애그리거트 루트는 N개의 애거리거트 루트와 관계를 가질 수 있다
  • N개의 도메인은 하나의 애거리거트 루트와 관계를 가질 수 있다
  • 애거리거트 간의 관계를 1:N , N:1 , 1:1로 구분할 수 있다

※ 데이터베이스 테이블 간의 관계는 외래키를 통해 맺어지지만

    클래스끼리 관계는 객체의 참조를 통해 맺어진다

 

※ 참조 링크

 https://martinfowler.com/bliki/DDD_Aggregate.html

 

bliki: DDD_Aggregate

A pattern from Domain-Driven Design describing a cluster of domain objects that can be treated as a single unit for persistant storage and transactions.

martinfowler.com

https://www.alibabacloud.com/blog/an-in-depth-understanding-of-aggregation-in-domain-driven-design_598034

 

An In-Depth Understanding of Aggregation in Domain-Driven Design

This article discusses the definition, value, and practices of aggregation in domain-driven design (DDD)

www.alibabacloud.com

 

1. Hello World 샘플 코드 구현

  • 클라이언트 쪽에서 “Hello, World” 문자열 데이터를 Request Body로 전송한다
  • Spring Data JDBC를 이용해서 전송받은  “Hello, World” 문자열을 H2 데이터베이스에 저장한다

 1) Package 생성

  • 기존 프로젝트 파일의 java > com.codestates > hello_world 패키지를 만든다
  • 아래의 클래스 또는 인터페이스를 생성한다

- MessageDto(DTO 클래스) - PostDto / ResponseDto

package com.codestates.hello_world;

import lombok.Getter;

import javax.validation.constraints.NotBlank;

//클라이언트가 Request Body로 전달하는 “Hello, World” 문자열을 바인딩하는 DTO 클래스
@Getter
public class MessagePostDto {
    @NotBlank
    private String message;
}
package com.codestates.hello_world;

import lombok.Getter;
import lombok.Setter;

//Response에 사용할 Dto
@Getter
@Setter
public class MessageResponseDto {
    private long messageId;
    private String message;
}

 

- MessageController

package com.codestates.hello_world.messageController;

import com.codestates.hello_world.messageMapper.MessageMapper;
import com.codestates.hello_world.messageService.MessageService;
import com.codestates.hello_world.message.Message;
import com.codestates.hello_world.messageDto.MessagePostDto;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;

//문자열을 전달받는 Controller
@RequestMapping("/v1/message")
@RestController
public class MessageController {
    private final MessageService messageService;
    private final MessageMapper mapper;

    public MessageController(MessageService messageService,
                             MessageMapper mapper){
        this.messageService = messageService;
        this.mapper = mapper;
    }

    @PostMapping
    public ResponseEntity postMessage(
            @Valid @RequestBody MessagePostDto messagePostDto){
        Message message =
                messageService.createMessage(mapper.messageDtoToMessage(messagePostDto));
        return ResponseEntity.ok(mapper.messageToMessageResponseDto(message));
        }
}

 

- MessageMapper

package com.codestates.hello_world.messageMapper;

import com.codestates.hello_world.message.Message;
import com.codestates.hello_world.messageDto.MessagePostDto;
import com.codestates.hello_world.messageDto.MessageResponseDto;
import org.mapstruct.Mapper;

//DTO 클래스와 엔티티(Entity) 클래스를 매핑해주는 Mapper 인터페이스
@Mapper(componentModel = "spring")
public interface MessageMapper {

    Message messageDtoToMessage(MessagePostDto messagePostDto);
    MessageResponseDto messageToMessageResponseDto(Message message);

}

 

- MessageService

package com.codestates.hello_world.messageService;

import com.codestates.hello_world.message.Message;
import com.codestates.hello_world.messageRepository.MessageRepository;
import org.springframework.stereotype.Service;

//MessageRepository 인터페이스를 DI를 통해 주입받는다
//데이터베이스에 데이터를 저장하고 데이터베이스에 저장된 데이터를 다시 리턴
@Service
public class MessageService {
    private MessageRepository messageRepositoty;
    public MessageService(MessageRepository messageRepositoty){
        this.messageRepositoty = messageRepositoty;
    }
    public Message createMessage(Message message){
        return messageRepositoty.save(message);
    }
}

  → MessageRepository가 상속받은 CrudRepository에 save() 메서드가 정의되어 있다

  → 개발자가 데이터의 생성, 조회, 수정, 삭제 작업을 위한 별도의 코드를 구현하지 않아도 CrudRepository가 작업을 대신해주는 역할을 한다


- Message(엔티티 클래스)

package com.codestates.hello_world.message;

import lombok.Getter;
import lombok.Setter;
import org.springframework.data.annotation.Id;

//Message 클래스명은 데이터베이스의 테이블명에 해당한다
//@Id 애너테이션을 추가한 멤버 변수는 해당 엔티티의 고유 식별자 역할을 한다
//식별자는 데이터베이스의 Primary key로 지정한 컬럼에 해당한다
@Getter
@Setter
public class Message {
    @Id
    private long messageId;
    private String message;
}


- MessageRepository

package com.codestates.hello_world;

import org.apache.logging.log4j.message.Message;
import org.springframework.data.repository.CrudRepository;

//데이터베이스와의 연동을 담당하는 Repository
public interface MessageRepository extends CrudRepository<Message, Long>{

}

  →  CrudRepository라는 인터페이스를 상속하고 있고, CrudRepository의 제너릭 타입이 <Message, Long>으로 선언되어 있다   

  → CrudRepository 는 데이터베이스에 CRUD(데이터 생성, 조회, 수정, 삭제) 작업을 진행하기 위해 Spring에서 지원해주는 인터페이스이다

  → CrudRepository<Message, Long> 와 같이 제너릭 타입을 지정해줌으로써 Message 엔티티 클래스 객체에 담긴 데이터를 데이터베이스 테이블에 생성 또는 수정하거나 데이터베이스에서 조회한 데이터를 Message 엔티티 클래스로 변환할 수 있다

  → <Message, Long>에서 LongMessage 엔티티 클래스의 멤버 변수 중에 식별자를 의미하는 @Id 라는 애너테이션이 붙어있는 멤버 변수의 데이터 타입이다

  ※ 짧은 문장이고 MessageRepository 인터페이스 내부에 아무런 코드도 없으나, MessageRepository 인터페이스를 서비스 계층에서 DI를 통해 주입받은 후 데이터베이스 작업을 위해 사용하게 된다

 

프로젝트를 실행시키고 error 발생 시 변수들의 경로에 유의하여 수정하도록 한다!!!

 

2) 프로젝트 실행

  • 실행 전에 property 설정을 추가한다
spring:
  h2:
    console:
      enabled: true
      path: /h2     # (1) Context path
  datasource:
    url: jdbc:h2:mem:test     # (2) JDBC URL
  sql:
    init:
      schema-locations: classpath*:db/h2/schema.sql      # 추가할 부분
      # 테이블 생성을 위한 SQL 문이 추가된 ‘schema’라는 파일명으로 .sql 파일의 경로를 지정
  • 추가 후 resource > db.h2 > schema.sql 파일을 보면 아래와 같이 테이블이 추가되어 있다
CREATE TABLE IF NOT EXISTS MESSAGE (
    MESSAGE_ID bigint NOT NULL AUTO_INCREMENT,
    MESSAGE varchar(100) NOT NULL,
    PRIMARY KEY (MESSAGE_ID)
);
  • postman에서 post : http://localhost:8080/v1/message 입력 후 전송하면 아래와 같이 응답한다

  • 요청 전송 시 Request Body에 messageId 값은 포함하지 않았지만 Response Body에는 messageId가 포함되어 출력된다
  • MESSAGE 테이블의 식별자(Primary key)인 ‘message_id’ 컬럼에 AUTO_INCREMENT 설정이 되어 있으므로 ‘message_id’ 컬럼에 값을 입력하지 않더라도 데이터가 저장이 될 때 마다 자동으로 포함이 된다
  • chrome 화면으로 가서 다시 출력한 후 'SELECT *FROM MESSAGE'를 입력하고 RUN SELECT를 실행한다
    - 1개의 데이터 (ID:1 , MESSAGE:Hello World)가 입력되어 있음을 확인할 수 있다

 

2. JDBC DATA 구현 순서

 

1. SQL 중심 기술

  • 애플리케이션에서 데이터베이스에 접근하기 위해 SQL 쿼리문을 애플리케이션 내부에 직접적으로 작성하는 것이 중심이 되는 기술이다
<select id="findMember" resultType="Member">
  SELECT * FROM MEMBER WHERE member_id = #{memberId}
</select>
  • mybatisSpring JDBC는 대표적인 SQL 중심 기술이다
  • mybatis은 SQL Mapper라는 설정 파일이 있고 SQL Mapper에서 SQL 쿼리문을 직접적으로 작성한다
    -
    작성된 SQL 쿼리문을 기반으로 데이터베이스의 특정 테이블에서 데이터를 조회하여 Java 객체로 변환해 준다
  • Spring JDBC의 경우에도 Java 코드에 SQL 쿼리문이 직접적으로 포함이 되어 있다
Member member = this.jdbcTemplate.queryForObject(
				"select * from member where member_id=?", 1, Member.class);

 

2. Object 중심 기술

  • 데이터를 SQL 쿼리문 중심으로 보는 것이 아니라 모든 데이터를 객체(Object) 관점으로 바라보는 기술이다
  • 애플리케이션 내부에서 Java 객체(Object)를 SQL 쿼리문으로 자동 변환 후에 데이터베이스의 테이블에 접근한다
  • 객체(Object) 중심의 데이터 액세스 기술을 ORM(Object-Relational Mapping)이라고 한다
  • Java에는 대표적인 ORM 기술로 JPA(Java Persistence API)가 있다

 

3. Spring Data JDBC

  • Spring Data JDBC를 사용하기 위하여 Spring Boot Starter를 추가한다
dependencies {
	...
	...
	implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
	runtimeOnly 'com.h2database:h2'
}
  • 데이터베이스에서 데이터를 관리할 것이므로 개발 환경에서 손쉽게 사용할 수 있는 인메모리(In-memory) DBH2를 의존 라이브러리 설정에 추가한다

※ In-memory DB

  • 메모리 안에 데이터를 저장하는 데이터베이스이다
  • 애플리케이션이 실행되는 동안에만 데이터를 저장하고 있다
  • 애플리케이션 실행을 중지했다가 다시 실행시키면 인메모리(In-memory) DB에 있던 데이터는 삭제된다
  • 로컬 개발 환경에서는 테스트를 진행하기 위하여 인메모리(In-memory) DB를 주로 사용한다

※ H2 활성화 설정

  • Spring Boot Initializr를 통해 샘플 프로젝트를 생성하면 기본적으로 ‘src/main/resources’ 디렉토리 하단에 application.properties 라는 비어 있는 파일이 있다
  • application.properties 또는 application.yml 파일을 통해 Spring에서 사용하는 다양한 설정 정보들을 입력할 수 있다
  • 웹 브라우저 H2 콘솔에서 H2 DB에 접속한 후, 데이터베이스를 관리할 수 있도록 설정한다
spring:
  h2:
    console:
      enabled: true
  • 프로젝트 실행 시 H2 console available at '/h2'. Database available at 'jdbc:h2:mem:test'와 같은 문구가 출력되면 정상적으로 연동되었음을 의미한다
  • 애플리케이션 연결
    - https://github.com/codestates-seb/be-reference-spring-data-jdbc 연결하여 fork 한 후 실행한다
    - chrome 브라우져에 http://local8080/h2 입력 후 출력한다
    - 원래는  http://local8080/h2-console 입력해야 하지만, 미리 URL을 추가 설정했으므로 h2까지만 입력한다

     - 프로젝트 실행에서 출력된 로그에서 주소를 확인한 후 'jdbc:h2:mem:test'을 JDBC URL에 입력한다

     - 정상적으로 H2 브라우저에 연결되면 아래와 같이 출력된다

  • H2 DB는 애플리케이션을 재시작 할때 마다 애플리케이션 로그에 출력되는 JDBC URL이 매번 랜덤하게 변경된다
  • application.yml 파일에 H2에 대한 추가 설정을 통해 JDBC URL을 다시 입력하는 불편을 해소한다
    - 예제 Git 파일은 사전에 입력되어 있으므로 추가 변경이 필요하지 않다
spring:
  h2:
    console:
      enabled: true
      path: /h2    
  datasource:
    url: jdbc:h2:mem:test
  • Spring Boot에서는 appliation.properties 또는 application.yml 파일을 통해 설정 정보를 추가할 수 있다

※ 참조 링크

▶ 인메모리(In-memory) DB

 : https://www.h2database.com/html/main.html

 

H2 Database Engine

H2 Database Engine Welcome to H2, the Java SQL database. The main features of H2 are: Very fast, open source, JDBC API Embedded and server modes; in-memory databases Browser based Console application Small footprint: around 2.5 MB jar file size     Supp

www.h2database.com

 : https://ko.wikipedia.org/wiki/%EC%9D%B8%EB%A9%94%EB%AA%A8%EB%A6%AC_%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4

 

인메모리 데이터베이스 - 위키백과, 우리 모두의 백과사전

 

ko.wikipedia.org

 

H2 Console : http://www.h2database.com/html/quickstart.html

 

Quickstart

Quickstart Embedding H2 in an Application The H2 Console Application Embedding H2 in an Application This database can be used in embedded mode, or in server mode. To use it in embedded mode, you need to: Add the h2*.jar to the classpath (H2 does not have a

www.h2database.com

 

Property 설정 : https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html

 

Common Application Properties

 

docs.spring.io

 

1. JDBC(Java Database Connectivity)

  • Java 기반 애플리케이션의 코드 레벨에서 사용하는 데이터를 데이터베이스에 저장 및 업데이트 하거나 반대로 데이터베이스에 저장된 데이터를 Java 코드 레벨에서 사용할 수 있도록 해주는 Java에서 제공하는 표준 API이다
  • Java 초창기(JDK 1.1) 버전부터 제공되는 데이터베이스 액세스 기능을 위한 API이다
  • JDBC API를 사용해서 Oracle, MS SQL, MySQL 등의 데이터베이스와 연동할 수 있다

Java Application → JDBC API → JDBC Driver → Database

  • JDBC Driver
    - 데이터베이스와의 통신을 담당하는 인터페이스이다
    - Oracle이나 MS SQL, MySQL 같은 다양한 벤더에서는 해당 벤더에 맞는 JDBC 드라이버를 구현해서 제공을 한다
    - JDBC 드라이버의 구현체를 이용해서 특정 벤더의 데이터베이스에 액세스 할 수 있다

2. Connection Pool

  • 데이터베이스 Connection을 보관하고 애플리케이션이 필요할 때 Connection을 제공해주는 Connection 관리자이다
  • 애플리케이션 로딩 시점에 Connection 객체를 미리 생성한다
  • 생성한 Connection 객체를 Connection Pool에 저장한다
  • 애플리케이션이 데이터베이스에 연결이 필요한 경우 미리 생성 보관 중인 Connection 객체를 사용한다
  • Spring Boot 2.0 이전 버전에는 Apache 재단의 오픈 소스인 Apache Commons DBCP(Database Connection Pool, DBCP)를 주로 사용했다
  • Spring Boot 2.0 부터는 HikariCP기본 DBCP로 채택하고 있다

 

※ 참조 링크

▶ JDBC : https://docs.oracle.com/javase/8/docs/technotes/guides/jdbc/

 

Java JDBC API

Java JDBC API The Java Database Connectivity (JDBC) API provides universal data access from the Java programming language. Using the JDBC API, you can access virtually any data source, from relational databases to spreadsheets and flat files. JDBC technolo

docs.oracle.com

https://docs.oracle.com/javase/tutorial/jdbc/basics/index.html

 

Lesson: JDBC Basics (The Java™ Tutorials > JDBC Database Access)

The Java Tutorials have been written for JDK 8. Examples and practices described in this page don't take advantage of improvements introduced in later releases and might use technology no longer available. See Java Language Changes for a summary of updated

docs.oracle.com

https://ko.wikipedia.org/wiki/JDBC

 

JDBC - 위키백과, 우리 모두의 백과사전

 

ko.wikipedia.org

https://techxyte.com/tutorials/hibernate/pros-and-cons-of-jdbc.php

 

Premium Bootstrap 5 HTML, Angular 11, VueJS, React & Laravel Admin Dashboard Themes

Metronic - #1 Selling Premium Bootstrap Admin Theme of All Time. Built with Twitter Bootstrap 5 HTML, Angular 11, VueJS, React and Laravel. Trusted By Tens of Thousands Users.

keenthemes.com

HikariCP : https://github.com/brettwooldridge/HikariCP

 

GitHub - brettwooldridge/HikariCP: 光 HikariCP・A solid, high-performance, JDBC connection pool at last.

光 HikariCP・A solid, high-performance, JDBC connection pool at last. - GitHub - brettwooldridge/HikariCP: 光 HikariCP・A solid, high-performance, JDBC connection pool at last.

github.com

 

1. @ExceptionHandler를 이용한 예외 처리

  • 위와 같이 postman의 GET에서 요청을 보냈을 경우 데이터를 찾지 못하여 에러가 발생하는 경우가 있다
  • 클라이언트가 전달 받는 Response Body는 애플리케이션에서 예외(Exception)가 발생했을 때, 내부적으로 Spring에서 전송해주는 에러 응답 메시지 중의 하나이다 (위에서는 '404 Not Found' 메시지를 전송하고 있다)

 1) Spring에서의 예외 처리

  • 애플리케이션의 문제가 발생하였을 경우 알려주어 처리를 유도한다
  • 유효성 검증에 실패하였을 경우 예외로 간주하고 처리를 유도한다
  • MemberController(v6)에 @ExceptionHandler 애너테이션을 이용해서 예외를 처리하도록 handleException() 메서드를 추가한다
  • handleException() 메서드에서 유효성 검사 실패에 대한 에러 메시지를 구체적으로 전송해 주는 역활을 한다
package com.codestates.member.controller;

import com.codestates.member.dto.MemberPatchDto;
import com.codestates.member.dto.MemberPostDto;
import com.codestates.member.dto.MemberResponseDto;
import com.codestates.member.entity.Member;
import com.codestates.member.mapper.MemberMapper;
import com.codestates.member.service.MemberService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;

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


/**
 * - DI 적용
 * - Mapstruct Mapper 적용
 */

@RestController
@RequestMapping("/v6/members")
@Validated
public class MemberController {
    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 = mapper.memberPostDtoToMember(memberDto);

        Member response = memberService.createMember(member);

        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);

        Member response =
                memberService.updateMember(mapper.memberPatchDtoToMember(memberPatchDto));

        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);
        return new ResponseEntity<>(mapper.memberToMemberResponseDto(response), HttpStatus.OK);
    }

    @GetMapping
    public ResponseEntity getMembers() {
        List<Member> members = memberService.findMembers();
        List<MemberResponseDto> response = mapper.membersToMemberResponseDtos(members);
        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);
    }

    @ExceptionHandler
    public ResponseEntity handleException(MethodArgumentNotValidException e) {
        //MethodArgumentNotValidException 객체에서
        //getBindingResult().getFieldErrors()를 통해 발생한 에러 정보를 확인할 수 있다
        final List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
        //getBindingResult().getFieldErrors()를 통해 확인한 에러 정보를
        //ResponseEntity를 통해 Response Body로 전달한다
        return new ResponseEntity<>(fieldErrors, HttpStatus.BAD_REQUEST);
    }
}
  • 위의 코드 흐름은 다음과 같이 진행된다
  1. 클라이언트 쪽에서 회원 등록을 위해 MemberController의 postMember() 핸들러 메서드에 요청을 전송한다
  2. RequestBody에 유효하지 않은 요청 데이터가 포함되어 있어 유효성 검증에 실패하고, MethodArgumentNotValidException이 발생한다
  3. MemberController에는 @ExceptionHandler 애너테이션이 추가된 예외 처리 메서드인 handleException()이 있기 때문에 유효성 검증 과정에서 내부적으로 던져진 MethodArgumentNotValidExceptionhandleException() 메서드가 전달 받는다
  4. postman으로 정확하지 않은 data를 json 형식으로 전송하면 아래와 같이 오류가 발생하고 상세한 오류 내역을 Response Body로 전송해 준다

  • 지금까지는 @ExceptionHandler 애너테이션과 handleException() 메서드를 통해 유효성 검사 결과를 그대로 전송받았다
  • 전송받은 데이터 중에는 불필요한 내용도 포함되어 있다

 

  • 필요한 내용들만 선택적으로 받을 수 있도록 코드를 수정한다
  • 별도의 ErrorResponse 클래스를 생성한다
    - DTO 클래스의 유효성 검증 실패 시, 실패한 필드(멤버 변수)에 대한 Error 정보만 담아서 응답으로 전송한다
  • MemberController(v6)에 @ExceptionHandler 애너테이션 적용 코드를 수정한다
package com.codestates.member.controller;

import lombok.AllArgsConstructor;
import lombok.Getter;

import java.util.List;

@Getter
@AllArgsConstructor

public class ErrorResponse {

    //한 개 이상의 유효성 검증에 실패한 필드의 에러 정보를 담기 위해서 List 객체를 이용한다
    private List<FieldError> fieldErrors;

    @Getter
    @AllArgsConstructor
    
    //한개의 필드 에러 정보는
    //FieldError 라는 별도의 static class를 ErrorResponse 클래스의 멤버 클래스로 정의한다
    public static class FieldError{
        private String field;
        private Object rejectedValue;
        private String reason;
    }
}
  • FieldError 클래스는 ErrorResponse 클래스 내부에 정의되어 있으나 내부(Inner) 클래스라고 부르기보다는 ErrorResponse 클래스의 static 멤버 클래스라고 부르는 것이 적절하다
  • 클래스가 멤버 변수와 멤버 메서드를 포함하듯이 static 멤버 클래스를 포함할 수 있다

 

  • MemberController(v6)에 @ExceptionHandler 애너테이션 적용 코드를 수정한다
@ExceptionHandler
    public ResponseEntity handleException(MethodArgumentNotValidException e) {
        //MethodArgumentNotValidException 객체에서
        //getBindingResult().getFieldErrors()를 통해 발생한 에러 정보를 확인할 수 있다
        final List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();

        //필요한 정보들만 선택하여 ErrorResponse.FieldError 클래스에서 List로 변환한다
        List<ErrorResponse.FieldError> errors =
                fieldErrors.stream()
                        .map(error->new ErrorResponse.FieldError(
                                error.getField(),
                                error.getRejectedValue(),
                                error.getDefaultMessage()
                        ))
                        .collect(Collectors.toList());

        //getBindingResult().getFieldErrors()를 통해 확인한 에러 정보를 List<ErrorResponse.FieldError>에 넣은 후
        //ResponseEntity를 통해 Response Body로 전달한다
        return new ResponseEntity<>(new ErrorResponse(errors), HttpStatus.BAD_REQUEST);
    }

 

  • post를 전송하면 아래와 같이 필요한 데이터만 응답받을 수 있다
    - email 주소가 오류인 경우

        - email 와 name 이 동시에 오류인 경우

 

2. @ExceptionHandler 의 단점

  • 각각의 Controller 클래스에서 @ExceptionHandler 애너테이션을 사용하여 Request Body에 대한 유효성 검증 실패에 대한 에러 처리를 해야되므로 각 Controller 클래스마다 코드 중복이 발생한다
  • Controller에서 처리해야 되는 예외(Exception)가 유효성 검증 실패에 대한 예외(MethodArgumentNotValidException)만 있는것이 아니기 때문에 하나의 Controller 클래스 내에서 @ExceptionHandler를 추가한 에러 처리 핸들러 메서드가 늘어날 수 있다

 

※ 참조 링크

Exceptions : https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-exceptionhandler

 

Web on Servlet Stack

Spring Web MVC is the original web framework built on the Servlet API and has been included in the Spring Framework from the very beginning. The formal name, “Spring Web MVC,” comes from the name of its source module (spring-webmvc), but it is more com

docs.spring.io

BindingResult 클래스 : https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/validation/BindingResult.html

 

BindingResult (Spring Framework 5.3.21 API)

Record the given value for the specified field. To be used when a target object cannot be constructed, making the original field values available through Errors.getFieldValue(java.lang.String). In case of a registered error, the rejected value will be expo

docs.spring.io

 ConstraintViolation 인터페이스와 구현 클래스

 : https://docs.oracle.com/javaee/7/api/javax/validation/ConstraintViolation.html

 

ConstraintViolation (Java(TM) EE 7 Specification APIs)

Object getLeafBean() Returns: the bean instance the constraint is applied on if it is a bean constraint the bean instance hosting the property the constraint is applied on if it is a property constraint null when the ConstraintViolation is returned after c

docs.oracle.com

 : https://docs.jboss.org/hibernate/validator/5.3/api/org/hibernate/validator/internal/engine/ConstraintViolationImpl.html

 

ConstraintViolationImpl (Hibernate Validator 5.3.6.Final)

equals public boolean equals(Object o) IMPORTANT - some behaviour of Validator depends on the correct implementation of this equals method! (HF) expressionVariables and dynamicPayload are not taken into account for equality. These variables solely enric

docs.jboss.org

 

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