티스토리 뷰

Validation

객체를 생성하거나 수정하는 등의 작업을 할 때 올바르지 않은 데이터를 걸러내고 유지하기 위해 데이터 검증이 적용됩니다.

Client Side 뿐만 아니라 Server Side에서도 데이터 유효성을 반드시 검사해야합니다. 클라이언트 쪽에서 데이터를 변조하여 서버로 쉽게 보낼 수 있는 상황이 생길 수 있기 때문에 반드시 서버측에서도 데이터 유효성 검사를 실시해야합니다.

 

스프링에서는 @validated 에노테이션을 이용하여 검증할 수 있습니다.

 

그러나 검증 기능을 매번 코드로 작성하는 것은 번거롭습니다. 객체 값 하나하나를 If 문을 통해서 검증하는 것은 코드를 작성하는 과정에서 반복적이고 효율적이지 않습니다. 게다가 특정 필드에 대한 검증 로직은 대부분 빈 값(Null)인지 아닌지, 특정 크기를 넘는지 아닌지와 같이 매우 일반적인 로직입니다. 

 

Item 객체가 있을 때, 이 객체가 등록하는 과정과 수정하는 과정에서 요구되는 범위나 값의 범위가 다를 수 있습니다.

예를 들어 등록하는 과정에서는 가격이 최대 백만원이지만, 수정하는 과정에서는 제한을 두지 않는다고 하면 적용하기 힘들어집니다.

 

그러므로 아래와 같은 검증 로직을 모든 프로젝트에 적용할 수 있게 하는 것이 Bean Validation입니다.

특정 클래스 필드에 @어노테이션을 적용하여 필드가 갖는 제약 조건을 정의하는 구조로 이루어져있습니다.

public class Item {
      private Long id;
      
      @NotBlank
      private String itemName;
      
      @NotNull
      @Range(min = 1000, max = 1000000)
      private Integer price;
  
 }

@NotBlank: 빈값 + 공백만 있는 경우를 허용하지 않음

@NotNull: null 값을 허용하지 않음

@Range(min =100, max=1000): 범위 안의 값이면 허용, 아니면 허용하지 않음

@Max(999): 최대 999까지만 허용

 

 

@Valid, @Validated
@Valid는 JAVA에서 지원하는 어노테이션이고, @Validated는 스프링에서 지원해주는 어노테이션입니다.
검증시 둘 다 사용가능합니다. 둘 중 아무거나 사용해도 동일하게 작동하지만,
@Validated는 내부에 groups라는 기능을 포함하고 있다는 특징이 있습니다.

Bean Validation

우선 빈 검증을 사용하기 위해서는 build.gradle에 의존관계 하나를 추가해야합니다.

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

 

검증 순서

1. @ModelAttribute 각각의 필드에 타입 변환 시도

- 성공하면 다음으로

- 실패하면 typeMismatch로 FieldError 추가

 

2. Validator 적용

바인딩에 성공한 필드만 Bean Validation을 적용한다.

 

즉, @ModelAttribute -> 각각의 필드 타입 변환시도 -> 변환에 성공한 필드만 BeanValidation 적용


Item 저장 API 예시

먼저 Item 객체와 객체 필드를 다음과 같이 정의한다고 가정합니다.

HTTP Message -> ItemSaveForm -> Controller -> Item 생성 -> Repository 과정을 거친다고 가정합니다.

@Data
public class Item {
    private Long id;
    private String itemName;
    private Integer price;
    private Integer quantity;

    public Item() {}

    public Item(String itemName, Integer price, Integer quantity) {
        this.itemName = itemName;
        this.price = price;
        this.quantity = quantity;
    }
}

 

Item을 저장하는 ItemSaveForm을 만들어 Item을 저장하는 검증 폼을 생성합니다.

@Data
public class ItemSaveForm {

    @NotBlank
    private String itemName;

    @NotNull
    @Range(min = 1000, max = 1000000)
    private Integer price;

    @NotNull
    @Range(max = 9999)
    private Integer quantity;
}

 

Item 객체를 저장하는 REST API 형식의 컨트롤러를 정의합니다.

POST 방식으로 전송하며 @RequestBody를 사용하여 Body 형태로 데이터를 전송받습니다.

 참고
@ModelAttribute는 HTTP Request Parameter를 다룰 때 사용하고
@RequestBody는 HTTP Body의 데이터를 객체로 변환할 때 사용합니다. 주로 API JSON 요청을 다룰 때 사용.
@Slf4j
@RestController
@RequestMapping("/validation/api/items")
public class ValidationItemApiController{

    @PostMapping("/add")
    public Object addItem(@RequestBody @Validated ItemSaveForm form, BindingResult bindingResult){
        log.info("API 컨트롤러 호출");

        if (bindingResult.hasErrors()) {
            log.info("검증 오류 발생 errors={}", bindingResult);
            return bindingResult.getAllErrors();
        }

        log.info("성공 로직 실행");
        return form;
    }
}

 

Postman을 사용해서 성공 테스트와 실패 테스트를 각각 진행해보면 로그가 다르게 찍히는 것을 확인할 수 있습니다.

 

API 요청의 경우 3가지 경우를 나눠 생각해야합니다.

1. 성공요청: 성공

- 객체의 모든 데이터가 형태와 값이 올바르게 입력되었을 경우

 

2. 실패요청: JSON을 객체로 생성하는 것 자체가 실패

- 객체의 데이터가 하나라도 형태가 맞지 않아 객체를 생성하는 과정부터 실패

 

3. 검증 오류 요청: JSON을 객체로 생성하는 것은 성공했고, 검증에서 실패 

- 객체의 데이터 형태는 모두 올바르지만, 데이터 값이 검증 범위내에 들어있지 않아 실패

 

 


@ModelAttribute vs @RequestBody

@ModelAttribute
HTTP 요청 파라미터를 처리하는 @ModelAttribute는 객체의 필드 단위로 적용되기 때문에 특정 필드에 타입이 맞지 않는 오류가 발생해도 나머지 필드는 정상적으로 처리할 수 있습니다.

 @ModelAttribute는 필드 단위로 정교하게 바인딩이 적용됩니다. 특정 필드가 바인딩 되지 않아도 나머지 필드는 정상 바인딩 되고, Validator를 사용한 검증도 적용할 수 있습니다.

 

 

@RequestBody
HttpMessageConverter는 @ModelAttribute와 다르게 각각의 필드 단위로 적용되는 것이 아니라, 전체 객체 단위로 적용됩니다.
따라서 메시지 컨버터의 작동이 성공해서 객체를 만들어야 @Valid, @Validated가 적용됩니다.

@RequestBody는 HttpMessageConvert 단계에서 JSON 데이터를 객체로 변경하지 못하면 이후 단계 자체가 진행되지 않고 예외가 발생합니다. 컨트롤러 호출도 되지 않고 Validator도 적용할 수 없습니다.

 

 

 

 

댓글