개발새발/Spring

220227_스프링 입문_스프링의 기능을 활용해보자_Spring Boot Custom Validation_패스트캠퍼스 챌린지 35일차

막동이아빠 2022. 2. 27. 22:48

220227_스프링 입문_스프링의 기능을 활용해보자_Spring Boot Custom Validation_패스트캠퍼스 챌린지 35일차

<2022년 02월 27일 _ 패스트캠퍼스 챌린지 35일차>

[스프링 입문_스프링의 기능을 활용해보자_Spring Boot Custom Validation]

1. Validation

 : 프로그래밍에 있어서 가장 필요한 부분

 : Java에서는 null값에 대해 접근할 때, null pointer exception이 발생하므로

  , 이 부분에 대한 방지를 위해, 미리 검증 하는 과정

ex)

public void run(String account, Spring pw, int age){
	if(account == null || pw == null){
    	return;
    }
    
    if(age == 0){
    	return;
    }
    
    //정상 logic
}

 (1) 검증해야 할 값이 많은 경우 코드 길이가 길어짐

 (2) 구현에 따라 다르지만, 일반적으로 Service Logic과의 분리 필요

 (3) 흩어져 있는 경우, 어디에서 검증을 하는지 알기 어렵고, 재사용의 한계

 (4) 구현에 따라 다르지만, 검증 Logic이 변경 될 경우, 테스트 코드등의 참조 클래스에서 Logic이 변경되어야 할 수 있음

@Size 문자 길이 측정 Int Type 불가
@NotNull null 불가  
@NotEmpty null, "" 불가  
@NotBlank null, "", " " 불가  
@Past 과거 날짜  
@PastOrPresent 오늘이거나 과거 날짜  
@Future 미래 날짜  
@FutureOrPresent 오늘이거나 미래 날짜  
@Pattern 정규식 적용  
@Max 최댓값  
@Min 최솟값  
@AssertTrue / False 별도 Logic 적용  
@Valid 해당 object validation 실행  

 

2. 실습 준비

 (1) File > New > Project > Spring Initializr

 (2) Language: Java, Type: Gradle, Artifact: validation, Java: 11

 (3) Web > Spring Web 선택

 

3. Validation 세팅

 (1) gradle dependencies

   : bundle.gradle 선택

   : implementation("org.springframework.boot:spring-boot-starter-validation")

dependencies에 다음과 같이 한줄 추가한다

implementation 'org.springframework.boot:spring-boot-starter-validation'

 

 (2) bean validation spec : 어노테이션의 릴리즈 내용들을 볼 수 있다

   https://beanvalidation.org/2.0-jsr380/

 

Jakarta Bean Validation - Bean Validation 2.0 (JSR 380)

Bean Validation 2.0 focused on the following topics: support for validating container elements by annotating type arguments of parameterized types e.g. List<@Positive Integer> positiveNumbers. This also includes: more flexible cascaded validation of contai

beanvalidation.org

 

 (3) 핸드폰번호 정규식

   "^\\d{2,3}-\\d{3,4}-\\d{4}$"

 

4. 실습 시작

 (1) controller 패키지 추가 > ApiController 클래스 추가

package com.example.validation.controller;

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

@RestController
@RequestMapping("/api")
public class ApiController {

    @PostMapping("/user")
    public Object user(){
        return null;
    }

}

 (2) dto 패키지 추가 > User 클래스 추가

package com.example.validation.dto;

public class User {

    private String name;

    private int age;

    private String email;

    private String phoneNumber;

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getEmail() {
        return email;
    }

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

    public String getPhoneNumber() {
        return phoneNumber;
    }

    public void setPhoneNumber(String phoneNumber) {
        this.phoneNumber = phoneNumber;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", email='" + email + '\'' +
                ", phoneNumber='" + phoneNumber + '\'' +
                '}';
    }
}

 

 (3) ApiController 에서 User를 받아봅시다

package com.example.validation.controller;

import com.example.validation.dto.User;
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;

@RestController
@RequestMapping("/api")
public class ApiController {

    @PostMapping("/user")
    public User user(@RequestBody User user){
        System.out.println(user);
        return user;
    }

}

 

 (4) Talend API 에서 테스트

  : (POST) http://localhost:8080/api/user

  : body:

{
  "name" : "홍길동",
  "age" : 10,
  "email" : "abcdefg",
  "phoneNumber" : "01011112222"
}

결과:

User{name='홍길동', age=10, email='abcdefg', phoneNumber='01011112222'}

 정상적으로 200은 응답이 떨어졌지만, 사용자가 원하는 email, phoneNumber 양식이 아닙니다.

 이 때, 옛날 개발 방식은 어떻게 할까요?

 

 (5) 옛 validation 방식(실무에선 여전히 많이 쓰이고 있을 수 있음)

package com.example.validation.controller;

import com.example.validation.dto.User;
import org.springframework.http.HttpStatus;
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;

@RestController
@RequestMapping("/api")
public class ApiController {

    @PostMapping("/user")
    public ResponseEntity user(@RequestBody User user){
        System.out.println(user);

        if(user.getAge() >= 90){
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(user);
        }
        return ResponseEntity.ok(user);
    }

}

 테스트:

  : (POST) http://localhost:8080/api/user

  : body:

{
  "name" : "홍길동",
  "age" : 100,
  "email" : "abcdefg",
  "phoneNumber" : "01011112222"
}

 결과:

User{name='홍길동', age=100, email='abcdefg', phoneNumber='01011112222'}

 당장은 하나이지만, 이런 것들이 굉장히 많이 늘어난다면? 굉장히 복잡해질것입니다. 그럴때 사용하는 바로바로!!

 

 (6) Spring validation 적용

  : User 클래스 수정(@Email 어노테이션 추가)

@Email
private String email;

  : ApiController 클래스 수정(user 메소드 파라미터 앞에 @Valid 어노테이션 추가)

@PostMapping("/user")
public ResponseEntity user(@Valid @RequestBody User user){
    System.out.println(user);

    if(user.getAge() >= 90){
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(user);
    }
    return ResponseEntity.ok(user);
}

 테스트:

  : (POST) http://localhost:8080/api/user

  : body:

{
  "name" : "홍길동",
  "age" : 89,
  "email" : "abcdefg",
  "phoneNumber" : "01011112222"
}

 결과:

2022-03-01 22:25:32.853  WARN 13596 --- [nio-8080-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public org.springframework.http.ResponseEntity com.example.validation.controller.ApiController.user(com.example.validation.dto.User): [Field error in object 'user' on field 'email': rejected value [abcdefg]; codes [Email.user.email,Email.email,Email.java.lang.String,Email]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.email,email]; arguments []; default message [email],[Ljavax.validation.constraints.Pattern$Flag;@725599d1,.*]; default message [올바른 형식의 이메일 주소여야 합니다]] ]

 : 이메일에 대한 양식이 맞지 않음을 체크한다

 테스트:

  : (POST) http://localhost:8080/api/user

  : body:

{
  "name" : "홍길동",
  "age" : 89,
  "email" : "steve@gmail.com",
  "phoneNumber" : "01011112222"
}

 결과:

User{name='홍길동', age=89, email='steve@gmail.com', phoneNumber='01011112222'}

 (7) Spring validation 적용2 : 정규식으로 검증(휴대폰번호)

  : User 클래스 수정 (@Pattern 어노테이션 추가)

@Pattern(regexp = "^\\d{2,3}-\\d{3,4}-\\d{4}$")
private String phoneNumber;

 테스트:

 (POST) http://localhost:8080/api/user

 body:

{
  "name" : "홍길동",
  "age" : 89,
  "email" : "steve@gmail.com",
  "phoneNumber" : "01011112222"
}

결과:

2022-03-01 23:38:39.013  WARN 22348 --- [nio-8080-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public org.springframework.http.ResponseEntity com.example.validation.controller.ApiController.user(com.example.validation.dto.User): [Field error in object 'user' on field 'phoneNumber': rejected value [01011112222]; codes [Pattern.user.phoneNumber,Pattern.phoneNumber,Pattern.java.lang.String,Pattern]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.phoneNumber,phoneNumber]; arguments []; default message [phoneNumber],[Ljavax.validation.constraints.Pattern$Flag;@7e28223c,^\d{2,3}-\d{3,4}-\d{4}$]; default message ["^\d{2,3}-\d{3,4}-\d{4}$"와 일치해야 합니다]] ]

@Pattern 에 맞지 않기때문에 위와 같은 결과가 나타나게 됩니다.

 

테스트:

 (POST) http://localhost:8080/api/user

 body:

{
  "name" : "홍길동",
  "age" : 89,
  "email" : "steve@gmail.com",
  "phoneNumber" : "010-1111-2222"
}

 결과:

User{name='홍길동', age=89, email='steve@gmail.com', phoneNumber='010-1111-2222'}

 

 (8) Spring validation 적용3(BindingResult 사용하기)

   : BindingResult를 사용하면 validation에 대한 값이 BindingResult 파라미터 변수로 들어가게 된다

  ApiController 클래스의 user 메소드 변경

@PostMapping("/user")
public ResponseEntity user(@Valid @RequestBody User user, BindingResult bindingResult){

    if(bindingResult.hasErrors()){
        StringBuilder sb = new StringBuilder();
        bindingResult.getAllErrors().forEach(objectError -> {
            FieldError field = (FieldError) objectError; //어떤 필드에서 에러가 났는지 가져오기
            String message = objectError.getDefaultMessage();

            System.out.println("field : " + field.getField());
            System.out.println(message);
        });
    }

    System.out.println(user);

    return ResponseEntity.ok(user);
}

테스트:

 (POST) http://localhost:8080/api/user

 body:

{
  "name" : "홍길동",
  "age" : 89,
  "email" : "steve@gmail.com",
  "phoneNumber" : "01011112222"
}

결과:

field : phoneNumber
"^\d{2,3}-\d{3,4}-\d{4}$"와 일치해야 합니다

User{name='홍길동', age=89, email='steve@gmail.com', phoneNumber='01011112222'}

 

 에러 메시지를 변경해봅시다. User 클래스의 @Pattern 변경

@Pattern(regexp = "^\\d{2,3}-\\d{3,4}-\\d{4}$", message = "핸드폰 번호의 양식과 맞지 않습니다. 01x-xxx[x]-xxxx")
private String phoneNumber;

테스트:

 (POST) http://localhost:8080/api/user

 body:

{
  "name" : "홍길동",
  "age" : 89,
  "email" : "steve@gmail.com",
  "phoneNumber" : "01011112222"
}

결과:

field : phoneNumber
핸드폰 번호의 양식과 맞지 않습니다. 01x-xxx[x]-xxxx
User{name='홍길동', age=89, email='steve@gmail.com', phoneNumber='01011112222'}

 

 그렇다면, 좀 더 모양을 이쁘게 만들어볼까요

 

  ApiController 클래스의 user 메소드 변경/추가

@PostMapping("/user")
public ResponseEntity user(@Valid @RequestBody User user, BindingResult bindingResult){

    if(bindingResult.hasErrors()){
        StringBuilder sb = new StringBuilder();
        bindingResult.getAllErrors().forEach(objectError -> {
            FieldError field = (FieldError) objectError; //어떤 필드에서 에러가 났는지 가져오기
            String message = objectError.getDefaultMessage();

            System.out.println("field : " + field.getField());
            System.out.println(message);

            sb.append("field : " + field.getField());
            sb.append("message : " + message);
        });

        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(sb.toString());
    }

    System.out.println(user);

    return ResponseEntity.ok(user);
}

테스트:

 (POST) http://localhost:8080/api/user

 body:

{
  "name" : "홍길동",
  "age" : 89,
  "email" : "steve@gmail.com",
  "phoneNumber" : "01011112222"
}

 결과:

field : phoneNumber
핸드폰 번호의 양식과 맞지 않습니다. 01x-xxx[x]-xxxx

테스트:

 (POST) http://localhost:8080/api/user

 body:

{
  "name" : "홍길동",
  "age" : 89,
  "email" : "steve@gmail.com",
  "phoneNumber" : "010-1111-2222"
}

 결과:

User{name='홍길동', age=89, email='steve@gmail.com', phoneNumber='010-1111-2222'}

 

 (9) Spring validation 적용4(@NotBlank, @Min, @Max)

  User 클래스 수정

@NotBlank
private String name;

@Max(value = 90)
private int age;

테스트:

 (POST) http://localhost:8080/api/user

 body:

{
  "name" : "홍길동",
  "age" : 100,
  "email" : "steve@gmail.com",
  "phoneNumber" : "010-1111-2222"
}

 결과:

field : age
90 이하여야 합니다

 

정리:

 - 위와 같이 BindingResult 객체를 통해 새로운 변환 값을 받을 수도 있다.

 - BindingResult가 없다면, 예외처리를 통해 가능하다(추후 강의)

 

35일차 강의 완료~

필기 & 내 화면 스크린샷 저장

 

 

https://bit.ly/37BpXiC

 

패스트캠퍼스 [직장인 실무교육]

프로그래밍, 영상편집, UX/UI, 마케팅, 데이터 분석, 엑셀강의, The RED, 국비지원, 기업교육, 서비스 제공.

fastcampus.co.kr

본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성되었습니다.