[Spring] Spring Boot Validation

WOOK JONG KIM·2022년 10월 22일
0

패캠_java&Spring

목록 보기
43/103
post-thumbnail

Validation

Java의 경우 null값에 대해 접근하려할 때 null Pointer Exception이 발생함

-> 이러한 부분을 방지하기 위해 미리 검증하는 과정 Validation

코드 예시

public void run(String account, String pw, int age){
	if(account == null || pw == null){
    	return
    }
    
    if(age == 0){
    	return
    }
    
    // 정상 Logic -> 위에다가 검증을 하는 것은 바람직하지 않음 
    

사용 이유

  1. 검증해야 할 값이 많은 경우 코드가 길어진다

  2. 구현에 따라서 달라 질 수 있지만 Service Logic과의 분리가 필요 하다

  3. 흩어져 있는 경우 어디에서 검증을 하는지 알기 어려우며, 재사용에 한계

  4. 검증 Logic이 변경되는 경우 테스트 코드 등 참조하는 클래스에서 Logic이 변경되어야 하는 부분 발생


어노테이션 정리

  • grade dependencies에 추가 해야함
    implementation("org.springframework.boot:spring-boot-starter-validation")

  • bean validation spec은 필요 시 찾아보기


Validation 활용 예시

Validation을 하지 않으면 유효하지 않은 값까지 들어옴

또한 생각 없이 그냥 적으면 ApiController코드에다가 무작정 if~else 사용 하게 됨

ResponseEntity

사용자의 HttpRequest에 대한 응답 데이터를 포함하는 클래스

프레임워크에서 제공하는 클래스중 HttpEntity(Http 요청, 응답) 클래스 존재
-> HttpHeader와 HttpBody를 포함하는 클래스

HttpEntity 클래스를 상속받아 구현한 클래스가 RequestEntity, ResponseEntity 클래스

package com.example.validation.dto;

import javax.validation.constraints.Email;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;

public class User {
    @NotBlank
    private String name;

    @Max(value = 90)
    private int age;

    @Email
    private String email;

    // 정규식 활용 : 필요한거 왠만하면 찾아쓰자!, 원하는 형태로 메세지 띄우고자한다면 인자로 message
    @Pattern(regexp = "^\\d{2,3}-\\d{3,4}-\\d{4}$", message =
            "핸드폰 번호의 양식과 일치하지 않습니다. 01x-xxx-xxxx" )
    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 + '\'' +
                '}';
    }
}
package com.example.validation.controller;

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

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

    @PostMapping("/user")
    // @Valid 가 붙은 객체는 어노테이션을 통해 검증을 진행하게 된다
    // 예외처리 방법 말고 예외값을 다루고 싶다면 BindingResult 활용하게 됨
    // Validation에 대한 결과가 bindingResult에 들어오게 됨

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

        // logic

        System.out.println(user);

        return ResponseEntity.ok(user);
    }
}

Custom Validation

AssertTrue/False와 같은 method 지정을 통해서 Custom Logic 적용 가능
-> 이것은 재사용이 불가함!!!!!

ConstraintValidator를 적용하여 재사용이 가능한 Custom Logic 적용 가능

커스텀 애너테이션 생성

package com.example.validation.annotation;

import com.example.validation.validator.YearMonthValidator;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Constraint(validatedBy = {YearMonthValidator.class}) // 얘를 통해서 검사한다는 의여
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
// 여러 곳에서 재사용 가능!!!
public @interface YearMonth {

    String message() default "yyyyMM 형식에 맞지 않습니다.";

    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default { };

    String pattern() default "yyyyMMdd";
}

커스텀 어노테이션 검증 하기 위한 Validator

package com.example.validation.validator;

import com.example.validation.annotation.YearMonth;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

// 커스텀한 어노테이션 활용하는 클래스 만들기 위해 벨리데이터 생성하였음
// 첫인자 : 어노테이션 두번째 인자 : 어노테이션에 들어가는 값 지정
public class YearMonthValidator implements ConstraintValidator<YearMonth, String> {
    private String pattern;

    @Override
    public void initialize(YearMonth constraintAnnotation) {
        this.pattern = constraintAnnotation.pattern();
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        System.out.println("assert true");
        //yyyyMM01 디폴트 값을 붙여서 검색하는 것 LocalDate 활용 위해
        // 어노테이션에 지정된 패턴대로 값이 잘 들어가 있는 지 확인 하기 위해 this.pattern
        // 사용자가 입력을 yyyyMM 형태로 하더라도 처리 로직에 01을 붙여 처리하였음
        try{
            LocalDate localDate = LocalDate.parse(value + "01", DateTimeFormatter.ofPattern(this.pattern));
        }catch(Exception e){
            return false;
        }
        return true;
    }
}

api controller

package com.example.validation.controller;

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

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

    @PostMapping("/user")
    // @Valid 가 붙은 객체는 어노테이션을 통해 검증을 진행하게 된다
    // 예외처리 방법 말고 예외값을 다루고 싶다면 BindingResult 활용하게 됨
    // Validation에 대한 결과가 bindingResult에 들어오게 됨

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

        // logic

        System.out.println(user);

        return ResponseEntity.ok(user);
    }
}

car_list의 car 객체

package com.example.validation.dto;

import com.fasterxml.jackson.annotation.JsonProperty;

import javax.validation.constraints.NotBlank;
import java.util.List;

public class Car {
    @NotBlank
    private String name;

    @NotBlank
    @JsonProperty("car_number")
    private String carNumber;
    
    @NotBlank
    @JsonProperty("TYPE")
    private String type;

    public String getName() {
        return name;
    }

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

    public String getCarNumber() {
        return carNumber;
    }

    public void setCarNumber(String carNumber) {
        this.carNumber = carNumber;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    @Override
    public String toString() {
        return "Car{" +
                "name='" + name + '\'' +
                ", carNumber='" + carNumber + '\'' +
                ", type='" + type + '\'' +
                '}';
    }
}

user 객체

package com.example.validation.dto;

import com.example.validation.annotation.YearMonth;

import javax.validation.Valid;
import javax.validation.constraints.*;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;

public class User {
    @NotBlank
    private String name;

    @Max(value = 90)
    private int age;

//    @Email
//    private String email;
//
//    // 정규식 활용 : 필요한거 왠만하면 찾아쓰자!, 원하는 형태로 메세지 띄우고자한다면 인자로 message
//    @Pattern(regexp = "^\\d{2,3}-\\d{3,4}-\\d{4}$", message =
//            "핸드폰 번호의 양식과 일치하지 않습니다. 01x-xxx-xxxx" )
//    private String phoneNumber;
//
//    @YearMonth //(pattern = "yyyyMM")
//    private String reqYearMonth; // yyyyMM

    @Valid // 이거 꼭 붙여줘야 -> Car 요소들의 NotBlank가 적용이 됨
    private List<Car> cars;

    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;
//    }
//
//    public String getReqYearMonth() {
//        return reqYearMonth;
//    }
//
//    public void setReqYearMonth(String reqYearMonth) {
//        this.reqYearMonth = reqYearMonth;
//    }

    // return 이 true일때만 정상, boolean 메서드에 대해선 is라는 메서드명이 붙어야함!!!!!!
    // User 안에 작성하였기에 재사용이 불가능함!!!(dto.user) -> 어노테이션을 만드는 것이 바람직
//    @AssertTrue(message =  "yyyymmdd 형태가 아닙니다.")
//    public boolean isreqYearMonthValidation(){
//        // 파싱이 안되면 false, LocalDate는 기본적으로 dd 까지 들어감
//        try{
//            LocalDate localDate = LocalDate.parse(getReqYearMonth() +"01", DateTimeFormatter.ofPattern("yyyyMMdd"));
//        }catch(Exception e){
//            return false;
//        }
//        return true;
//    }

    public List<Car> getCars() {
        return cars;
    }

    public void setCars(List<Car> cars) {
        this.cars = cars;
    }

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

profile
Journey for Backend Developer

0개의 댓글