Validation

validation이란 어떤 데이터의 값이 유효한지, 타당한지 확인하는 것을 의미합니다.

예를들어 이메일 주소 양식은 admin@example.com인데, 회원 가입을 할 때 이메일 양식이 일치하지 않으면 유효하지 않은 이메일이므로 회원 가입을 막을 수 있습니다.


UI에서 javascript로 "이메일 양식이 일치하지 않는다"는 것은 UX 측면에서 사용자에게 편의를 주기 위함입니다.

사용자가 잘못 입력하여 오타가 발생 했으니 다시 한 번 확인을 하라는 의미가 강하죠.

즉 UI단에서 유효성 검사를 하는 것은 보안적인 측면에서 아무 효과가 없다는 것을 말하고 싶습니다.


보안적인 측면에서 유효성 검사란 올바르지 않은 데이터가 서버로 전송되거나, DB에 저장되지 않도록 하는 것입니다.

스프링에서는 이에 대해 서블릿 2.3 표준 스펙 중 JSR-303 Validator를 확장하여 Annotation 기반으로 validation을 제공하고 있습니다.

[ JSR-303 스펙 기본 제공 ]



이 글에서는 Hibernate에서 제공하는 어노테이션을 사용하려고 합니다.

[ Hibernate 제공 ]




환경 설정

pom.xml

<!-- validation -->

<dependency>

   <groupId>javax.validation</groupId>

   <artifactId>validation-api</artifactId>

   <version>1.0.0.GA</version>

</dependency>

<dependency>

   <groupId>org.hibernate</groupId>

   <artifactId>hibernate-validator</artifactId>

   <version>4.2.0.Final</version>

</dependency>

만약 org.springframework.beans.factory.BeanCreationException: Error creating bean with name

'org.springframework.validation.beanvalidation.OptionalValidatorFactoryBean#0': Invocation of init method failed; nested exception is java.lang.NoClassDefFoundError: javax/xml/bind/JAXBException

에러가 발생한다면 아래의 라이브러리를 추가해보세요.

<dependency>

<groupId>javax.xml.bind</groupId>

<artifactId>jaxb-api</artifactId>

<version>2.3.0</version>

</dependency>


JAXBException 클래스를 찾을 수 없는 이유는, Hibernate가 JDK 버전에 민감하기 때문에 발생하는 문제이기 때문에 발생하는 오류일 것입니다.

( 스택오버플로우 링크 )


또는 hibernate-validator의 version을 바꾸시고, C드라이브의 .m2 폴더의 repository폴더를 삭제하신 후 이클립스를 재실행 해보시길 바랍니다.

<dependency>

<groupId>org.hibernate</groupId>

<artifactId>hibernate-validator</artifactId>

<version>4.3.2.Final</version>

</dependency>





컨트롤러 , VO 작성

이 글에서는 회원 가입을 할 때 유효성을 검사하는 예제를 만들어 볼 것입니다.


UserController

@RequestMapping(value="/user/join", method=RequestMethod.POST)
// binding한 결과가 result에 담긴다.
public String join(@ModelAttribute @Valid UserVO vo, BindingResult result) {
	// 에러가 있는지 검사
	if( result.hasErrors() ) {

		// 에러를 List로 저장
		List<ObjectError> list = result.getAllErrors();
		for( ObjectError error : list ) {
			System.out.println(error);
		}
		return "/user/join";
	}
	
	userService.join(vo);
	return "redirect:/user/joinsuccess";
}

파라미터에서 유효성 검사가 필요한 객체에 대해 @Valid 어노테이션을 추가했습니다.

그리고 BindingResult 객체는 검증 결과에 대한 결과 정보들을 담고 있습니다.


검증 결과 정보, 즉 BindingResult는 DispatcherServlet이 JSP에 넣어줍니다.

컨트롤러에서 뷰 이름을 반환하면 에러 내용을 바인딩해서 JSP에 넘겨줄 테니, 값을 사용자에게 보여주라는 의미의 forwarding 개념이 깔려있습니다.

( 위의 코드에서는 간단하게 에러 내용을 콘솔로 출력해보았는데, 다음 예제에서는 JSP 페이지로 에러메시지를 넘겨줄 것입니다. )


파라미터에서 @Valid 어노테이션을 붙이는 유효성 검사에 대한 선언은 컨트롤러에서 하고,

유효성 검사를 체크하는 그 근거는 UserVO 클래스에 작성합니다.




이제 UserVO에 데이터를 검증하는 조건을 추가하겠습니다.

UserVO

public class UserVO {
	  private Integer no;
	   
	  @NotEmpty
	  @Length(min=2, max=5)
	  private String name;
	   
	  @NotEmpty
	  @Email
	  private String email;
	   
	  @NotEmpty
	  @Pattern(regexp="(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&+=])(?=\\S+$).{8,}")
	  private String pwd;
	   
	  @NotEmpty
	  private String gender;
	   
	  private String regDate;
	 
	  /*
	        getter / setter 생략
	        ...
	  */
}

멤버 변수위에 어노테이션으로 검증 조건을 작성했습니다.

어노테이션 이름이 직관적이므로 따로 설명을 하지 않아도 될 것 같습니다.



이제 테스트를 위해 회원 가입 양식 HTML Form을 만들겠습니다.

form.jsp

<form method="POST" action="${pageContext.servletContext.contextPath }/user/join">

        이름 : <input name="name" type="text" value="">

        이메일 : <input id="email" name="email" type="text" value="">

        비밀번호 : <input name="pwd" type="password" value="">


        <fieldset>

               <legend>성별</legend>

               <label></label> <input type="radio" name="gender" value="female" checked="checked">

               <label></label> <input type="radio" name="gender" value="male">

        </fieldset>


        <input type="submit" value="가입하기">

</form>

회원 가입 form에서 내용을 모두 빈칸으로 입력한 후 "가입하기" 버튼을 누르면 콘솔에 에러 메시지가 출력 될 것입니다.





JSP에 메시지 출력

컨트롤러에서 Validation을 선언하고, VO 객체에서 검증 조건을 작성하면 유효한 데이터만 받을 수 있습니다.

그런데 검증 실패 원인에 대해서 콘솔로 에러메시지를 출력할 뿐만 아니라, 사용자가 알 수 있도록 JSP에 에러 메시지를 출력하면 좋을 것 같습니다.


form.jsp

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>

<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>

<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn"%>

<%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %>


<!-- 생략 -->



이름 : <input name="name" type="text" value="">

<spring:hasBindErrors name="userVO">

            <c:if test="${errors.hasFieldErrors('name') }">                                    

               <strong>${errors.getFieldError( 'name' ).defaultMessage }</strong>

</c:if>

</spring:hasBindErrors>

이름의 유효성 검사가 실패했을 경우 에러 메시지를 출력하도록 작성했습니다.


서버에서 전달해준 BindingResult 객체를 사용하기 위해서는 태그 라이브러리를 추가해야 합니다.

<%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %>


<spring:hasBindErrors name="userVO">에서 userVO는 UserVO 객체의 이름입니다.

UserVO의 앞 글자 U를 소문자 u로 바꿔야 하는데, 그 이유는 잘 모르겠지만 userVO가 변수로서 사용되니 관례로서 바꿔주는 것 같습니다.


defaultMessage는 유효성 검사가 실패했을 경우, 검증 조건을 알려주는 기본 메시지입니다.

테스트를 해보시면 아시겠지만 메시지가 영어이기 때문에 마음에 들지 않습니다.


그래서 개발자 입맛에 맞게 메시지를 커스터마이징 해보겠습니다.





메시지 커스터마이징

spring-servlet.xml

<!-- MessageSource -->

<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">

<property name="defaultEncoding">

<value>utf-8</value>

</property>

   <property name="basenames">

      <list>

               <value>messages/messages_ko</value>

      </list>

   </property>

</bean>

메시지를 커스터마이징 하기 위해서는 위와 같이 bean을 생성해야 합니다.

한국어로 바꿀 것이므로 messages/messages_ko를 value로 작성합니다.



이제 메시지를 어떤 내용으로 바꿀 것인지에 대한 설정 파일을 생성해야 합니다.

/src/main/resources/messages 폴더를 생성한 후 messages_ko.properties 이름으로 file을 생성합니다.

spring-servlet.xml 파일에서 그렇게 정의를 했으므로 꼭 폴더 이름과 파일 이름을 위와 같이 작성해야 합니다.

messages_ko.properties

NotEmpty.userVO.name = \uC774\uB984\uC774 \uBE44\uC5B4\uC788\uC2B5\uB2C8\uB2E4.

" NotEmpty라는 검증 조건 . 검증 조건이 있는 객체 이름 . 변수명 "의 양식으로 출력될 에러 메시지를 입력하면 위와 같이 UTF-8로 인코딩 되어 메시지를 등록할 수 있습니다.


UserVO 객체에 대해 각각의 유효성 검증이 실패했을 경우 출력 될 메시지를 작성해보면 아래 사진과 같습니다.

이렇게 각 변수의 검증 조건에 대하여 메시지를 커스터마이징 해봤습니다.

검증에 실패하면 기본 메시지로 위에서 작성한 메시지들이 출력될 것입니다.

form.jsp

이름<input name="name" type="text" value="">

         <spring:hasBindErrors name="userVO">

                  <c:if test="${errors.hasFieldErrors('name') }">                                    

                           <spring:message

                                   code="${errors.getFieldError('id').codes[0]}"

                                   text="${errors.getFieldError('id' ).defaultMessage  }"

                           />      

                  </c:if>

         </spring:hasBindErrors>




그런데 한 가지 문제점이 남아 있습니다.

하나의 입력 값에 대해서 유효성 검증에 실패하면 모든 입력 값이 날라가게 됩니다.

사용자가 길게 작성한 입력을 했는데, 검증에 실패했다고 입력했던 내용이 모두 날라가 버리면 기분이 안좋겠죠?

그래서 입력 값을 유지 할 수 있는 방법이 필요합니다.



form 양식 유지

검증이 실패했을 경우, 서버로 데이터가 전송되지 않는 것은 지금까지 확인해보았습니다.

이번에는 검증에 실패해도, 사용자가 입력했던 내용을 그대로 유지하는 방법을 알아보겠습니다.

form.jsp

<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %>


<form:form

         modelAttribute="userVO"

         method="POST"

         action="${ pageContext.servletContext.contextPath }/user/join">

</form:form>

<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %>

form 태그 라이브러리를 추가하고 기존의 <form> 태그를 위와 같이 <form:form> 태그로 바꿔줍니다.

form 태그 라이브러리는 태그 같지만 실제로는 java 코드가 돌고 있습니다. 태그가 아닙니다.


modelAttribute 속성에는 유효성 검증을 하고자 하는 객체를 작성하면 됩니다.

마찬가지로 실제로는 UserVO지만, userVO를 작성합니다.


이 때 form.jsp 페이지를 반환하는 컨트롤러에서 UserVO 객체가 없다면 에러가 발생하기 때문에 GET방식 컨트롤러에 UserVO 객체를 추가해야 합니다.

UserController

@RequestMapping(value="/join", method=RequestMethod.GET)
public String join(@ModelAttribute UserVO vo) {
	return "/user/join";
}

컨트롤러에 위와 같이 작성했다면, 처음 "회원가입" 버튼을 클릭했을 때 form.jsp 페이지의 <form: 태그의 @ModelAttribute에는 userVO가 없지만,

사용자가 회원 가입을 할 때 유효성 검사에 실패한다면, 컨트롤러로부터 userVO가 담겨서 오므로, 사용자가 입력했던 값이 유지됩니다.




최종적으로 form.jsp를 작성하면 다음과 같습니다.

<form:form

         modelAttribute="userVO"

         method="POST"

         action="${ pageContext.servletContext.contextPath }/user/join">

         이름 : <form:input path="name" />

         <form:errors path="name" />

         이메일 : <form:input path="email" />

         <form:errors path="email" />

         비밀번호 : <form:password path="pwd" />

         <form:errors path="pwd" />


         <fieldset>

                  <legend>성별</legend>

                  <label></label> <form:radiobutton path="gender" value="female" />

                  <label></label> <form:radiobutton path="gender" value="male" />

         </fieldset>


         <input type="submit" value="가입하기">

</form:form>

default message를 출력하는 spring-message 태그를 사용했을 때 보다 form 태그를 사용하니까 에러 메시지를 출력하기 위한 코드가 상당히 짧아졌고, 가독성도 좋아졌습니다.


이제 유효성 검사에 실패했을 때 기존에 작성했던 입력 값은 그대로 남아 있게 되고 커스터마이징한 에러 메시지가 출력될 것입니다.




이상으로 유효한 데이터만 DB에 저장될 수 있도록 유효성 검사를 적용해봤습니다.

이 글을 통해

1) @Valid 어노테이션과 검증 조건을 추가하는 방법

2) 메시지를 커스터마이징 하는 방법,

3) form 태그 라이브러리로 입력 값을 유지하면서 가독성이 좋게 하는 방법

을 알아보았습니다.