Spring

[Spring] Spring Bean Validation의 @Valid vs @Validated

DEV숨 2022. 11. 14. 23:04

Spring Bean을 검증하는 방법

Spring Bean을 검증하는 방법에는 어떤 것들이 있을까?

  • 코드로 검증하기
  • Bean Validation

코드로 검증한다고 한다면?

다음과 같은 객체가 있다고 가정하자.

public class Item {
	private final String itemName;
	private final int price;
}

그리고 다음과 같은 검증이 필요하다고 가정해보자.

  • itemName은 null값이 올 수 없다.
  • itemName은 2자 이상, 30자 이하의 값이 와야한다.
  • price는 null값이 올 수 없다.
  • price 는 1000이상이어야한다….

이러한 과정을 모두 코드로 작성한다면?

코드가 복잡해질 수 있다.

Bean Validation을 활용한다면 어노테이션 하나로 검증 로직을 매우 편리하게 적용할 수 있다.

 

 

Bean Validation이란?

Bean Validation이란 특정 구현체가 아니라 Bean Validation 2.0(JSR-380)이라는 기술 표준이다.

즉, 검증 어노테이션과 여러 인터페이스의 모음이다.

마치 JPA가 표준 기술이고 그 구현체로 하이버네이트가 있는 것과 같다.

 

Bean Validation을 구현한 기술중에 일반적으로 사용하는 구현체는 하이버네이트 Validator이다.

이름이 하이버네이트가 붙어서 그렇지 ORM과는 관련이 없다.

public class Item {

	@NotNull
	private final String itemName;

	@NotNull
	private final int price;
}

다음과 같이 어노테이션으로 검증코드를 대체하는 것이다.

그렇다면 이 BeanValidation 어노테이션에는 두가지가 있다.

 

 

@Validated vs @Valid

  • @Valid는 자바 표준 검증 어노테이션이다. @Valid를 사용하려면 build.gradle파일에implementation ‘org.springframework.boot:spring-boot-starter-validation’의존성 추가가필요하다.
  • @Validated는 스프링 전용 검증 어노테이션이다. 내부에 groups라는 기능을 포함하고 있다.

spring-boot-starter-validation 의존관계를 추가하면 라이브러리가 추가된다.

Jakarta Bean Validation

  • jakarta.validation-api: Bean Validation 인터페이스
  • hibernate-validator 구현체

 

@Valid동작 원리

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

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

스프링부트는 자동으로 LocalValidatorFactoryBean 을 글로벌 Validator로 등록한다.

이 Validator는 @NoNull 같은 어노테이션을 보고 검증을 수행한다. 이렇게 글로벌 Validator가 적용되어 있기 때문에 @Valid, @Validated만 적용하면 된다.

 

모든 요청은 디스패처 서블릿을 통해 컨트롤러로 전달된다. 전달 과정에서는 컨트롤러 메소드의 객체를 만들어주는 ArgumentResolver가 동작하는데, @Valid는 이 ArgumentResolver에 의해 처리된다.

  • @RequestBody는 json 형식의 요청을 객체로 변환해주는 작업이 ArgumentResolver 구현체인 RequestResponseBodyMethodProcessor가 처리하며, 이 내부에 @Valid로 시작하는 어노테이션이 있을 경우에 유효성을 검사한다.
  • @ModelAttribute는 ModelAttributeMethodProcessor에 의해 @Valid가 처리된다.

 

검증에 오류가 있다면 MethodArgumentNotValidException 예외가 발생한다. 디스패처 서블릿에 기본으로 등록된 Exception Resolver인 DefaultHandlerExceptionResolver에 의해 400 BadRequest 에러가 발생한다.

 

@Valid는 컨트롤러 게층에서만 동작하 다른 계층에서는 검증되지 않는다고 한다.

다른 계층 (Service 등)에서도 파라미터를 검증하기 위해서는 @Validated와 결합이 되어야 한다고 한다. 

 

 

@Validated 동작 원리

입력 파라미터의 유효성 검증은 컨트롤러에서 최대한 처리하고 넘겨주는 것이 좋다. 그러나 개발을 하다보면 팀 컨벤션에 따라서 다른 계층에서 파라미터를 검증해야 할 수도 있다. 예) Service단에 requestDto가 오는 경우 등

 

Spring에서는 AOP기반으로 메소드의 요청을 가로채서 유효성 검증을 진행해주는 @Validated를 제공하고 있다.

@Validated는 JSR 표준 기술이 아니며 Spring 프레임워크에서 제공하는 어노테이션 및 기능이다.

 

유효성 검증에 실패하면@Valid의 MethodArgumentNotValidException 예외가 아닌 ConstraintViolationException 예외가 발생한다. @Valid는 특정 ArgumentResolver에 의해 유효성 검사가 진행되었다. 그러나 @ValidatedAOP기반으로 메소드 요청을 인터셉터하여 처리된다. @Validated를 클래스 레벨에 선언하면 해당 클래스에 유효성 검증을 위한 인터셉터(MethodValidationInterceptor)가 등록된다.

 

그리고 해당 클래스의 메소드들이 호출될 때 AOP의 포인트 컷으로써 요청을 가로채서 유효성 검증을 진행한다.

이러한 이유로 @Validated를 사용하면 컨트롤러, 서비스, 레포지토리 등 계층에 무관하게 스프링 빈이라면 유효성 검증을 진행할 수 있다.

 

대신 클래스에는 유효성 검증 인터셉터를 등록하도록 @Validated를, 검증을 진행할 메소드에는 @Valid를 선언해주어야 한다.

이러한 이유로 @Valid에 의한 예외는 MethodArgumentNotValidException이며, @Validated에 의한 예외는 ConstraintViolationException이다. 이를 알고 있으면 나중에 예외 처리를 할 때 도움이 된다.

 

또한 @Validated는 groups라는 기능도 지원한다.

groups 기능은 동일한 객체에 대해 제약조건이 요청에 따라 달라질 수 있을 때 사용하면 좋다.

 

다음과 같은 필드를 가지는 객체가 있을 때

public class Item { 
	private final Stirng itemName;
	private final int price;
}

예를 들어 생성하는 요청은 price가 5000이상이어야하지만

수정하는 요청은 price가 10000이상이어야한다.

이럴 때는 제약 조건이 적용될 검증 그룹을 지정할 수 있는 기능을 @Validated를 통해 제공하고 있다.

public interface SaveCheck {}
public interface UpdateCheck {}
public class Item { 
	
	private final Stirng itemName;

	@Min(value = 5000, groups = SaveCheck.class)
	@Min(value = 10000, groups = UpdateCheck.class)
	private final int price;
}
@PostMapping
public String editItem(@Validated(UpdateCheck.class)
@ModelAttribute Item item) {
	...
}

다음과 같이 파라미터로 UpdateCheck를 넣어주었다면 UpdateCheck에 해당하는 제약 조건만 검증된다. 만일 @Validated에 특정 마커를 지정해주지 않았거나, groups가 지정되어 있는데 @Valid를 이용한다면 다음과 같이 처리된다.

  • @Validated에 특정 클래스를 지정하지 않는 경우: groups가 없는 속성들만 처리
  • @Valid or @Validated에 특정 클래스를 지정한 경우: 지정된 클래스를 groups로 가진 제약사항만 처리

그러나 groups 기능을 복잡해서 실제 잘 사용되지 않는다고 한다. 다음과 같은 경우는 그냥 ItemSaveRequest, ItemUpdateRequest처럼 객체를 분리해 사용한다.

 

제약 조건 어노테이션 예

  • @NotNull: 해당 값이 null이 아닌지 검증함
  • @NotEmpty: 해당 값이 null이 아니고, 빈 스트링("") 아닌지 검증함(" "은 허용됨)
  • @NotBlank: 해당 값이 null이 아니고, 공백(""과 " " 모두 포함)이 아닌지 검증함
  • @AssertTrue: 해당 값이 true인지 검증함
  • @Size: 해당 값이 주어진 값 사이에 해당하는지 검증함(String, Collection, Map, Array에도 적용 가능)
  • @Min: 해당 값이 주어진 값보다 작지 않은지 검증함
  • @Max: 해당 값이 주어진 값보다 크지 않은지 검증함
  • @Pattern: 해당 값이 주어진 패턴과 일치하는지 검증함

hibernate validator 공식문서에 더 있다.

https://docs.jboss.org/hibernate/validator/6.2/reference/en-US/html_single/#validator-defineconstraints-spec

 

검증 순서

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

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

BeanValidator는 바인딩에 실패한 필드는 BeanValidation을 적용하지 않는다.

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

private final Stirng itemName;
private final int price;

위와 같은 필드가 있다고 가정할 때

예)

  • itemName에 문자 “A” 입력 → 타입 변환 성공 → itemName 필드에 BeanValidation 적용
  • price에 문자 “A”입력 → “A”를 숫자 타입 변환 시도 실패 → typeMismatch FieldError 추가 → price 필드는 BeanValidation 적용 X

 

 

@ModelAttribute vs @RequestBody

  • HTTP 요청 파라미터를 처리하는 @ModelAttribute는 각각의 필드 단위로 세밀하게 적용된다. 그래서 특정 필드에 타입이 맞지 않는 오류가 발생해도 나머지 필드는 정상 처리할 수 있다.
  • HttpMessageConverter를 사용하는 @RequestBody는 @ModelAttribute와 다르게 각각의 필드 안뒤로 적용되는 것이 아니라, 전체 객체 단위로 적용된다. 즉 JSON객체를 데이터 객체로 변환하지 못하면 이후 단계 자체가 진행되지 않고 예외가 발생한다. 컨트롤러도 호출되지 않고, Validator도 적용할 수 없다.

 

 

reference

https://mangkyu.tistory.com/174

인프런 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술