Java 와 Spring 의 Validation

Junghoon Song
12 min readAug 18, 2018

--

이 글은 Bean Validation 과 그 구현체인 Hibernate Validator 그리고 Spring Validation 에 대해서 다루고 있습니다.

Java Bean Validation

출처 Hibernate Validator 6.0.11.Final — JSR 380 Reference Implementation: Reference Guide

일반적으로 데이터 검증 (Validation) 은 여러 계층에 걸쳐서 이루어지게 됩니다.
거의 동일한 내용의 검증로직이 각 계층별로 구현된다면 그것은 중복이고 낭비가 심한 작업일것입니다.
또한 그러한 경우 각 계층별로 구현된 검증로직간 불일치로 인하여 오류가 발생하기도 쉽습니다.

출처 Hibernate Validator 6.0.11.Final — JSR 380 Reference Implementation: Reference Guide

이를 해결하기 위하여 데이터 검증을 위한 로직을 도메인 모델 자체에 묶어서 표현하는 방법이 있습니다.
실제 코드로 표현된다면 너무 장황하고 복잡할것이기 때문에, Java 에서는 Bean Validation 라는 이름으로 애노테이션 (Annotation) 을 데이터 검증을 위한 메타데이터로 사용하는 방법을 제시하고 있습니다.

Bean Validation 명세는 현재 2.0 (JSR 380) 까지 나와있습니다.

Hibernate Validator

Hibernate Validator 는 Bean Validation 명세에 대한 구현체입니다.

Bean Validation 2.0 에 대한 구현은 Hibernate Validator 6.0.1.Final 이며 Spring Boot 2.0 이상에서 이것을 사용하고 있습니다.

제약조건의 작성

위에서 언급했던것처럼 Bean Validation 은 애노테이션을 사용하여 제약조건을 명시하게 됩니다.
즉 아래와 같은 모습이 됩니다.

위 제약조건에 의하면 name 은 null 이거나 빈문자열이어서는 안되며, age 는 0 이상이어야만 합니다.

Java 에서 기본적으로 제공해주는 제약조건들이 있으며, Hibernate 에서 추가적으로 제공하는 제약조건이나 필요한 경우 직접 제약조건을 만드는것도 가능합니다.

필드, 클래스, 메소드 또는 파라메터에 애노테이션을 작성하는게 가능하며, 위치에 따라 제약조건의 적용 범위가 달라지게 됩니다.

Java 와 Hibernate 에서 제공하는 제약조건들은 Hibernate Validator 6.0.12.Final#builtin-constraints 에서 확인하실 수 있습니다.

제약조건에 대한 유효성검증

제약조건의 확인은 javax.validation.Validator 를 사용해서 이루어집니다.

가장 쉽고 빠르게 Validator 를 가져오는 방법은 ValidatorFactory 를 사용하는 방법입니다.

그리고 validate() 를 사용해서 빈의 유효성검증을 실행합니다.
제약조건을 위반한 내용은 ConstraintViolation 인터페이스로 반환됩니다.

제약조건 위반내용에 대한 확인

위반내용은 아래와 같이 ConstraintViolation 인터페이스에 포함되어 있습니다.

위에 언급했던 Person 을 사용한 예제는 아래와 같습니다.

그룹핑된 제약조건

Bean Validation 1.1 (JSR-349) 부터 포함된 내용으로, 그룹을 사용하여 유효성검증을 위한 세트를 제한할 수 있습니다.

그룹에 해당하는 인터페이스 (혹은 클래스) 를 생성하고, 해당 그룹세트에 해당하는 제약조건 애노테이션의 groups 에 인터페이스를 넣으면 됩니다.

위 코드는 PersonGroups.Driver 그룹세트에 포함된 경우, age 가 18 이상이어야 하고 driverLicenseNumber 가 있어야 한다는 제약조건을 명시하고 있습니다.

제약조건은 복수의 그룹세트에 포함될 수 있습니다.

그리고 name 에 선언된 @NotEmpty 또는 age 에 선언된 @Positive 와 같이 groups 가 지정되지 않은 제약조건의 경우 javax.validation.groups.Default 그룹세트에 포함되어 동작하게됩니다.

특정 그룹세트에 위반되는 사항들을 확인하기 위해서는 validate()PersonGroups.Driver 그룹클래스를 추가적으로 전달하면 됩니다.

하지만 대부분의 경우는 javax.validation.groups.Default 에 해당하는 제약조건들도 같이 확인하기를 원할것입니다.

따라서 javax.validation.groups.DefaultPersonGroups.Driver 2개 그룹세트를 넣어주는 방식으로 사용하시면 됩니다.

매번 javax.validation.groups.Default 그룹세트를 추가하는것이 번거롭다면, PersonGroups.Driverjavax.validation.groups.Default 를 확장클래스로 만들면 됩니다.

그렇다면 validate()PersonGroups.Driver동 만을 넘겨주더라도 javax.validation.groups.Default 포함하여 유효성검증을 하게 됩니다.

필드에 대한 제약조건과 속성에 대한 제약조건

유효성검사에서 필드와 속성에 대한 제약조건을 구분해서 생각할 필요성이 있습니다.

위와 같은 코드에서 1번 제약조건의 경우는 필드에 대한 제약조건입니다. getName() 이라는 name 필드에 대한 접근자가 있지만 무시되며 “송정훈” 이라는 값에 대해서 유효성검사가 진행되게 됩니다.

2번 제약조건은 속성에 대한 제약조건입니다. “이름은 송정훈” 이라는 값에 대해서 유효성검사가 진행됩니다.

getXXX 또는 isXXX 와 같은 이름의 응답값을 가진 메소드들이 속성에 속하게되고, 빈에 대한 유효성검사를 진행하면 속성들에 대해서도 검사가 진행이 되게 됩니다.

계단식 유효성검사 (cascaded validation)

계단식 유효성검사를 위해서는 해당되는 필드에 @Valid 를 추가해 주셔야 합니다.

위 예제에서는 driver 필드에 @Valid 애노테이션이 선언되어 있습니다.

따라서 driver 객체에 대한 유효성검사도 이뤄지게 됩니다.

컨테이너 요소에 대한 유효성검사

Iterable (Set, List 등), Map, Optional 과 같은 컨테이너형의 요소들에 대한 유효성검사가 Bean Validation 2.0 (JSR-380) 에 포함되었습니다.

유효성검사를 진행하기 위한 요소 앞에 애노테이션을 추가하면 되는데, 위에 언급한 Car 에서 여러명의 (하나 이상의) 운전자가 있어야 한다면 아래와 작성하게 됩니다.

그룹전환

우리는 Car 에 대해서 유효성검사를 진행할때 drivers 는 PersonGroups.Driver 그룹세트로 유효성검사가 이루어지기를 바라겠지만, 아쉽게도 Car 와 동일하게 javax.validation.groups.Default 에 대한유효성검사만 이뤄지게 됩니다.

이런경우에 @ConvertGroup 을 사용하여 그룹전환을 제약조건에 추가하는것이 유용합니다.

참고로 @ConvertGroup 에서 from 은 아무것도 적지 않으면 javax.validation.groups.Default 를 가지게 됩니다.

따라서 위의 경우 from 을 생략하셔도 됩니다.

매개변수에 대한 유효성검사

메소드의 매개변수 또는 응답값에 대한 유효성검사도 가능합니다.

먼저 매개변수에 대한 제약조건은 아래와 같이 작성하면 됩니다.

매개변수에 대한 유효성검사는 ExecutableValidator 를 사용하게 됩니다.

응답값에 대한 유효성검사

메소드에 응답값에 대한 유효성검사는 아래와 같이 가능합니다.

위에서 나온 속성에 대한 유효성검사와는 다르게, 이 경우에는 ExecutableValidator를 사용하게 됩니다.

복수의 매개변수에 대한 유효성검사

매개변수 하나하나가 아닌, 여러 매개변수에 대한 유효성검증이 필요할 경우가 있습니다.

이럴때는 사용자정의 애노테이션과 ConstraintValidator 를 만드는 방법으로 해결이 가능합니다.

update 메소드는 18세 미만의 경우 운전면허번호는 null 만 허용해야 합니다. 즉 2개의 매개변수를 같이 검증해야 합니다.

우선 검증을 위한 사용자정의 애노테이션을 추가하였습니다.

이 제약조건에 대한 유효성을 검증하기 위하여 PersonUpdateValidator 를 사용하도록 되어있습니다.

이 검사기는 age (첫번째 매개변수) 가 18 이상인경우 유효하지만, 미만인 경우에는 운전면허번호 (두번째 매개변수) 가 null 이어야만 유효한것으로 판단합니다.

이 사용자정의 ConstraintValidator@SupportedValidationTargetValidationTarget.PARAMETERS 로 하여 매개변수에 대한 유효성검증기임을 나타내고 있습니다.

제약조건의 합성

여러 제약조건 애노테이션을 합성한 애노테이션을 만드는것도 가능합니다.

boolean-constraint-composition 를 참고하시기 바랍니다.

스크립트를 사용한 유효성검증

Hibernate 에서 제공하는 @ScriptAssert 와 같은것을 사용하여, 스크립트를 사용한 유효성검증도 가능합니다.

예를든다면 18세 미만인 경우 운전면허번호가 null 이어야 하는 제약조건은 아래와 같이 작성하는게 가능합니다.

Spring Validation

Spring 에서는 Java Bean Validation 을 완벽하게 지원하면서, 검사기를 직접 사용하지 않고 AOP 와 같은 방식으로 더 편리하게 유효성검사를 할 수 있는 장치들을 제공해주고 있습니다.

그것들에 대해 알아보도록 하겠습니다.

Validated

Bean Validation 에서 @Valid 를 사용하여 유효성검사가 진행될 대상을 지정하고는 하였습니다.

하지만 이것은 어떤 그룹세트에서 유효성검사가 일어날지 표현하기 적합하지는 않습니다.

Spring 에서는 Validator.validate() 를 사용하여 유효성검사를 진행하기보다는 AOP 와 같은 방법을 사용하므로 진입점이 되는 애노테이션에 그룹세트를 명시적으로 지정해야 합니다.

따라서 스프링에서는 유효성검사에 진입하게되는 지점에 @Validated 라는 애노테이션을 사용하는 방법을 제공하고 있습니다.

Spring 에서 메소드에 대한 유효성검사

AOP 를 사용하여 메소드를 실행할때 유효성검사를 실행하는것이 가능합니다.

MethodValidationPostProcessor 를 빈을 정의하고, 필요한 클래스 혹은 메소드에 @Validated 애노테이션을 추가하면 됩니다.

만약 제약조건에 위반되는 내용이 발견된다면 ConstraintViolationException 이 발생하게 됩니다.

SpringBoot 의 ValidationAutoConfiguration 를 살펴보면 이미 MethodValidationPostProcessor 빈이 정의되어 있다는것을 알 수 있습니다.

Spring Validator

Spring 내부에서 사용하는 독자적인 Validator 인터페이스가 있습니다.

org.springframework.validation.Validator 로서 아래와 같은 코드를 가지고 있습니다.

기본적으로 Spring 에서는 Java Bean Validation 을 지원하는 완벽하게 지원하는 LocalValidatorFactoryBeanSpringValidatorAdapter 을 제공하고 있습니다.

SpringValidatorAdapterorg.springframework.validation.Validator 이외에도 javax.validation.ValidatorFactory, javax.validation.Validator 의 구현체로 동작합니다.

SpringBoot 의 ValidationAutoConfiguration 를 살펴보면 LocalValidatorFactoryBean 가 defaultValidator 로 동작한다는것을 알 수 있습니다.

Spring MVC 에서 유효성검사

Spring MVC 에서 Data Binding 시점에 유효성검사를 실행하게 됩니다.

이 부분은 WebMvcConfigurationSupport 또는 WebFluxConfigurationSupport 를 보시면 좀 더 자세하게 아실 수 있습니다. ConfigurableWebBindingInitializer 를 생성할때 Validator 를 주입하는걸 확인하실 수 있습니다.

바인딩에 실패한경우 org.springframework.web.bind.MethodArgumentNotValidException 이 발생하게 되는데, 만약 필요하다면 이 예외에 대한 ExceptionHandler 를 만들어서 처리하는것이 가능합니다.

ExceptionHandler 를 사용하지 않고 직접 BindingResult 를 받아서 처리하는것도 가능합니다.

사용자정의 Validator 를 사용하여 처리하는것도 가능합니다.

org.springframework.validation.Validator 를 확장한 Validator 를 만들어서 아래와 같이 추가해주도록 합니다.

여기까지 Java Bean Validation 과 Spring Validation 을 사용한 유효성검사에 대해서 간단하게 알아보았습니다.

이 글은 아래 자료들을 참고하였습니다.

--

--