Validation (2)

dereck·2025년 2월 5일

TIL

목록 보기
19/21

Bean Validation

Bean Validation 개요

Bean Validation은 특정한 구현체가 아니라 Bean Validation 2.0(JSR-380)이라는 기술 표준이다. 쉽게 말하면 검증 어노테이션과 여러 인터페이스의 모음이며 마치 JPA 기술 표준이고 그 구현체로 하이버네이트가 있는 것과 같다.

Bean Validaion을 구현한 기술 중에 일반적으로 사용하는 구현체는 하이버네이트 Validator이다. 이름에 하이버네이트가 붙었지만 ORM과는 관련없다.

하이버네이트 Validator 관련 링크
공식 사이트: http://hibernate.org/validator/
공식 메뉴얼: https://docs.jboss.org/hibernate/validator/6.2/reference/en-US/html_single/
검증 어노테이션 모음: https://docs.jboss.org/hibernate/validator/6.2/reference/en-US/html_single/#validator-defineconstraints-spec

의존 관계 추가

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

참고
jakarta.validation.constraints.NotNull
org.hibernate.validator.constraints.Range

jakarta.validation으로 시작하면 특정 구현에 관계없이 제공되는 표준 인터페이스이고, org.hibernate.validator로 시작하면 하이버네이트 validator 구현체를 사용할 때만 제공되는 검증 기능이다. 실무에서 대부분 하이버네이트 validator를 사용하므로 자유롭게 사용 가능하다.

동작 원리

@Valid

@Valid의 경우 ArgumentResolver에 의해 처리된다.

대표적으로 @RequestBody에서 JSON 메시지를 객체로 변환해주는 작업을 ArgumentResolver의 구현체인 RequestResponseBodyMethodProcessor가 처리하며 이 내부에 @Valid로 시작하는 어노테이션이 있을 경우에 유효성 검사를 진행한다.

@ModelAttribute의 경우엔 ModelAttributeMethodProcessor에 의해 @Valid가 처리된다.

그리고 검증에 오류가 있다면 MethodArgumentNotValidException 예외가 발생하게 된다.

@Valid는 기본적으로 Controller에서만 동작하며 기본적으로 다른 계층에서는 검증이 되지 않는다.

@Validated

입력 파라미터의 유효성 검증은 컨트롤러에서 최대한 처리하고 넘겨주는 것이 좋다. 하지만 개발을 하다보면 불가피하게 다른 곳에서 파라미터를 검증해야 할 수 있다.

이를 위해 Spring에서는 AOP 기반으로 메서드의 요청을 가로채서 유효성 검증을 진행해주는 @Validated를 제공하고 있다. @Validated는 JSR 표준 기술이 아니며 Spring Framework에서 제공하는 어노테이션 및 기능이다.

클래스에 @Validated를 붙여주고, 유효성을 검증할 메서드의 파라미터에 @Valid를 붙여주면 된다.

@Service
@Validated
public class ItemService {
	public void saveItem(@Valid SaveItemRequest request) {
    	...
    }
}

유효성 검증에 실패하면 ConstraintViolationException이 발생한다. @Validated는 AOP 기반으로 메서드 요청을 인터셉터하여 처리된다. @Validated를 클래스 레벨에 선언하면 해당 클래스에 유효성 검증을 위한 AOP의 어드바이스 또는 인터셉터(MethodValidationInterceptor)가 등록된다.

따라서 @Validated를 사용하면 계층에 무관하게 Spring Bean이라면 유효성 검증을 진행할 수 있다.

Spring Boot 3.2 이후

스프링 부트 3.2(Spring Framework 6.1)부터 Spring MVC와 WebFlux에서 유효성 검사를 위한 @Constraint 관련 어노테이션을 기본적으로 지원하도록 개선되었다. 컨트롤러를 위한 파라미터를 생성하는 ArgumentResolver들이 모두 동작하고, 컨트롤러의 메서드 호출이 준비되었을 때 유효성 검사가 진행된다. 스프링은 이를 MethodValidator라고 부른다.

스프링의 MethodValidator 관련 기능을 활용하기 위해서는 다음의 조건들이 충족되면 된다.

  1. 컨트롤러에 @Validated를 통한 AOP 기반 검증이 존재하지 않음
  2. LocalValidatorFactoryBean과 같은 jakarta.validation.Validator 타입의 빈이 등록됨
  3. 메서드 파라미터 유효성 검증 어노테이션이 붙어있음
@RestController
public class HelloController {
	@GetMapping("/hello")
    public String hello(@RequestParam @Length(min = 1) String name) {
    	...
    }
}

이를 통해 기존에는 불가능했던 방식으로도 동작이 가능해진다. 왜냐하면 파라미터가 모두 준비된 이후에 파라미터에 붙어있는 유효성 검사 어노테이션을 파싱하여 유효성 검사를 진행하기 때문이다.

스프링 통합 전

검증기 생성

ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();

위 코드와 같이 검증기를 생성한다. 이후 스프링과 통합하면 직접 코드를 작성하지 않기 때문에 '이렇게 사용하는구나' 정도만 참고하자.

검증 실행

Set<ConstraintViolation<Item>> violations = validator.validate(item);

검증 대상(item)을 직접 검증기에 넣고 그 결과를 받는다. Set에는 ConstraintViolation이라는 검증 오류가 담긴다. 따라서 결과가 비어있으면 검증 오류가 없는 것이다.

스프링 통합 후

스프링 MVC는 어떻게 Bean Validator를 사용할까?

스프링 부트가 spring-boot-starter-validation 라이브러리를 넣으면 자동으로 BeanValidation를 인지하고 스프링에 통합한다.

LocalValidatorFactoryBean을 글로벌 Validator로 등록한다. 이 Validator는 @NotNull 같은 어노테이션을 보고 검증을 수행한다. 이렇게 글로벌 Validator가 적용되어 있기 때문에 @Valid, @Validated만 적용하면 된다. 만약 검증 오류가 발생하면 FieldError, ObjectError를 생성해서 BindingResult에 담아준다.

검증 순서

  1. @ModelAttribute 각각의 필드에 타입 변환 시도
    1. 성공하면 다음으로
    2. 실패하면 typeMismatchFieldError 추가
  2. Validator 적용

BeanValidator는 바인딩에 실패한 필드는 BeanValidation을 적용하지 않는다. 타입 변환에 성공해서 바인딩에 성공한 필드여야 BeanValidation 적용이 의미가 있기 때문이다.

주의
글로벌 Validator를 직접 등록하면 스프링 부트는 BeanValidator를 글로벌 Validator로 등록하지 않는다.

에러 코드

메시지 등록

BeanValidation이 기본으로 제공하는 오류 메시지를 좀 더 자세히 변경하고 싶으면 메시지를 등록하면 된다.

# errors.properties
NotBlank={0} 공백 X
Range={0}, {2} ~ {1} 허용
Max={0}, 최대 {1}

{0}은 필드명이고, {1}, {2}는 각 어노테이션 마다 다르다.

BeanValidation 메시지 찾는 순서

  1. 생성된 메시지 코드 순서대로 messageSource에서 메시지 찾기
  2. 어노테이션의 message 속성 사용
    • @NotBlank(message = "{0}은 공백일 수 없습니다.")
  3. 라이브러리가 제공하는 기본 값 사용
    • 공백일 수 없습니다.

오브젝트 오류

BeanValidation에서 특정 필드가 아닌 해당 오브젝트 관련 오류는 @ScriptAssert()를 사용할 수도 있다.

@Data
@ScriptAssert(
	lang = "javascript", script = "this.price * this.quantity >= 10000"
)
public class Item {
	...
}

하지만 실제로 사용해보면 제약이 많고 복잡하며 실무에서는 검증 기능이 해당 객체의 범위를 넘어서는 경우들도 종종 등장할 때 대응이 어렵기 때문에 오브젝트 오류(글로벌 오류)의 경우 @ScriptAssert를 억지로 사용하는 것보다 오브젝트 오류 관련 부분만 자바 코드로 작성하는 것을 권장한다.

HTTP 메시지 컨버터

@Valid, @ValidatedHttpMessageConverter(@RequestBody)에도 적용할 수 있다.

API의 경우 3가지 경우를 나누어 생각해야 한다.

  • 성공 요청
  • 실패 요청: JSON을 객체로 생성하는 것 자체가 실패함
  • 검증 오류 요청: JSON을 객체로 생성하는 것은 성공했지만, 검증에서 실패함

참고
@ModelAttribute는 HTTP 요청 파라미터(URL, 쿼리 스트링, POST Form)를 다룰 때 사용한다.
@RequestBody는 HTTP Body의 데이터를 객체로 변환할 때 사용한다. 주로 API JSON 요청을 다룰 때 사용한다.

References

0개의 댓글