JavaBean Validation과 Hibernate Validator 그리고 Spring Boot

이 글은 JavaBean Validation(이하 Bean Validation)의 기본 개념과 Hibernate Validator와의 관계 그리고 Spring Boot에서 간단한 사용법을 소개한다.

Bean Validation

Bean Validation은 JavaBean 유효성 검증을 위한 메타데이터 모델과 API에 대한 정의이며 여기서 언급하고 있는 JavaBean은 직렬화 가능하고 매개변수가 없는 생성자를 가지며, Getter 와 Setter Method를 사용하여 프로퍼티에 접근이 가능한 객체라고 위키피디아는 정의한다.

Bean Validation defines a metadata model and API for JavaBean validation - https://en.wikipedia.org/wiki/Bean_Validation JavaBeans are classes that encapsulate many objects into a single object (the bean). They are serializable, have a zero-argument constructor, and allow access to properties using getter and setter methods. The name "Bean" was given to encompass this standard, which aims to create reusable software components for Java. - https://en.wikipedia.org/wiki/JavaBeans

그렇다면  '메타데이터 모델'은 무엇일까?

힌트는 JSRJava Specification Requests 303 문서에서 찾을 수 있다.(Bean Validation은 최초에 JSR 303으로 제안되었다)

Validating data is a common task that occurs throughout an application, from the presentation layer to the persistence layer. Often the same validation logic is implemented in each layer, proving to be time consuming and error prone. To avoid duplication of these validations in each layer, developers often bundle validation logic directly into the domain model, cluttering domain classes with validation code that is, in fact, metadata about the class itself. This JSR defines a metadata model and API for JavaBean validation. The default metadata source is annotations, with the ability to override and extend the meta-data through the use of XML validation descriptors. - JSR 303

한 문장씩 살펴보자.

데이터 검증은 애플리케이션의 여러 계층(Presentation Layer, Business Layer, Data Access Layer)에 전반에 걸쳐 발생하는 흔한 작업이다. 종종 동일한 데이터 검증 로직이 각 계층에 구현되는데, 이는 오류를 일이 키기 쉽고, 시간을 낭비하는 일이 된다.

출처 : https://docs.jboss.org/hibernate/validator/5.4/reference/en-US/html_single/

출처 : https://docs.jboss.org/hibernate/validator/5.4/reference/en-US/html_single/

개발자는 이런 것을  피하기 위해 도메인 클래스들 사이에 퍼져 있는 유효성 검증 로직(실제로 클래스 자신의 메타데이터)을 도메인 모델로 묶는다.

출처 : https://docs.jboss.org/hibernate/validator/5.4/reference/en-US/html_single/

출처 : https://docs.jboss.org/hibernate/validator/5.4/reference/en-US/html_single/

위의 내용을 종합하여 필자가 이해한 데로 풀어 보면

  • 애플리케이션을 개발할 때 데이터를 각 계층으로 전달하게 된다. 이러한 데이터는 JavaBean의 형태를 띠게 된다.
  • 데이터 유효성 검증을 위해 사용하게 되는 것이 데이터에 대한 데이터 즉 어떤 목적을 가지고 만들어진 데이터(Constructed data with a purpose) 바로 메타데이터이다.[1]
  • Java에서는 메타데이터를 표현할 수 있는 대표적인 방법이 애노테이션Annotation이다.
  • 애노테이션(기본적으로는)을 이용하여 메타데이터를 정의하고 이를 통해 JavaBean의 유효성을 검증하는 것에 대한 명세가 Bean Validation이다.

Bean Validation 명세서Specification는 계속 발전하여 현재 2.0까지 나와 있다.

출처 : http://beanvalidation.org/

Bean Validation 1.0 JSR 303
Bean Validation 1.1 JSR 349

Bean Validation 1.1 focused on the following topics:

  • openness of the specification and its process
  • method-level validation (validation of parameters or return values)
  • dependency injection for Bean Validation components
  • integration with Context and Dependency Injection (CDI)
  • group conversion
  • error message interpolation using EL expressions

Bean Validation 2.0 JSR 380

Bean Validation 2.0 focused on the following topics:

  • support for validating container elements by annotating type arguments of parameterized types e.g. List<@Positive Integer> positiveNumbers. This also includes:
    • more flexible cascaded validation of container types
    • support for java.util.Optional
    • support for the property types declared by JavaFX
    • support for custom container types
  • support for the new date/time data types (JSR 310) for @Past and @Future
  • new built-in constraints: @Email@NotEmpty@NotBlank@Positive@PositiveOrZero@Negative@NegativeOrZero@PastOrPresent and@FutureOrPresent
  • leverage the JDK 8 new features (built-in constraints are marked repeatable, parameter names are retrieved via reflection)

Bean Validation과 Hibernate Validator

Bean Validation은 명세일 뿐 동작하는 코드가 아니다. 애플리케이션에 적용하기 위해서는 이를 구현한 코드가 필요하다.

Bean Validation을 실제 동작하도록 구현Implementation한 것이 바로 Hibernate Validator[2]이다.

image2017-12-28_11-19-17

Bean Validation 버전에 따른 Hibernate Validator 버전은 아래와 같다.

출처 : http://beanvalidation.org/

Bean Validation

Hibernate Validator
1.0(JSR 303) Hibernate Validator 4.3.1.Final
1.1(JSR 349) Hibernate Validator 5.1.1.Final
2.0(JSR 380) Hibernate Validator 6.0.1.Final

Bean Validation과 Spring Boot

Spring Boot(1.5.9 RELEASE 기준)는 Bean Validation을 Validation 모듈로 제공하고 있으며, Web 모듈에는 Validation 모듈을 포함하고 있다.

Spring Boot Web 모듈(spring-boot-starter-web-1.5.9.RELEASE.pom)을 자세히 살펴보면 Hibernate Validator(5.3.6.Final)를 사용하는 것을 확인할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <artifactId>spring-boot-starter-web</artifactId>
    <name>Spring Boot Web Starter</name>
    ...
    <dependencies>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-validator</artifactId>
        </dependency>
        ...
    </dependencies>
</project>
1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-dependencies</artifactId>
    <version>1.5.9.RELEASE</version>
    <packaging>pom</packaging>
    ...
    <properties>
        <hibernate-validator.version>5.3.6.Final</hibernate-validator.version>
    </properties>
    ...
</project>

정리하면 Spring Boot 버전에 따라 다르겠지만 1.5.9 RELEASE 기준으로 보면 BeanValidation의 구현체로 Hibernate Validator를 사용하고 Hibernate Validator 버전이 5.3.6 Final이기 때문에 BeanValidation 1.1을 지원한다.

Spring Boot + Hibernate Validator 맛보기

Hibernate Validator의 여러 쓰임새 중에서도 이 글에서는 REST 개발할 때 HTTP Reqeust Body 검증에 초점을 맞춰 소개한다.

검증 로직이 없는 코드부터 시작해 보자.

1
2
3
4
5
6
7
8
9
@RestController
@RequestMapping("/api/v1/members")
public class MemberRestController {
    @PostMapping
    public ResponseEntity<Void> registerMember(@RequestBody final MemberRegistration registration) {
        // ...
        return new ResponseEntity<>(HttpStatus.OK);
    }
}
1
2
3
4
5
6
7
public class MemberRegistration {
    private String name;
    private String password;
    private String email;
    private String phoneNumber;
    // getter, setter
}

지금 코드에는 아무런 검증 로직이 없기 때문에 Request Body에 빈 값을 넣어 요청해도 HTTP Response Code를 200(OK)을 반환한다.

image2018-1-4_21-37-52

이제 하나씩 검증 로직을 추가해 보자.

Validation 1 - 'name'은 필수이어야 한다.

name 필드에 @NotEmpty 붙이고 검증 로직이 통과하지 못했을 때 메시지를 기술해 준다.

1
2
3
4
5
6
7
import org.hibernate.validator.constraints.NotEmpty;
public class MemberRegistration {
    @NotEmpty(message = "이름은 필수 입니다")
    private String name;
    // ...
    // getter, setter
}

마지막으로 Validation을 실행하는 Trigger[3]로써 @Valid를 MemberRegistration에 붙여준다.

1
2
3
4
5
6
7
8
9
import javax.validation.Valid;
@RestController
@RequestMapping("/api/v1/members")
public class MemberRestController {
    @PostMapping
    public ResponseEntity<Void> registerMember(@Valid @RequestBody final MemberRegistration registration) {
        // ...
    }
}

이전과 동일하게 HTTP Request Body 빈 값으로 REST API를 호출해 보자.

HTTP Response Code로 400(Bad Request)로 그리고 ResponseBody로 @NotEmpty message에 기술했던 부분을 반환한다.

image2018-1-4_21-33-31

Validation 2 - 'password'는 필수이면서 6~20자리로 숫자와 특수 문자가 포함된 영문 대소문자로 문자열이어야 한다

password 필드에 @NotEmpty을 붙이고, @Pattern을 사용하여 비밀번호 제약 조건을 정규식으로 기술해 준다.

1
2
3
4
5
6
7
8
9
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
public class MemberRegistration {
    // ...
    @NotEmpty(message = "비밀번호는 필수 입니다")
    @Pattern(regexp = "((?=.*\\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%]).{6,20})", message = "비밀 번호는 6~20자리로 숫자와 특수 문자가 포함된 영문 대소문자로 입력해 주세요")
    private String password;
    // getter, setter
}

Request Body의 password 가 빈값이 아니면 정규식으로 제약 조건을 검증 후 통과하지 못 하면 일전과 동일하게 HTTP Response Code로 400(Bad Request)로 그리고 ResponseBody로 @NotEmpty message에 기술했던 부분을 반환한다.

image2018-1-4_21-49-34

Validation 3 - 'email' 또는 'phoneNumber' 둘 중 하나는 필수이어야 한다

@ScriptAssert를 사용하면 필드 수준이 아닌 클래스 수준에서 검증이 가능하다.

1
2
3
4
5
6
7
8
9
10
import org.hibernate.validator.constraints.ScriptAssert;
@ScriptAssert(lang = "javascript",
        script = "(_.email != null && _.email.length() > 0) || (_.phoneNumber != null && _.phoneNumber.length() > 0)",
        alias = "_", message = "이메일 혹은 전화 번호 둘 중 하나는 필수 입니다")
public class MemberRegistration {
    // ...
    private String email;
    private String phoneNumber;
    // getter, setter
}

Validation 4 - 'email'에 값이 있다면 email 형식에 따라야 한다

1
2
3
4
5
6
7
8
9
10
11
12
import org.hibernate.validator.constraints.ScriptAssert;
import org.hibernate.validator.constraints.Email;
@ScriptAssert(lang = "javascript",
        script = "(_.email != null && _.email.length() > 0) || (_.phoneNumber != null && _.phoneNumber.length() > 0)",
        alias = "_", message = "이메일 혹은 전화 번호 둘 중 하나는 필수 입니다")
public class MemberRegistration {
    // ...
    @Email(message = "이메일 형식으로 입력해 주세요")
    private String email;
    private String phoneNumber;
    // getter, setter
}

Validation 5 - 'phoneNumber'에 값이 있다면 10자리 이하여야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import org.hibernate.validator.constraints.ScriptAssert;
import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.Length;
@ScriptAssert(lang = "javascript",
        script = "(_.email != null && _.email.length() > 0) || (_.phoneNumber != null && _.phoneNumber.length() > 0)",
        alias = "_", message = "이메일 혹은 전화 번호 둘 중 하나는 필수 입니다")
public class MemberRegistration {
    // ...
    @Email(message = "이메일 형식으로 입력해 주세요")
    private String email;
    @Length(min = 1, max = 10, message = "전화 번호는 10자리 이하로 입력해 주세요")
    private String phoneNumber;
    // getter, setter
}

GitHub

전체 코드는 필자의 GitHub에서 확인 가능하다.


주석

[1] https://ko.wikipedia.org/wiki/메타데이터

[2] Hibernate 이름에서 ORM을 떠올리는 경우가 많은데 ORM과는 별개이다.

[3] https://spring.io/blog/2009/11/17/spring-3-type-conversion-and-validation/

Validation It is common to validate a model after binding user input to it. Spring 3 provides support for declarative validation with JSR-303. This support is enabled automatically if a JSR-303 provider, such as Hibernate Validator, is present on your classpath. When enabled, you can trigger validation simply by annotating a Controller method parameter with the @Valid annotation


Popit은 페이스북 댓글만 사용하고 있습니다. 페이스북 로그인 후 글을 보시면 댓글이 나타납니다.