Spring

Spring Framework - Validation

728x90

참조한 강의 : Spring & Hibernate For Beginners (www.udemy.com/course/spring-hibernate-tutorial/)

 

 

- Validation

앞선 포스팅에서 까지는, 사용자의 입력을 받아서 컨트롤러와 모델을 이용해서 로직을 처리한뒤, 뷰를 통해 화면상에 보여주는 방법을 알아봤다. 

 

이번에는, 사용자가 입력을 할때, 입력값에 대해서 유효한 값인지를 검증하는 방법에 대해서 알아본다.

예를들면, 회원가입시에, 비밀번호는 몇자리가 가능하고, 어떤 부분은 필수로 넣어야 하는 값이고 등 이런것들을 스프링으로 어떻게 처리하는지 알아본다.

 

 

- Bean Validation

Bean Validation 이란 것은 스프링에서 Bean 에 대한 데이터 유효성 검증을 위한 메타데이터 모델과 API 에 대한 정의를 말한다.

JSR (Java Specification Requests) 이란게 있는데, 자바 플랫폼에 추가된 사양 및 기술들을 정의해놓은 공식 문서를 말하며, Bean Validation 은 JSR-303 에서 처음 등장하였다.

 

이것은 그냥 명세서처럼 기능들에 대해 추상적으로 정의를 해놓은 부분이지, 실제로 어떤 코드를 제공하거나 하는 것은 아니다.

그래서, 이것을 실제로 구현하기 위해서는, 이를 구현해놓은 코드를 사용해야하는데,

이를 구현해놓은 코드가 바로 Hibernate Validator 이다.

 

 

- Hibernate Validator

Hibernate Validator 는 현재 기준(2021-01-30) 7.0, 6.2, 6.0 버전을 제공하고 있다.

가장 최신 버전인 7.0 버전은 Jakarta EE 9 이란것을 사용하는데, 이는 Java EE 와는 다른것이고, Spring 5 는 아직 이 Jakarta EE 에 대한 지원을 하고 있지 않다. 따라서, Spring 5 에서 Hibernate Validator 를 사용하고자 한다면, 7.0 버전이 아니라, 6.2 버전을 사용해야한다.

 

이클립스 IDE 를 기준으로 Hibernate Validator 를 사용하기 위해서는, 먼저 아래의 사이트에 가서 6.2 버전에 대한 ZIP 파일을 받아와야 한다.

 

hibernate.org/validator/releases/6.2/

 

6.2 series - Hibernate Validator

Express validation rules in a standardized way using annotation-based constraints and benefit from transparent integration with a wide variety of frameworks.

hibernate.org

 

zip 파일을 받은뒤, 압축을 풀고 dist 폴더 안에 있는 3개의 jar 파일을 이클립스 프로젝트의 WEB-INF/lib 폴더에 복사해서 넣어주며

dist 폴더 안에 있는 3개의 jar 파일들

 

다음으로 압축을 푼 폴더의 dist/lib/required 에 있는 jar 파일들도 이클립스 프로젝트의 WEB-INF/lib 폴더에 복사해서 넣어준다.

dist/lib/required

 

여기까지하면, 이클립스에서 hibernate validator 를 사용할 준비가 되었다.

다음은, 몇가지 예제를 통해서, 실제로 직접 코드를 짜보자.

 

 

 

 

예제 1 - @NotNull, @Size, @InitBinder

아주 단순하게 고객의 이름 정보를 받고, 대신에 성(last name) 은 필수로 넣어야하며, 최소길이가 1 이상이어야 한다는 조건을 넣는 예제를 만들어보자

 

- Customer.java

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
 
public class Customer {
    
    private String firstName;
    
    @NotNull(message="is required")
    @Size(min=1, message="is required")
    private String lastName;
 
    public String getFirstName() {
        return firstName;
    }
 
    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }
 
    public String getLastName() {
        return lastName;
    }
 
    public void setLastName(String lastName) {
        this.lastName = lastName;
    }
    
}
cs

위 코드에서 사용한 두개의 어노테이션이 바로 hibernate validator API 에 정의된 어노테이션들 중 하나이다.

@NotNull 은 이름에서 알수있듯이 이 값이 null 이면 안된다는 조건을 넣은것이고,

@Size 는 이 값의 크기를 지정하는것이다. (min=1) 은 최소 1개 이상되어야 한다는 의미가 된다.

message 는 조건에 부합하지 않아서 오류가 났을때, 보여주는 메시지 이다.

 

 

 

- CustomerController.java

import javax.validation.Valid;
 
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
 
@Controller
@RequestMapping("/customer")
public class CustomerController {
    
    @RequestMapping("/showForm")
    public String showForm(Model theModel) {
        
        theModel.addAttribute("customer"new Customer());
        
        return "customer-form";
    }
    
    @RequestMapping("/processForm")
    public String processForm(@Valid @ModelAttribute("customer") Customer theCustomer, 
            BindingResult theBindingResult) {
        
        System.out.println("last name : |" + theCustomer.getLastName() + "|");
        
        if (theBindingResult.hasErrors()) {
            return "customer-form";
        } else {
            return "customer-confirmation";
        }
    }
}
cs

이전 포스팅들에서 작성했던, 컨트롤러와는 다른점은 @Valid, BindingResult 를 사용했다는 점이다.

 

가장 먼저, @Valid 는 해당 객체에 대한 유효성 검증을 실시하라는 어노테이션이다.

즉 여기서는 theCustomer 인스턴스에 대한 유효성 검증을 실시하라는 의미가 된다.

 

그리고 이 유효성 검증에 대한 결과물은 BindingResult 객체에 담기게 된다.

만약 검증시에 오류가 났다면, 멤버함수인 hasErrors() 가 true 를 리턴하고, 그렇지 않으면 false 를 리턴한다.

 

 

- customer-form.jsp

<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<!DOCTYPE html>
<html>
    <head>
        <title>Customer Registration Form</title>
        <style>
            .error {
                color: red;
            }
        </style>
    </head>
    <body>
        <form:form action="processForm" modelAttribute="customer">
            First Name : <form:input path="firstName" /> <br><br>
            Last Name (*) : <form:input path="lastName" />
            <form:errors path="lastName" cssClass="error" />
            <br><br>
            <input type="submit" value="Submit" />
        </form:form>
    </body>
</html>
cs

이전 포스팅과 다른점은, <form:errors> 를 사용했다는점이다.

이 태그는 오류시에 어떤 css 를 설정할지, 그리고 어떤 path 와 연결될지를 정의한다.

(cssClass 는 css 에서 클래스 이름과 일치해야한다)

 

 

- customer-confirmation.jsp

<!DOCTYPE html>
<html>
    <head>
        <title>Customer Confirmation Page</title>
    </head>
    <body>
        The Customer name is : ${customer.firstName} ${customer.lastName}
    </body>
</html>
cs

 

여기 까지 하면, 이름을 입력했을때, 그리고 이름을 입력하지 않았을때, 걸러지긴 하지만

한가지 문제가 있다.

 

그것은 lastName 부분을 그냥 공백으로 전부 채웠을때이다.

그냥 공백으로 채워서 넣어버려도 처리가된다.

공백은 엄연히 아스키코드값이 존재하는 값(32)이고 null (0) 과는 다른 아스키 코드 값을 갖기 때문이다.

 

 

이를 위한 해결책으로, StringTrimmerEditor 라는 스프링이 제공하는 API 를 쓸 수 있다.

이를 위해서는, 유효성 검증 과정을 거치기 전에 처리 작업이 필요한데, 이런 전처리 작업을 도와주는 어노테이션이 @InitBinder 이다.

 

컨트롤러 부분에 다음의 코드를 추가해준다.

 

- CustomerController.java

@InitBinder
public void initBinder(WebDataBinder dataBinder) {
    StringTrimmerEditor stringTrimmerEditor = new StringTrimmerEditor(true);    
 
    dataBinder.registerCustomEditor(String.class, stringTrimmerEditor);        
}
cs

 

StringTrimmerEditor 에 들어가는 매개변수로 true 값의 의미는, 빈 문자열이 들어올 경우, null 로 설정하겠다는 의미가 된다. 빈 공백만 입력해서 넣으면, 이 클래스에서 해당 공백 값들을 전부 지우고, 빈 문자열로 만들어버린다. 그리고 true 로 설정하였으므로, null 로 인식되게 된다.

 

WebDataBinder 는 StringTrimmerEditor 로 인해서, 지워진 문자열 결과를 담기위한 객체이다.

 

이렇게하고, 실행을 하면 더이상 공백은 통과하지 못하고 막히게 된다.

 

 

 

 

예제 2 - Number Range

이번에는 숫자의 유효범위를 잡는 방법에 대해서 알아본다

간단하게 고객으로 부터 주문수량의 범위를 제한하는 멤버변수를 넣었다고 치자

 

- Customer.java

import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
 
public class Customer {
    
    @Min(value=1, message="must be greater or equal to one")
    @Max(value=10, message="must be less or equal to ten")
    private int orderQuantity;
 
    public int getOrderQuantity() {
        return orderQuantity;
    }
 
    public void setOrderQuantity(int orderQuantity) {
        this.orderQuantity = orderQuantity;
    }
        
}
cs

 

주문 수량의 최소값을 1, 최대 값을 10 으로 잡기위해 @Min, @Max 를 사용했다.

 

컨트롤러 부분은 예제 1과 다를게 없다. 로직이 똑같기 때문이다.

 

뷰 부분만 다음을 추가해준다.

 

- customer-form.jsp

    <body>
        <form:form action="processForm" modelAttribute="customer">
            Order Quantity (*) : <form:input path="orderQuantity"/>
            <form:errors path="orderQuantity" cssClass="error" />
            <br><br>
        </form:form>
    </body>
cs

 

- customer-confirmation.jsp

    <body>
        Order Quantity : ${customer.orderQuantity}
    </body>
cs

 

만약 이런 숫자 자료형을 필수 필드로 만들고 싶으면 어떻게 해야될까?

단순히 생각하면, @NotNull 을 써야 된다고 생각할 수 있다.

그러나, int 형 같은 primitive type 의 자료형은 null 이란게 존재하지 않는다. 그 이유는 객체가 아니기 때문이다.

그래서 @NotNull 을 써서 필수 필드로 만들고 싶으면 Wrapper Class 로 만들어서 사용하면 된다.

여기서는 int 대신 Integer 를 쓰면 되는것.

 

 

근데 예제2 에는 한가지 또 다른 문제가 있다.

수량부분에 만약 숫자대신 문자를 넣으면 어떻게 될까

이런식으로 긴 오류 메시지가 뜨게 된다.

이유는, 입력받은 문자열들을 숫자형으로 형 변환에 실패했기 때문에 이런 오류 메시지가 뜬것이다.

이렇게 긴 메시지를 사용자에게 보여주는 것은 상당히 깔끔하지 못하다

그래서 자체 오류 메시지를 넣어주기 위해서 다른 방법을 써야 한다.

 

먼저 properties 파일을 만들어준다

 

- messages.properties

1
typeMismatch.customer.orderQuantity=Invalid number
cs

첫번째 typeMismatch 는 에러 타입 이름을 말하고

두번째 customer 는 spring model attribute 이름을 말한다 (컨트롤러 부분에서 정의해둔 이름을 말함)

세번째 orderQuantity 는 폼의 필드 이름을 의미한다.

 

이 파일을 src/resources 부분에 넣어줬다 

그리고 xml 파일에 다음과 같은 코드를 추가한다

 

- spring-mvc-demo-servlet.xml

<!-- load custome error messages -->
<bean id="messageSource"
    class="org.springframework.context.support.ResourceBundleMessageSource">
    <property name="basenames" value="resources/messages" />
</bean>
cs

 

bean 태그의 id 와 class 값은 그대로 넣어야 한다 

폴더의 위치만 property 태그의 value 부분에 잘 설정해 주면 된다.

 

오류 메시지를 프론트 앤드 부분이 아닌 백엔드 부분에서 더 자세한 로그를 보고 싶다면

컨트롤러 부분에서 BindingResult 를 출력해주면 자세한 에러 결과가 로그로 남게 된다.

로그 예시)

 

 

 

 

 

예제 3 - 정규식을 이용한 검증

이번엔 고객으로 부터 고객의 우편번호를 얻도록 설정해보고, 우편번호의 제약사항으로 영문자 혹은 숫자를 이용해서 5글자만 가능하도록 처리해보자

이를 위해서는 정규식을 써야 한다.

 

- Customer.java

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
 
public class Customer {
        
    @NotNull(message="is required")
    @Pattern(regexp="^[a-zA-Z0-9]{5}", message="only 5 characters or digits")
    private String postalCode;
 
    public String getPostalCode() {
        return postalCode;
    }
 
    public void setPostalCode(String postalCode) {
        this.postalCode = postalCode;
    }
        
}
 
cs

 

스프링에서 정규식을 쓰려면 @Pattern 을 사용해야 하고, 정규식에 대한 자세한 사항은 아래 사이트 참조

docs.oracle.com/javase/tutorial/essential/regex/index.html

 

Lesson: Regular Expressions (The Java™ Tutorials > Essential Classes)

The Java Tutorials have been written for JDK 8. Examples and practices described in this page don't take advantage of improvements introduced in later releases and might use technology no longer available. See Java Language Changes for a summary of updated

docs.oracle.com

 

(추가적으로 정규식에 대해서 연습하기 좋은 사이트로 해커 랭크에 가면 문제를 풀 수 있다)

www.hackerrank.com/domains/regex?filters%5Bstatus%5D%5B%5D=unsolved

 

Solve Programming Questions | HackerRank

Join over 7 million developers in solving code challenges on HackerRank, one of the best ways to prepare for programming interviews.

www.hackerrank.com

 

나머지 뷰 부분이나, 컨트롤러 부분도 위의 다른 예제들과 똑같이 진행해주면 되므로 생략함.

 

 

 

 

예제 4 - Custom Validation

이번에는 Hibernate Validator 가 제공하는 API 가 아닌 자체적인 유효성 검증 코드를 만드는 방법에 대해서 알아보자

 

이를 위해서는 크게 두가지를 만들어야한다.

 

첫번째로, 해당 프로젝트를 만드는 프로그래머가 직접 작성한 어노테이션

두번째로, ConstraintValidator 인터페이스를 구현한 클래스

를 정의해줘야한다

 

둘에 대한 코드를 쓰기 전에 먼저 앞에서 사용하던 예제들의 패키지에 소속된 서브 패키지를 하나 더 만들어주고 폴더 구조를 이렇게 구성하자

 

이렇게 만들어줬다면, @CourseCode 라는 별도의 어노테이션을 만들어보려한다

아래의 코드를 살펴보자

 

- CourseCode.java

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
 
import javax.validation.Constraint;
import javax.validation.Payload;
 
@Constraint(validatedBy = CourseCodeConstraintValidator.class)
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface CourseCode {
    
    // define default course code
    public String value() default "LUV";
    
    // define default error message
    public String message() default "must start with LUV";
    
    // define default groups
    public Class<?>[] groups() default {};
    
    // define default payloads
    public Class<extends Payload>[] payload() default {};
}
cs

 

자체 어노테이션을 제작할때는, @interface 타입으로 정의하고, 

이 어노테이션에 대한 정의부분 위에 3개의 다른 어노테이션을 사용했는데 각각의 의미는 이렇다

 

-1) @Constraint(validatedBy = CourseCodeConstraintValidator.class)

-> @CourseCode 라는 자체제작 어노테이션을 이용해서 데이터 유효성 검증을 실시할때, 이 어노테이션만 정의한다고 해서, 유효성 검증을 위한 로직이 처리되는게 아니다. 별도의 로직을 구현해 줘야 하는데, 여기서 그 로직 구현을 위한 클래스로 CourseCodeConstraintValidator 라는 클래스를 사용하겠다는 의미가 된다.

 

 

* ConstraintValidator 인터페이스

: 위에서 사용한 CourseCodeConstraintValidator 는 ConstraintValidator 라는 인터페이스를 상속받아서 재정의한 클래스이다. 이 인터페이스는 어떤 어노테이션과 타입이 T 인 어떤 값이 주어졌을때, 이들에 대한 유효성 검증 과정을 거치기 위해, 초기작업시에는 어떤 일을 수행하고 (void initialize()), 이게 유효한지 아닌지 검증하는 메소드 (boolean isValid()) 가 존재한다. 

 

(자세한 부분은 아래의 공식문서 참조)

docs.oracle.com/javaee/7/api/javax/validation/ConstraintValidator.html

 

ConstraintValidator (Java(TM) EE 7 Specification APIs)

Implements the validation logic. The state of value must not be altered. This method can be accessed concurrently, thread-safety must be ensured by the implementation.

docs.oracle.com

 

-2) @Target({ElementType.METHOD, ElementType.FIELD})

-> 이 어노테이션은 어떤 부분에 이 어노테이션을 적용시킬지를 정의하는 것이다.

여기서 METHOD 와 FIELD 를 썼는데, 이는, 메소드와 필드에 이 어노테이션을 적용시키겠다는 소리가 된다.

 

(이게 무슨 말인가 하면, 앞선 포스팅에서도 몇개의 어노테이션들을 봤지만, 어떤 어노테이션은 메소드의 매개변수 안에서 사용되고, 어떤것은 멤버변수에 사용되고, 어떤것은 메소드에 사용되고 .. 등등 여러 경우가 많았다, 이런것 처럼 해당 어노테이션이 사용될 범위를 지정하는 것이라고 보면 된다)

 

 

-3) @Retention(RetentionPolicy.RUNTIME)

-> 이 어노테이션의 의미는 앞서 정의한 @CourseCode 어노테이션이 런타임 시기 동안 유지되게 해라 라는 의미가 된다. @CourseCode 어노테이션을 사용하는 클래스는 이 어노테이션을 런타임 시기 내내 유지하고 있어야 한다.

 

그리고 그 아래에 정의해놓은 value(), message(), groups(), payload() 는 어노테이션을 정의할때, 사용되는 속성값들로, value() 는 어떤 값이 들어올지를 의미하며, message() 는 오류시에 어떤 메시지가 나오게 할지 정해주는 것이다.

 

groups() 과 payload() 는 그 내용이 다소 복잡한데,

groups() 는 어떤 유효성 검증 범위에 소속되게 할지 정의하는 것으로, 어노테이션 사용시에 아무런 groups 값을 주지 않으면, 기본값으로 javax.validation.groups.Default 에 담기게 된다.

payload() 는 공식문서의 예제로 봤을때, 오류의 심각성 정도에 따라서 세부적으로 분할해서 나눠놓는거 같은데, 이 부분은 유데미에서 가르치는 강사나 관련 조교들도 잘 모르는것 같다. 아무래도 잘 쓰이지는 않는것 같아서 그런것 같다

(참고)

1. stackoverflow.com/questions/64493818/what-is-the-use-of-groups-and-payload-in-custom-annotation-in-java

 

What is the use of groups and payload in custom Annotation In Java?

Here's My code , I tried reading many stuff online but was not able to understand actual use of Class [] groups() default{}; Class[] payload() default{}; Here is my

stackoverflow.com

2. beanvalidation.org/1.1/spec/#constraintsdefinitionimplementation-constraintdefinition-properties

 

Bean Validation specification

This document is the specification of the Java API for JavaBean validation in Java EE and Java SE. The technical objective of this work is to provide an object level constraint declaration and validation facility for the Java application developer, as well

beanvalidation.org

 

 

 

이제 자체적인 어노테이션에 대해서 정의했으니, ConstraintValidator 인터페이스를 구현할 클래스를 정의해보자

 

 

 

- CourseCodeConstraintValidator.java

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
 
public class CourseCodeConstraintValidator 
    implements ConstraintValidator<CourseCode, String> {
 
    private String coursePrefix;
    
    @Override
    public void initialize(CourseCode theCourseCode) {
        coursePrefix = theCourseCode.value();
    }
    
    @Override
    public boolean isValid(String theCode, ConstraintValidatorContext theConstraintValidatorContext) {
        boolean result; 
        
        if (theCode != null) result = theCode.startsWith(coursePrefix);
        else result = true;
        
        return result;
    }
 
}
cs

ConstraintValidator 를 구현하기 위해서는 <어노테이션, 타입> 이런식으로 넣어줘야 하고,

 

initialize 함수는 CourseCode 어노테이션을 사용하면서 유효성검증을 하려고 시작할때, 초기화 하는 작업으로, 여기서는 CourseCode 에 정의된 value 값을 멤버 변수인 coursePrefix 에 담아뒀다.

(이는 사용자로부터 입력받은 값과 CourseCode 에 기본으로 정의된 값("LUV")과 일치하는지 비교하기 위해서이다)

 

isValid 함수는 매개변수를 두개를 받는데,

전자는 HTML form 태그에서 사용자로부터 입력받은 값이 들어가며,

후자는 어노테이션에서 정의한 오류메시지 대신 어떤 추가적인 에러메시지를 주거나, 다른 추가적인 데이터를 넣기 위해서 사용되는 인터페이스이다.

(참고) : docs.oracle.com/javaee/7/api/javax/validation/ConstraintValidatorContext.html

 

ConstraintValidatorContext (Java(TM) EE 7 Specification APIs)

Returns a constraint violation builder building a violation report allowing to optionally associate it to a sub path. The violation message will be interpolated. To create the ConstraintViolation, one must call either one of the addConstraintViolation() me

docs.oracle.com

 

그래서 입력받은 값 theCode 가 null 이 아니면, 초기화 작업에서 저장해둔 변수 값 coursePrefix 와 문자열이 앞에서부터 일치하는지 비교한다

즉 여기서는 LUV 로 시작하는 값이 들어왔는지 비교하는것임.

 

그래서 실행해보면, LUV 로 시작하는 값이 아니라면 오류가 나는 것을 볼 수 있다.

 

 

 

여기까지 스프링에서의 데이터 유효성 검증 방법에 대해서 알아보았다

쓰고나니 글이 너무 길었는데, 글을 나눠서 쓸걸 그랬다..

다음은, Hibernate 에 대해서 알아본다.

 

 

 

- References)

1. Bean Validator : medium.com/@SlackBeck/javabean-validation%EA%B3%BC-hibernate-validator-%EA%B7%B8%EB%A6%AC%EA%B3%A0-spring-boot-3f31aee610f5

2. Regular Expression : docs.oracle.com/javase/tutorial/essential/regex/index.html

3. Constraint Validator : docs.oracle.com/javaee/7/api/javax/validation/ConstraintValidator.html

 

 

 

728x90