본문 바로가기
Spring

[Spring] @Valid, @Validated 동작 원리

by doodoom 2022. 10. 20.

0. 이 글을 쓰게 된 이유

개발을 할 때, 데이터의 유효성 검증은 굉장히 중요하고 애플리케이션 전체에서 발생한다.
클라이언트에서 넘어온 데이터가 유효한지, 서비스 레이어에서 파라미터 값이 제대로 된 값인지 등 여러 곳에서 데이터 검증은 필수다.
하지만 유효성 검증에는 이런 특징이 있는 만큼 문제점이 존재한다.

문제점

  1. 애플리케이션 전체에 분산되어 있다.
  2. 코드 중복이 심하다.
  3. 비즈니스 로직에 섞여있어 복잡하다.
    이러한 문제점들 때문에 유효성 검증은 유지/보수가 어렵다.

해결책

Java에서도 이러한 문제를 알고 Bean Validation이라는 유효성 검증 프레임워크를 제공하고 있다(JSR-303 표준 스펙). Bean Validation 프레임 워크는 위에서 말한 문제들을 해결하기 위해 다양한 제약을 모델(domain model)에 어노테이션으로 해결 할 수 있게 한다. 이 제약을 유효성 검증이 필요한 객체에 직접 어노테이션으로 정의하는 방법으로 기존 유효성 검사 로직의 문제를 해결한다. 제약은 구현체를 따로 놓아 어노테이션에 매핑 될 수 있게 한다.

1. @Valid과 @Validated

1.1 @Valid

@Valid는 Bean Validator를 통해 객체의 데이터 유효성 검증을 지시하는 어노테이션이다. 하지만 이는 Bean Validation의 표준 명세일 뿐 구현체를 가지고 있지 않는다. 그래서 구현체를 따로 연결해서 사용해야 한다.
Spring에서는 이 구현체를 Hibernate Validator를 사용한다. Hibernate Validator는 Bean Validator의 모든 어노테이션을 구현하고 있다고 보면 된다.
Bean Validator 어노테이션과 Hibernate Validator 구현체와의 연결은 spring의 어플리케이션 컨텍스트가 뜰 때, ConstraintHelper에서 연결해주는 것 같다.(뜰 때인지 호출될 때인지 잘 모르겠음)

1.1.1 @Valid의 동작 과정


알다싶이 Client에서 요청이 들어오면 Dispatcher Servlet에서 요청에 맞는 컨트롤러에 요청을 전달한다. 이때, 전달 과정에서 컨트롤러 메소드의 객체를 전달해주는 HandlerMethodArgumentResolver 동작하는데, @Valid 역시 HandlerMethodArgumentResolver 의해 처리가 된다.(ArgumentResolver가 하는 일로 대표적인 것은 @RequestBody가 붙은 데이터를 Json 포멧으로 변경해준다)
HandlerMethodArgumentResolver의 resolveArgument 메소드에서 @Valid로 시작하는 어노테이션이 붙어있으면 valid를 진행하는 식으로 동작한다. 즉, aop 방식이라기 보다는 dispathcher servlet에서 검사를 한번 해준다는게 맞다.

다음과 같이 유효성 검증을 할 파라미터에 @Valid를 붙여줘야 진행된다.

@PostMapping("/") 
public String add(@Valid Request request) {
      ...
}

그리고 오류가 발생하면 MethodArgumentNotValidException이 발생하고, 디스패치 서블릿에 기본으로 등록된 Exception Resolver인 DefaultHandlerExceptionResolver에 의해 400 에러가 발생한다.

spring에서 @Valid는 기본적으로 컨트롤러에서만 동작하도록 설계되어있고 다른 계층에서는 검증이 되지 않는다.
다른 계층에서 검증하기 위해서는 @Validated와 결합되어야하는데, 이제 살펴보도록 하자.

1.1.2 @Validated의 동작 과정

어플리케이션 개발을 하다보면 Controller 뿐만 아니라 다른 계층에서도 데이터 유효성 검증을 해야할 필요가 있다. Spring에서는 이를 위해 AOP 기반으로 메소드 요청을 가로채서 유효성 검증을 해주는 @Validated를 제공하고 있다. 즉, @Validated는 JSR 표준 기술이 아니며 Spring에서 제공하는 AOP 기반 기능이다.

사용법은 다음과 같이 유효성 검증이 필요한 클래스에 @Validated를 붙이고, 유효성 검증을 할 파라미터에 @Valid를 붙여주면 진행된다.

@Service
@Validated
public class Service {

    public void add(@Valid Request request) {
        ...
    }
}

오류가 발생하면 @Valid와 다르게 ConstraintViolationException이 발생한다.

정리하자면 @Validated를 클래스에 선언하면 해당 클래스에 유효성 검증을 위한 인터셉터인 MethodValidationInterceptor가 등록된다. 그리고 해당 클래스의 메소드가 호출 될 때 AOP가 포인트컷으로 확인을 하고 요청을 중간에 가로채어 유효성 검증을 진행하는 코드가 들어간다. @Validated는 스프링의 기능이기 때문에 스프링 빈이라면 모두 유효성 검증이 가능하다. 그리고 MethodValidationInterceptor에 Validator이 의존성 주입으로 들어가므로 @Valid와 똑같이 Hibernate Validator를 사용한다. 즉, Bean Validation을 그대로 이용할 수 있는 것이다.

2. 정리

유효성 검증을 할 떄에는 Bean Validation을 이용하면 편하다. java에서 제공하는 어노테이션을 잘 확인하면서 사용하면 어플리케이션 어디서든지 유효성 검증을 효율적으로 할 수 있다.
java validation 공식문서 : https://javaee.github.io/javaee-spec/javadocs/javax/validation/constraints/package-summary.html