본문 바로가기

백엔드 개발/SpringMVC

데이터 검증 - 매소드 만들기, 등록, view에 보여주는 법

<개요>

요청의 파라미터가 컨트롤러 매소드의 인수로 바인딩 될 때, 파라미터는 형 변환과 데이터 검증 절차를 거친다. 

형 변환은 파라미터를 바인딩될 매소드의 인수와 같은 형으로 변환하는 절차이고, 데이터 검증은 파라미터가 인수의 값으로서 말이 되는지 확인하는 절차이다. 이 둘을 모두 에러 없이 거치면 바인딩 되고, 과정에서 에러가 나면 바인딩은 실패하고 에러 내용은 BindingResult의 객체에 저장된다. 

저번 시간에는 형 변환 매소드를 만들어 Binder에 등록해서 WebDataBinder가 원래 할 수 없었던 형 변환을 할 수 있도록 만드는 것을 했다. 이번엔 같은 과정을 데이터 검증 쪽에서 할 것이다. 데이터 검증의 경우에도, WDB에 기본 내장된 기능 말고 사용자가 만들어 하고 싶은 검증 절차가 따로 있을 수 있다. (1) 이 검증 절차를 만들어 Binder에 등록하지 않고, 사용자가 수동으로 사용할 수도 있고, 이걸 Binder에 등록 시켜서 자동으로 사용되도록 만들 수도 있다. 

또한 (2)ServletContext.xml에 등록하여 범 컨트롤러적 데이터 검증을 하도록 만들 수도 있고, 컨트롤러 내부에서만 검증의 영향력이 미치도록 할 수도 있다. 

(3)마지막으로 데이터 검증 시 난 오류를 메세지로 화면에 띄우는 것도 더 이상 Javascript를 쓰지 않고 Spring 내 설정 변경 (Servletcontext에 새 속성 추가) 및 JSP의 새 태그 라이브러리인 Form 라이브러리를 써서 나타낼 수 있다.  

 

이 3가지 절차에 대해 배워보겠다.

1. Validator(데이터 검증기) 생성 및 수동 검증, 자동 검증

(1)Validator란?

//Validator 인터페이스 
public interface Validator{
	
    // 검증하려는 객체가 이 검증기로 검증 가능한 객체인지 알려주는 매소드 
    // Class 클래스의 객체로서 해당 객체의 클래스 정보(메타 데이터)를 확인한다.
	boolean supports(class<?> clazz);
    
    //객체를 검증하는 매서드, target이 검증할 객체, errors가 검증 시 발생한 에러 저장소
    void validate(Object target, Errors errors)
}

우리는 우리만의 데이터 검증 기를 만들 때, 해당 인터페이스를 뼈대로 구현하여 만든다. 

여기서 객체 검증하는 Validate 추상화 매소드가 errors라는 객체를 받는 것을 확인할 수 있다. Errors는 Spring에서 제공하는 인터페이스로서 에러가 저장되는 장소로서의 역할을 한다. BindingResult 또한 Errors의 자식 인터페이스이다. 

// errorCode는 error의 내용이다.

public interface Errors{
	// Errors 객체안에 인수로 들어온 errorCode를 저장
	void reject(String errorCode);

	// Errors 객체안에 error가 일어난 필드와 errorCode를 저장 
	void rejectValue(String field, String errorCode);
   
   // 위의 매소드 오버로딩 버젼, errsArgs는 errorcode내에서 %s %n처럼 지정 값 넣고 싶을 때 쓰임.
   // defaultMessage는 에러 코드에 맞는 메세지를 못 찾았을 때 대신 쓰임.
    void rejectValue(String field, String errorCode, Object[] errorsArgs, String defaultMessage);
}

첫번째 reject는 어디서 에러가 발생했는지 알 수 없다. 과잉 정보를 클라이언트에게 주면 오히려 해킹의 원인이 될 수 있어서 id 또는 pwd 가 틀렸다는 것처럼 모호하게 줄 때 사용한다. 

 

이제 위의 Validator 인터페이스를 가지고 나만의 검증기를 만들어보자. 이건 회원가입 때, 클라이언트의 입력 값들을 전부다 연결 받는 User 객체의 변수들을 검증하는 검증기 이다. 

	//1. Validator 구현
	public class UserValidator implements Validator {
		@Override
		// Class 클래스로 어떤 객체의 클래스 정보(메타 데이터)가 저장되어 있다.
		// 1-1 해당 검증기로 검증할 수 있는 객체인지 확인
		public boolean supports(Class<?> clazz) {
			
			// (둘 다같은 말) clazz가 User 또는 그 자손의 클래스 정보인지 확인
			// return User.class.equals(clazz); 
			return User.class.isAssignableFrom(clazz); 
		}

		//1-2 진짜 유효성 확인. 
		@Override
		// Object는 모든 클래스의 조상이라, 다형성의 원리에 따라 Object 객체는 모든 객체를 가리킬 수 있어서 사용 
        public void validate(Object target, Errors errors) { 

			//1-2-1 자료형을 Object로 받아오기 때문에 형 변환 필요. 
			
			User user = (User)target;
			
			String id = user.getId();
			
			//trim은 앞 뒤 불편한 공백 제거 해주는 함수
			//if(id==null || "".equals(id.trim())) {
			//	errors.rejectValue("id", "required");
			//		}
			
			// 1-2-2 만약 id 혹은 pwd 안이 비어 있거나 ""같은 공백으로 채워져 있으면
          	// 객체 errors안에 errorCode로 "required"를 저장해라
			ValidationUtils.rejectIfEmptyOrWhitespace(errors, "id",  "required");
			ValidationUtils.rejectIfEmptyOrWhitespace(errors, "pwd", "required");
			
			// 1-2-3 id의 길이 검사 5이상 12이하가 아니라면 invalidLength 에러 코드 errors에 저장하라.
			if(id==null || id.length() <  5 || id.length() > 12) {
				errors.rejectValue("id", "invalidLength");
			}
		}
	}

(2) 수동 검증

수동 검증은 별 거 없다. 위에 Validator를 구현한 UserValidator 클래스의 객체를 생성해서, 해당 객체의 유효성 검사 매소드(validate)에 값을 넣고 사용하는 것 뿐이다. 

이때 Validate는 첫번째 인수인 target을 검사해서 검사 결과를 errors에 저장한다. 

우린 이 error가 에러를 가지고 있다면 다시 회원가입 폼으로 돌아가도록 하면 된다. 

여기서 BindingResult가 Errors의 자식 인터페이스 이므로 우리는 BindingResult의 객체 result를 errors자리에 대신 넣을 수 있다.

	public String save(@Valid User user,BindingResult result ,Model m) throws Exception {


		//수동 검증 - validator 객체 직접 생성, validate 함수 직접 호출
		UserValidator userValidator = new UserValidator();
		userValidator.validate(user, result);
		
		
        	// 호출 결과 result에 error 저장될 시 다시 registerForm으로 돌아가라.
		if(result.hasErrors()) {
			return "registerForm";
		}

(3) 자동 검증

절차 : Binder에 등록 / @Valid 어노테이션 사용

a. Binder 추가 매소드에 아까 만든 UserValidator의 객체를 Local Validator로서 등록한다.

Binder 추가 매소드란 컨트롤러 내부 매소드 중 @InitBinder 어노테이션이 붙어 있는 매소드를 의미한다. 

해당 매소드 내부에서 형 변환 클래스의 객체나 데이터 검증 클래스의 객체를 명령어로 binder에 집어넣으면, 

Web Data Binder에 해당 기능들이 새로 등록된다.

// 저번에 파라미터를 Date 객체로 형 변환 하기 위해 만들었던 Binder 추가 매소드를 사용
@InitBinder
// WDB를 인수로 받는다.
public void toDate(WebDataBinder binder){
	//...
    
    // UserValidator의 객체를 새로운 Local Validator로 Binder에 등록
    binder.setValidator(new UserValidator());
}

이제 UserValidator가 새로운 Local Validator로서 Binder에 추가가 됐다. Local Validator인 이유는 컨트롤러 내부에 선언되어서 해당 데이터 검증의 영향력이 컨트롤러 내부에만 미치기 때문이다. 범 컨트롤러 적으로 미치는 방법은 다음 챕터에서 배워보자. 

b.이제 등록이 되었으니, 우리가 데이터 검증하고 싶은 매소드의 인수 옆에 @Valid 어노테이션을 붙여주면 된다. 

	@PostMapping("/add") 
	public String save(@Valid User user,BindingResult result ,Model m) throws Exception {

		System.out.println("result="+result);
		System.out.println("user="+user);

이러면 save의 인수 user에 대해서 자동으로 검증이 이루어진다.

 

**참고 @Valid 는 자바나 스프링의 기본 어노테이션으로는 없는 기능이다. 이건 Maven에 추가된 어노테이션인데 

이걸 이용하려면 MavenDependency에 추가 해줘야 한다. 

접속
validation 입력 후 두번째 선택
복사
pom.xml에 붙여넣기.

2. GlobalValidator만들기 

이번엔 범 컨트롤러적으로 전부 적용되는 GlobalValidator를 만들어보자. 

절차는 다음과 같다. 

(1) GlobalValidtor로 쓸 Validator Class를 하나 만들기 

package com.fastcampus.ch2;

import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;


	public class GlobalValidator implements Validator {
		@Override
		public boolean supports(Class<?> clazz) {
//			return User.class.equals(clazz); // 검증하려는 객체가 User타입인지 확인
			return User.class.isAssignableFrom(clazz); // clazz가 User 또는 그 자손인지 확인
		}

		@Override
		public void validate(Object target, Errors errors) { 
			System.out.println("GlobalValidator.validate() is called");

			User user = (User)target;
			
			String id = user.getId();
			
	//		if(id==null || "".equals(id.trim())) {
	//			errors.rejectValue("id", "required");
	//		}
			ValidationUtils.rejectIfEmptyOrWhitespace(errors, "id",  "required");
			ValidationUtils.rejectIfEmptyOrWhitespace(errors, "pwd", "required");
			
			if(id==null || id.length() <  5 || id.length() > 12) {
				errors.rejectValue("id", "invalidLength",new String[] {"0","5","12"}, null);
			}
		}
	}

내용은 위의 UserValidator와 같다. 이걸 Servlet-context에 Global Validator라고 등록 해야하기 때문에 Local 가 차이를 둘려고 하나 새로 만들었다. 

 

(2) 해당 Global Validator를 Servlet-context에 Bean으로 등록 및 annotaiton-driven 태그에도 검증기로서 등록

Spring의 모든 웹 설정을 담당하는 Servlet-context안에 해당 Class가 전역에 영향을 끼치는 놈이라고 등록 시키는 것

	<annotation-driven validator ="globalValidator" />
	<beans:bean id = "globalValidator" class ="com.fastcampus.ch2.GlobalValidator"/>
    
    
    <!--class는 작성한 Validator 클래스가 어디 있는가 위치를 나타낸다.
    	id는 해당 클래스를 부르는 이름이다. 
        이 id를 사용해 annotation-driven 태그에 validator로서 등록한 것이다.-->

(3) 그 후는 자동 검증 처럼 검증하고 싶은 객체 옆에 @Valid를 붙이면 WDB가 이 Validator 써서 자동으로 검증해준다. 

 

(참고) Global Validator가 있는 상황에서 특정 컨트롤러 내부에 Local Validator 만들어서 사용하고 싶은 경우

이때도 Local Validator를 Binder에 추가하는 과정과 똑같이 Binder 추가 매소드에 Validator 클래스를 등록하면 되는데 명령어가 좀 다르다. 이번엔 binder.setValidator 가 아니라 binder.addValidators이다. 이미 있는 Global Validator에 내용을 추가하는 개념이다. 

@InitBinder
public void toDate(WebDataBinder binder) {
	/...
    
    //Global Validator 없을 시 새 Local Validator 등록
    binder.setValidator(new UserValidator());
    //Global Validator 있을 시 새 Local Validator 등록
    binder.addValidators(new UserValidator());
}

3. JavaScript 쓰지 않고, 데이터 검증 시 확인한 오류를 view에 띄우는 법

(1) 코드 뜯어보기 

먼저 다양한 리소스에서 페이지를 읽기 위한 MessageSource라는 것이 있다.

우리는 Error에 저장된 에러 정보를 알맞은 메세지로 변환하여 view에 띄우기 위해 해당 인터페이스의 구현체인 ResourceBundleMessageSource라는 클래스를 쓴다. 

해당 클래스는 Servlet-Context의 Bean에 등록해야지 쓸 수 있다. 

Bean에 등록할 때 error_message를 value로 저장하는 것을 볼 수 있다. 

이 value에 저장된 것은 우리가 받은 에러 코드와 그에 알맞은 메세지가 map 형식으로 저장된 파일의 이름이다.

RBMS는 받은 에러 코드를 해당 error_message.properties를 이용해 알맞는 메세지로 변환한다. 

 

 

a.ResouceBundleMessageSource에서 getMessage가 어떻게 사용되는가?

RBMS에서 구체화된 getMessage는 인수 별로 다음과 같은 값들을 반환한다.

ㄱ. String code는 errors에 저장된 errorCode가 들어가는 곳이다. 

      RBMS는 이에 맞는 메세지를 반환한다.

ㄴ. Object[]args는 메세지에 쓰일 사용자 지정 값들이 들어가는 곳이다. 

errors.rejectValue("id", "invalidLength",new String[] {"0","5","12"}, null);

위와 같은 형식으로 error에 저장되면 getMessage가 차례대로 값들을 가져오는데, 

error_message.properties 안에 invalidLength 에러코드는 해당 메세지와 대응됨을 알 수 있다.

이떄 {1} {2}는 프린트문에서 쓰이던 %n %s 처럼 사용자가 지정한 값이 들어가는 위치이다. 

{1} {2}는 배열의 1번과 2번 원소를 쓰겠다는 소리이다. (배열은 0부터 시작해서 일부러 0번도 채웠다.)

 

ㄷ. defaultMessage는 에러 코드에 대응하는 메세지를 찾을 수 없을 때 대신 view 나오는 message이다. 

ㄹ. Locale은 해당 요청을 한 브라우저가 있는 국가 위치를 가져온다. 우리는 이를 이용해 미리 error_message를 국가별로 만들어 놓은 뒤 Locale의 값을 보고 국가별 언어로 된 error_message를 만들 수 있다.

(2)일이 행해지는 원리 

error 객체에 rejectValue로서 저장된 값들을 RBMS에서 모두 받아서 저장. 

이에 맞는 처리를 error_message.properties를 사용하며 하고 view에 결과를 보냄.

view가 결과 내용을 처리 

이때 처리를 위해 새로운 태그 라이브러리인 form 태그 라이브러리가 필요. 

 

(3) error_message.properties에서 에러코드에 맞는 메세지를 찾는 우선순위

만약 에러코드"requried"가 "id"라는 에러 필드와 함께 온다면

RBMS는 일단 해당 정보와 관련해서 "제일 구체적"인 에러코드의 에러메세지를 가져오려고 한다.  

여기서 id는 user 객체의 id 이므로 먼저 required.user.id라는 에러코드에 대응하는 메세지를 찾는다. 

이게 없다면, 필드 값이 같은 에러코드가 있는지 찾고 그 녀석의 메세지를 내보낸다. requried.id

이거도 없다면, 에러코드와 타입이 같이 적힌 에러코드가 있는지 찾는다. requried.java.lang.string

이것도 없으면 그냥 에러코드 자체를 찾고 매핑된 메세지를 찾는다. requried 

이것도 없으면 defaultmessage를 내보낸다. 

에러코드 해당 에러코드가 왔을 때 error_message에서 찾는것
"required" requried.user.id
requried.id
required.java.lang.string
requried
defaultMessage

(4)View에 띄우려면? 

스프링에서 제공하는 form 태그 라이브러리를 이용해야 한다. 

<!--form 태그 라이브러리를 jsp에 import  맨 초반부에 적어주면 됨.-->
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %>

위를 적어주면 이제 해당 jsp에서 form 태그 라이브러리를 쓸 수 있다.

우리는 이제 일반적으로 제공되는 <form> 태그가 아닌 form 태그 라이브러리의 form <form:form>을 쓸 것이다. 

해당 내용을 토대로 form 태그를 수정해보자. 

  <!--	modleAttribute="검증할 객체"를 써준다.  
  		form:error는 에러 메세지를 띄우는 부분으로 path="필드이름"을 적어주면
  		해당 필드에서 일어난 에러를 메세지로 변환해 화면에 띄운다.-->
  
  <!-- 바꾸기 전!  
  <form action="<c:url value="/register/save"/>" method = "POST" onsubmit="return formCheck(this)">
   	   바꾼 후!-->
  <form:form modelAttribute="user">
    <div class="title">회원등록</div>
    <div id="msg" class="msg"><form:errors path="id"/></div> 
	<!-- 내용 생략 -->
	</form:form>

해당 form 태그들은 html로 변환될 때 밑과 같이 변환된다.

<form:form modelAttribute="user">
<!----------------변환--------------------->
<form id="user" action="/ch2/register/add" method="post">

<!--현재와 같은 경로주소로 post하는 form 태그로 변환된다.-->
<form:errors path="id"/>
<span id="id.errors">length of id should be between 5 ~ 12 </span>

<!--대응하는 에러 메세지로 바뀐다.-->

 

4. 스스로 해보기

emali이 @를 가지고 있지 않으면 오류 뱉는 로직 설계 

//Global Validator 내부

String email = user.getEmail();
			
			if (!email.contains("@")) {
				errors.rejectValue("email", "requried");
			}
// properties 내부 
required=It's required item
required.user.pwd= User's password is required item

requried.user.email = email must have @ 
invalidLength.id=length of id should be between {1} ~ {2}
<div id="msg" class="msg"><form:errors path="email"/> <form:errors path="pwd"/></div> 
<!--이렇게 적으니까 둘 중 오류난 거 하나 있으면 그거 띄우게 하더라 굳-->