본문 바로가기
스프링

[Spring] 검증 - @Valid, @validated 동작 원리

by 리포터12 2022. 12. 29.
728x90

로그인 기능 구현 시도 중,  @Pattern 을 통해 값에 대한 검증을 할 수 있다는 이야기를 들었다.

 

 ?? 안 뜨네?

안 뜬다? => 의존 주입이 안 됐다.

구글링 해보니 @Pattern에 대한 정의는 ' spring-boot-starter-validation ' 에 돼 있다고 한다.

그래서 추가해 보았다.

// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-validation
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-validation', version: '2.7.6'

좋아. 좋아. 옳게 됐다.

 

validation 라이브러리의 주된 목적이 '확인(검증)' 이라는 것은 라이브러리 이름만 봐도 알겠다.

 

.. 지금 내 목적은 값에 대한 검증이다.

그 부분에 대해서만 봐 보자.

 

@Valid 를 이용한 유효성 검증

@Valid 는 '빈 검증기(Bean Validator)를 이용해 객체의 제약 조건을 검증하도록 지시하는 어노테이션'이다.

빈 검증 기술의 특징은 객체의 필드에 달린 @어노테이션 으로 편리하게 검증을 할 수 있다는 것.

 

@Getter
@RequiredArgsConstructor
public class AddUserRequest {

	@Email
	private final String email;

	@NotBlank
	private final String pw;

	@NotNull
	private final UserRole userRole;

	@Min(12)
	private final int age;

}

위의 내용과 같이 필드에 어노테이션을 달아놓고~

@Controller
public class Controller {

    @PostMapping("/user/add") 
    public ResponseEntity<Void> addUser(@RequestBody @Valid AddUserRequest addUserRequest) {
          ...
	}

}

컨트롤러에서 @Vaild 어노테이션을 작성하면~

유효성 검증이 진행된다.

 

...?

찝찝..

왜 하필 Controller 에 작성해야할까?

클라이언트로부터 오는 모든 요청은 'Front Controller' 인 '디스패처 서블릿' 을 통해 Controller로 전달된다.

전달 과정에서 Controller Method의 객체를 만들어주는 ArgumentResolver 가 동작하는데, 검증을 위한 @Valid 가 ArgumentResolver에 의해 처리 된다고 한다.

 

대표적으로 @RequestBody 는 JSON 메세지를 객체로 변환해주는 작업이

ArgumentResolver의 구현체인 RequestResponseBodyMethodProcessor 가 처리하며,

이 내부에서 @Valid로 시작하는 어노테이션이 있을 경우 유효성 검사를 진행한다.

 

검증 중, 오류가 있다면 MethodArgumentNotValidException (메서드 인자 검증 실패 예외(가 발생한다.

이 때, 디스패처 서블릿(Front Controller) 에 기본적으로 등록된 예외 리졸버(Exception Resolver) 인 DefaultHandlerExceptionResolver 에 의해 400 BadRequest 가 발생한다.

400 에러 뜬다는 내용임.

org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public org.springframework.http.ResponseEntity<java.lang.Void> com.example.testing.validator.UserController.addUser(com.example.testing.validator.AddUserRequest) with 2 errors: [Field error in object 'addUserRequest' on field 'email': rejected value [asdfad]; codes [Email.addUserRequest.email,Email.email,Email.java.lang.String,Email]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [addUserRequest.email,email]; arguments []; default message [email],[Ljavax.validation.constraints.Pattern$Flag;@18c5ad90,.*]; default message [올바른 형식의 이메일 주소여야 합니다]] [Field error in object 'addUserRequest' on field 'age': rejected value [5]; codes [Min.addUserRequest.age,Min.age,Min.int,Min]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [addUserRequest.age,age]; arguments []; default message [age],12]; default message [12 이상이어야 합니다]] 
	at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.resolveArgument(RequestResponseBodyMethodProcessor.java:141) ~[spring-webmvc-5.3.15.jar:5.3.15]
	at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:122) ~[spring-web-5.3.15.jar:5.3.15]

이런 구조와 흐름이 있어 @Valid 는 기본적으로 'Controller' 에서 동작하며, 다른 계층에서는 검증이 되지 않는다.

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

 

 

@Validated

입력 파라미터의 유효성 검증은 Controller에서 최대한 처리하고 넘겨주는 것이 좋다. (왜?)

하지만, 개발을 하다보면 불가피하게 다른 곳에서 파라미터를 검증해야 할 수 있다.

Spring은 이를 위해 AOP 기반으로 메서드의 요청을 가로채서 유효성 검증을 진행해주는 @Validated 를 제공한다.

 

@Validated 는 Spring 프레임워크에서 제공하는 어노테이션 및 기능이다.

@Service
@Validated
public class UserService {

	public void addUser(@Valid AddUserRequest addUserRequest) {
		...
	}
}

이렇게 Class 에 @Validated를 붙여주고, 유효성을 검증할 Method 의 Parameter에 @Valid 를 붙여주면 유효성 검증이 진행된다.

 

유효성 검증에 실패하면 에러가 발생하는데,

@Valid 때의 MethodArgumentNotValidException 가 아닌, 

ConstraintViolationException 예외가 발생한다.

javax.validation.ConstraintViolationException: getQuizList.category: 널이어서는 안됩니다 
    at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:120) ~[spring-context-5.3.14.jar:5.3.14] 
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.14.jar:5.3.14] 
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:753) ~[spring-aop-5.3.14.jar:5.3.14] 
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:698) ~[spring-aop-5.3.14.jar:5.3.14] 
    at com.mangkyu.employment.interview.app.quiz.controller.QuizController$$EnhancerBySpringCGLIB$$b23fe1de.getQuizList(<generated>) ~[main/:na] 
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~

이는 @Valid 와 @Validated 의 동작 원리가 다르기 때문이다. 

 

@Validated 동작원리

특정 ArgumentResolver 에 의해 유효성 검사가 진행되던 @Valid 와 달리,

@Validated 는 AOP 기반으로 메소드 요청을 인터셉트하여 처리한다.

@Validated 를 Class 레벨에 선언하면 해당 Class에 유효성 검증을 위한 AOP의 어드바이스 또는 인터셉터(MethodValidationInterceptor)가 등록된다. (advice? 는 뭐지? 인터셉터가 어디에 등록된다는 거지?)

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

 

이러한 이유로 @Validated를 사용하면 Controller, Service, Repository 등 계층에 무관하게 '스프링 빈이라면 유효성 검증을 진행'할 수 있게 된다.

 

단, 클래스에는 유효성 검증 AOP가 적용되도록 @Validated를, 검증을 진행할 메소드에는 @Valid 를 선언해주어야 한다.

 

따라서, @Valid 에 의한 예외는 MethodArgumentNotValidException(메서드 인자가 검증 조건 통과 못함) 이고, @Validated 에 의한 예외는 ConstraintViolationException(제약 조건 위반) 이다. 

 

제약조건 어노테이션

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

 

728x90

댓글