본문 바로가기
IT 공부/자바와 웹 애플리케이션

[스프링] - 스프링이 Java Bean이 유효성을 검증하는 방법

by exdus3156 2024. 1. 8.

※ 이 포스팅은 스프링 프레임워크가 어떻게 @Valid 같은 어노테이션을 체크하고 실행하는지 그 원리를 공부한 것을 정리한 포스팅입니다. 저에게 어려운 주제라 오류가 있을 수 있습니다.

 

1. 유효성 검증은 JavaEE 스펙.

스프링 프레임워크는 자동으로 @Valid와 같은 어노테이션을 식별하고, 관련 데이터의 각종 유효성들을 검증해준다. 그리고 그 결과를 BindingResult 객체에 정보를 저장하고 컨트롤러에게 넘겨준다. 

이렇게 이야기를 듣고 기술을 쓰면은 마치 유효성 검증이 스프링의 특수한 기능처럼 들린다. 그러나 파고 들더가면 오히려 스프링이 유효성 검증 스펙을 사용하는 입장일 뿐이라는 것이 드러난다. 즉, 유효성 검사와 관련된 API와 기능들은 스프링에 종속된 개념들이 아니다.

우선, 유효성 검증(validation) 자체는 프로그래밍 언어에 상관없이 모든 소프트웨어가 공유하고 있는 문제 의식이다. 프로그래밍 오류에는 여러 가지 원인이 있지만, 알고리즘이 정상이여도 오류가 발생할 수 있는 원인이 바로 잘못된 데이터 입력이다. 입력 데이터에 대한 잘못된 가정으로 인해 프로그램의 로직의 빈틈이 들키는 것이다.

따라서 외부에서 데이터가 오고갈 때는 항상 데이터가 유효한지 체크해야 한다. 외부라는 것은 하나의 모듈 경계 바깥이다. 

문제는 소프트웨어가 복잡할수록 모듈 경계는 많아지기 때문에 위 그림처럼 데이터가 경계를 오고갈 때마다 계속 유효성을 검증해야 한다는 단점이 있다는 것이다.

이를 해결하기 위해 JavaEE 표준 스펙에서는 통합 유효성 검증을 위한 API 표준을 마련했으며, 이것은 JavaBeanValidation이라고 불린다. 이것은 위 그림처럼 각 레이어가 유효성을 검증하는 로직을 내부로 포함하는 것이 아니라, 외부에서 일관적으로 데이터를 검증하는 방식이다. 단, 이름에서도 알 수 있듯이 유효성 검증 대상은 그냥 객체가 아니라 Java Bean 이어야 한다.

중요한 것은 이 기술이 웹에 종속되지 않는다는 점을 캐치해야 한다. JavaBeanValidation은 웹뿐만 아니라 자바 애플리케이션 전체에 대해 적용할 수 있다. 그래서 JavaEE 표준 스펙이다. 아래는 스프링 공식 문서의 설명이다.

 

"Specifically, validation should not be tied to the web tier and should be easy to localize, and it should be possible to plug in any available validator."

 

중요한 점은 JavaBeanValidation(현재는 JakartaBeanValidation이라고 부름, 하지만 보통 BeanValidation으로 짧게 말함)은 어디까지나 스펙일 뿐이라는 점이다. 구체적인 구현과는 별개다.

 

 


2. 스프링은 JavaBeanValidation을 지원한다.

스프링은 기본적으로 스프링의 유효성은 JavaEE(JakartaEE) 스펙에 의존한다. 즉, 스프링은 JavaEE의 JavaBeanValidation 스펙의 API를 지원해준다.

"The Spring Framework provides support for the Java Bean Validation API."
from spring doc

 

실제로 @Valid, @NotEmpty, @Future 등 유효성 검증을 위한 어노테이션 코드의 패키지를 보면 스프링이 아니라 javax 패키지다. (버전에 따라 jakarta일 수도 있다)

스프링 웹 MVC도 위 어노테이션을 사용한다. 스프링 웹 MVC는 내부적으로 JavaBeanValidation API를 포함하고 있기 때문이다.

그러나 API에 의존하는 것이지, 실질적인 구현 클래스에 의존하지는 않는다. 원래 BeanValidation과 관련된 어노테이션들을 검증하고 그 결과를 판단하는 로직은 validator 객체가 수행한다. 따라서 만약 javax.Validation.Validator 구현 객체를 배포하지 않으면 스프링 웹 MVC는 유효성 검증을 하지 않는 것으로 실험 확인되었다.

실제로 위 코드는 아주 간단한 POST 요청으로 들어오는 외부 데이터에 대해 @Valid로 유효성을 검증하도록 만들고 있다.

하지만 위와 같이 BeanValidation API는 포함했으나 구현 클래스(Hibernate-Validator)는 일부러 빼보면 어떻게 될까? 컴파일과 빌드는 성공적으로 완료된다. 성공할 수 있는 이유는 위에서 말한대로, 스프링은 BeanValidation의 API에만 의존하므로 컴파일 완료를 위해 오직 필요한 것은 API이기 때문이다. 

이렇게 톰캣을 실행해보면...

오류가 뜬다. 잘못된 데이터를 보내 컨트롤러가 로직을 수행하다가 오류를 일으킨 것이다. 정확한 원인은 NULL 값이 와서는 안 되는 DB의 칼럼에 null 데이터를 넣다가 오류가 뜬 것이다. 즉, 이 실험에서 알 수 있는 것은 스프링은 Validator 구현 객체가 없으면 검증을 하지 못한다는 점이다. 아무리 JavaEE의 BeanValidation 명세에 의존하여 관련 어노테이션을 주렁주렁 달아봤자 그 어노테이션을 사용하는 클래스가 없다면 무슨 의미겠는가?

그렇다면 스프링 웹 MVC의 내부적인 validation support에 대해 자세히 알아보자. 핵심 목표는 어떻게 Validator를 스프링이 사용하게 만드는가 이다.

 

 


3. 스프링 웹 MVC가 BeanValidation을 지원하는 방식

스프링 웹 MVC 프로젝트에서 실질적인 검증 로직을 사용하기 위해서는 Validator 구현체가 필요하다. 스프링 웹MVC는 아래와 같은 방법으로 Validator 구현체를 사용한다. 내가 실험한 스프링 버전이 5.3.29이기 때문에 해당 버전의 공식 문서를 확인해보았다. (그림 클릭 시 링크 이동)

 

Hibernate Validator와 같은 구현체가 클래스패스에 존재한다면, 그것을 LocalValidatorFactoryBean을 통해 Validator를 자동 사용한다고 나와 있다. 사용이 아주 간단한 편이다. 즉, 개발자는 이런저런 설정을 전부 생략하고 그저 Hibernate Validator 라이브러리를 포함하여 클래스패스에 두는 것만으로 유효성 검증을 스프링 웹 MVC가 해준다는 것이다!

그렇다면 LocalValidatorFactoryBean 객체는 정체가 무엇일까? 우선 공식 문서를 보면, LocalValidatorFactoryBean은 javax.validation.ValidationFactory를 생성하고, 이 팩토리가 Validator 구현체를 생성하고 주입하여 사용한다고 나와있다.

" This is the central class for javax.validation (JSR-303) setup in a Spring application context: It bootstraps a javax.validation.ValidationFactory and exposes it through the Spring Validator interface as well as through the JSR-303 Validator interface and the ValidatorFactory interface itself. "

 

 

 


4. 버전에 대한 힌트

그렇다면 스프링 버전에 맞춰 도대체 어떤 JavaBeanValidation 버전을 따라야 하며, 또한 그 구현체는 어떤 버전이 해당 BeanValidation 버전과 일치하는지 맞춰야 할 것이다.

이 힌트는 LocalValidatorFactoryBean 객체의 공식 문서 설명에 나와 있다. 

" As of Spring 5.0, this class requires Bean Validation 1.1+, with special support for Hibernate Validator 5.x (see setValidationMessageSource(org.springframework.context.MessageSource)). This class is also runtime-compatible with Bean Validation 2.0 and Hibernate Validator 6.0, with one special note: If you'd like to call BV 2.0's getClockProvider() method, obtain the native ValidatorFactory through #unwrap(ValidatorFactory.class) and call the getClockProvider() method on the returned native reference there. "

 

전부 이해할 수는 없지만, 적어도 내가 실험하는 환경인 스프링5 버전에서는 이 클래스는 BeanValidation은 최소 1.1+ 이어야 하고, 구현체는 Hibernate Validator의 5.x 버전이어야 한다고 나와 있다. 그리고 런타임 환경에서는 Bean Validation 2.0 & Hibernate Validator 6.0 이면 된다고 나온다.

우리는 이 클래스를 런타임에서 사용하는 입장이기 때문에 아래와 같이 Hibernate-Validator 6.2.1 버전을 사용해도 충분했다.

재밌는 점은, 아래의 JavaBeanValidation API를 포함할 필요가 없다는 점이다. Hibernate Validator 라이브러리를 그레이들에서 다운받으면 자동으로 이 객체가 의존하고 있는 다른 라이브러리도 불러와주는데, Validator가 애초에 JavaBeanValidation 스펙의 구현체이므로 당연한 결과라 할 수 있다. 따라서 굳이 BeanValidation API를 포함할 이유가 전혀 없다.

 

 


무언가 내부적으로 복잡한 과정을 거치는 것 같다. 나로선 모든 것을 이해할 수는 없지만, 중요한 점은 공식 문서를 통해 validator를 등록하는 방법이 매우 쉽다는 것, 유효성 검증이 스프링 자체가 아니라 JavaEE의 JavaBeanValidation 스펙을 따른다는 것, 그리고 스프링 웹 MVC는 명세에만 따르기 때문에 반드시 구현체를 클래스패스에 등록해야만 비로소 사용할 수 있다는 점을 알게 되었다.