Validation, Bean Validation

dev_314·2023년 3월 23일
0

Spring - Trial and Error

목록 보기
7/7

Validation

요청을 효과적으로 검증하고 싶다.

Validator

  • 검증을 위해 스프링이 제공하는 인터페이스
package org.springframework.validation;

public interface Validator {
	boolean supports(Class<?> clazz);
    void validate(Object target, Errors errors);
}
  • boolean supports(Class<?> clazz)
    • Validator 구현체가 '어떤 Class를 검증하는지' 명시하는 메서드
  • void validate(Object target, Errors errors);
    • 실제로 검증 로직을 수행하는 메서드

Validator 구현체

import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;

// DI를 위해 Bean으로 등록
// Bean 등록 안 해도 사용은 할 수 있음
@Component 
public class ItemValidator implements Validator {

    @Override
    public boolean supports(Class<?> clazz) {
        return CreateItemRequest.class.isAssignableFrom(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        CreateItemRequest request = (CreateItemRequest) target;
        if (request.getName().isBlank() || request.getName().isEmpty()) {
            errors.reject("name", "name should not be blank");
        }
        if (1000 > request.getPrice() || request.getPrice() > 1_000_000) {
            errors.reject("price", "price should be in range from 1000 to 1_000_000");
        }
        if (request.getQuantity() > 9_999) {
            errors.reject("quantity", "quantity must be less than 9_999");
        }
    }
}

Controller에서 요청이 발생하면

  1. supports를 통해 내가 검증할 수 있는 형식(class)인가를 파악함
  2. 내가 검증할 수 있다면(supports 통과), validate메서드로 검증을 수행한 뒤, Errors에 검증 실패 필드를 추가

Controller에서 사용하기

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.*;

@Slf4j
@RestController
@RequestMapping("/api/items")
@RequiredArgsConstructor
public class ItemController {

    private final ItemValidator itemValidator;

	// 요청이 발생할 때 마다 validator가 호출된다.
    @InitBinder
    public void init(WebDataBinder dataBinder) {
        dataBinder.addValidators(itemValidator);
        // Validator 여러 개 등록 가능
    }

	// Validator에서 검증한 결과가 BindingResult bindingResult에 담겨있다.
    @PostMapping
    public void createItem(@Validated @RequestBody CreateItemRequest request, BindingResult bindingResult) {
        log.info("request: {}", request);

        if (bindingResult.hasErrors()) {
            log.info("hasErrors: {}", bindingResult);
        }
    }
}
  1. @InitBinder을 통해, 컨트롤러에서 사용할 Validator를 등록한다.
  2. @Validated 또는 @Valid를 통해 validator를 실행하야 함을 나타낸다.
  3. @InitBinder에 등록된 Validator 구현체들 중에서, support를 통과하는 구현체의 validate를 사용한다.
  4. 검증 결과가 BindingResult에 담겨있다.

주의사항

  1. @InitBinder에 등록된 Validator는 자기가 속한 컨트롤러의 요청에만 반응한다.
  2. 다른 컨트롤러에서도 반응하게 하려면, Global Validator로 등록해야 한다. (하단에서 추가 설명)
  3. 동일한 Validator가 Local과 Global 둘 다 설정되어 있으면, 검증을 두 번 수행한다. 이렇게 되면 실제 검증 실패 개수 * 2가 된다.

Bean Validation

Validator 구현체를 만들어서 사용자 요청을 검증할 수 있다.
그런데 필드마다 검증 로직을 하나하나 만들어야 해서 귀찮다.

Bean Validation을 사용하면 Annotation으로 간편하게 검증을 할 수 있다.

build.gradle 설정

depenedencies {
	...
	implementation 'org.springframework.boot:spinrg-boot-starter-validation'
    ...
}

spinrg-boot-starter-validation을 추가하면, 자동으로 Validator 인터페이스와 구현체를 사용할 수 있게 된다.
Bean Validation은 구현체가 아닌, 자바 표준 기술이다.
주로 Hibernate에서 만든 구현체가 사용된다.

jakarta.validation:jakarta.validation-api:2.0.2 -> 인터페이스
org.hibernate.validator:hiberante-validator:6.1.7.Final -> 구현체

기본 사용법

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.Range;

import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class CreateUserRequest {

    @NotBlank
    private String name;

    @NotNull
    @Range(min = 1_000, max = 1_000_000)
    private Integer price;

    @NotNull
    @Max(9_999)
    private Integer quantity;
}

Range는 표준에는 없는 Hibernate가 제공하는 Annotation이다. 그런데 보통은 Hibernate 구현체를 사용하므로 걱정없이 사용해도 된다고 한다.

다양한 Annotation은 여기서 알아보자

Spring과 같이 사용하기

스프링의 Bean Validator 등록 과정

  1. 스프링 부트가 spring-boot-starter-validation라이브러리를 인식하면, 알아서 Bean Validation을 인지하고 스프링과 통합시킨다.
  2. 스프링 부트는 LocalValidatorFactoryBean을 글로벌 Validator로 등록한다.
  3. 등록된 글로벌 Validator는 Annotation을 보고 검증을 수행한다.

사용 예제

// Controller
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequestMapping("/api/items")
public class ItemController {

    @PostMapping
    public void createItem(@Validated @ModelAttribute CreateItemRequest request) {
    	...
    }
}

요청 객체가 Annotation의 제약 조건을 위배하면 MethodArgumentNotValidException 예외가 발생하고, 이를 DefaultHandlerExceptionResolver가 처리하여 400응답을 보낸다.

그런데 검증 결과를 BindingResult 파라미터로 사용하면 예외로 인식하지 않는다.

@Slf4j
@RestController
@RequestMapping("/api/items")
public class ItemController {

    @PostMapping
    public void createItem(@Validated @RequestBody CreateItemRequest request, BindingResult bindingResult) {
        ...
    }
}

검증 조건을 만족하지 못하지만, 예외 응답(400 BAD REQUEST)대신 정상 응답 (200 OK)를 보낸다. 이는 Bean Validator를 사용하지 않는 상황에서도 마찬가지

주의사항

  1. WebMvcConfigurergetValidator를 통해 글로벌 Validator를 설정하면, 스프링 부트는 LocalValidatorFactoryBean을 글로벌 Validator로 등록하지 않는다.

  2. Bean Validation은 바인딩에 성공한 필드에만 이뤄진다.

    정수형 필드에 문자열 입력 -> 바인딩 실패 -> 검증할 필요 없음
    정수형 필드에 정수형 입력 -> 바인딩 성공 -> 검증 진행

조건적으로 검증하기

상황에 맞춰 검증할 필드를 변경하고 싶다.

EX) 생성할 때는 ID 없음 <-> 수정할 때는 ID 있음

groups 사용하기

// Check
public interface CreateCheck {}
public interface UpdateCheck {]

// DTO
@NotNull(groups = {CreateCheck.class, UpdateCheck.class})
private Long id;
    
// Controller
@PostMapping
public void create(@Valiated(CreateCheck.class) ...)
    
@PutMapping
public void update(@Validated(UpdateCheck.class) ...)

groups 옵션을 통해 검증할 상황을 구분할 수 있다.

요청마다 별도 객체 사용하기

위 방법은 사용이 번거로워서 잘 사용하지 않는다.
대신에 요청마다 별도의 객체를 만들어서 사용한다.

HttpMessageConverter

Bean Validation은 HttpMessageConverter(@RequestBody)에도 사용할 수 있다.

다음의 과정을 거쳐 요청을 처리하게 된다.

요청 발생 -> 객체로 만들기 -> 검증 -> Controller 호출

@ModelAttribute는 필드 단위로 바인딩이 진행되는 반면, @RequestBody는 객체 단위로 바인딩이 진행된다.

즉, 여러 필드 중 한 필드가 바인딩이 안 되는 경우, @ModelAttribute는 그 필드를 null로 처리하고 객체를 만드는데, @RequestBody는 객체 자체가 만들어지지 않는다. 따라서 @RequestBody는 이어지는 Validation 과정도 진행되지 않을 수 있다.

profile
블로그 이전했습니다 https://dev314.tistory.com/

0개의 댓글