Spring

[Spring Boot] Validation

shb 2022. 5. 12. 18:18

폼 데이터 검증 (Parameter Validation)
신뢰성 있는 웹 어플리케이션 작성을 위해 폼데이터 검증 필수
기본적으로 폼데이터는 2단계 걸쳐 검증해주어야 한다.
1단계: ‘클라이언트’ 에서 submit 하기 전에 검증 : (JavaScript) 사용
2단계: ‘서버’ 에서도 검증  : Spring, JSP 등 사용...

 

WriteDTO 클래스 가져오기
직전 단원에서 작성한 DTO 클래스와 패키지를  복사해옵시다.
생성자, getter, setter 의 출력코드 살려둡니다. 

 

일단 기본적인 form 동작 작성

 

컨트롤러 생성

package com.lec.spring.controller;

import java.util.List;

import javax.validation.Valid;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Errors;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import com.lec.spring.domain.BoardValidator;
import com.lec.spring.domain.WriteDTO;

@Controller
@RequestMapping("/board")
public class BoardController {
	
	@RequestMapping("write")
	public void write() {}
	
	
	// BindingResult : spring validator 가 유효성 검사를 한 결과가 담긴 객체
	@RequestMapping("writeOk")
	public String writeOk(@ModelAttribute("w") @Valid WriteDTO dto,
			BindingResult result,	
			Model model,  // Model 이 BindingResult 보다 앞에 있으면 에러..
			RedirectAttributes redirectAttrs   // redirect: 시 넘겨줄 값들
			) {
		System.out.println("writeOk : " + dto.getUid() + ":" + dto.getName());	
		String page = "board/writeOk";
		
//		System.out.println("validate전 "); showErrors(result);
		
		// validator 객체 생성
//		BoardValidator validator = new BoardValidator();
//		validator.validate(dto, result);
		
		System.out.println("validate후 "); showErrors(result);
		if(result.hasErrors()) {   // 에러가 있었으면
			redirectAttrs.addFlashAttribute("w", dto);  // 원래 입력한 값 돌려주기.
			if(result.getFieldError("uid") != null) {
				redirectAttrs.addFlashAttribute("errUid", "uid 값은 0보다 큰 정수이어야 합니다");
			}
			if(result.getFieldError("name") != null) {
				redirectAttrs.addFlashAttribute("errName", "name 은 필수 입니다");
			}
			if(result.getFieldError("subject") != null) {
				redirectAttrs.addFlashAttribute("errSubject", "subject 는 필수 입니다");
			}
			page = "redirect:/board/write";   // 리턴값(String)  에 redirect:url <- 해당 url 로 redirect 한다
		}
		
		
		return page;
	}
	
	// binding 에러 출력 도우미 메소드
	public void showErrors(Errors errors) {
		if(errors.hasErrors()) {
			System.out.println("에러개수: " + errors.getErrorCount());
			System.out.println("\t[field]\t|[code] ");
			List<FieldError> errList = errors.getFieldErrors();
			for(FieldError err : errList) {
				System.out.println("\t" + err.getField() + "\t|" + err.getCode());
			}
		} else {
			System.out.println("에러 없슴");
		}
	}
	
	// 이 컨트롤러의 클래스의 handler 에서 폼데이터를 바인딩할때 검증하는 객체 지정
	@InitBinder
	public void initBinder(WebDataBinder binder) {
		binder.setValidator(new BoardValidator());
	}
	
	
	
}

 

* 간단한 작성 폼 작성 : write.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>글 작성</title>
<style>
span { color: red;}
</style>
</head>
<body>
<form action="writeOk">
uid(<span>숫자</span>): 
	<input type="text" name="uid" value="${w.uid }"><span>${errUid }</span><br>
작성자(<span>*</span>): 
	<input type="text" name="name" value="${w.name }"><span>${errName }</span><br>
제목(<span>*</span>):
	<input type="text" name="subject" value="${w.subject }"><span>${errSubject }</span><br>
<input type="submit" value="등록"><br>
</form>
</body>
</html>

* writeOk.jsp 작성

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
uid: ${w.uid}<br>
작성자: ${w.name}<br>
제목: ${w.subject }<br>
<button onclick="history.back()">돌아가기</button>

동작 확인하기 - uid (숫자, int) 가 비어 있으면?

 

폼 데이터를 검증하려면 어디에?
커맨드 객체를 사용하는 상황에서 폼 검증은 어디서 해야 하나?
이미 스프링(Dispatcher Servlet)이 사전 처리 하는 단계에서  발생하는데?
스프링 MVC 에서 폼 데이터 검증 코드를 집어 넣으려면 어떻게?


스프링에서 폼 데이터 검증 validator

예제의 시나리오
아래와 같이 폼 데이터 에 대한 규칙이 있을때, 서버로 넘어온 폼데이터를 스프링에서 다루어 보도록 합니다

uid 값은 반드시 숫자 입력 :  비어 있으면 안됨.
name 은 반드시 입력 : 비어 있으면 안됨.

 

handler에 BindingResult 매개변수
마치 handler의 Model 매개변수와 마찬가지로 DispatcherServlet 에서 제공(자동주입) 해주는 객체
validator가 유효성 검사를 한 결과가 담긴 객체

BindingResult result

	// BindingResult : spring validator 가 유효성 검사를 한 결과가 담긴 객체
	@RequestMapping("writeOk")
	public String writeOk(@ModelAttribute("w") @Valid WriteDTO dto,
			BindingResult result,	
			Model model,  // Model 이 BindingResult 보다 앞에 있으면 에러..
			RedirectAttributes redirectAttrs   // redirect: 시 넘겨줄 값들
			) {
		System.out.println("writeOk : " + dto.getUid() + ":" + dto.getName());	
		String page = "board/writeOk";

이 상태에서 결과 확인
일단 400 에러 발생 안함 uid 값이 0
콘솔을 확인했을때 setUid( 0 )이 아예 호출되지 않았음

 

* 폼 데이터 오류 개수 확인

	// BindingResult : spring validator 가 유효성 검사를 한 결과가 담긴 객체
	@RequestMapping("writeOk")
	public String writeOk(@ModelAttribute("w") @Valid WriteDTO dto,
			BindingResult result,	
			Model model,  // Model 이 BindingResult 보다 앞에 있으면 에러..
			RedirectAttributes redirectAttrs   // redirect: 시 넘겨줄 값들
			) {
		System.out.println("writeOk : " + dto.getUid() + ":" + dto.getName());	
		String page = "board/writeOk";

결과 확인
정상적인 submit 인 경우  → 에러 개수 : 0
uid 가 없거나 숫자가 아니면 → 에러 개수 : 1

 

Data Binding
데이터 바인딩이란 프로퍼티 값을 타겟 객체에 설정해주는 것을 의미한다. 
ex) 사용자의 문자열 입력값을 어플리케이션 도메인 객체의 프로퍼티값으로 동적으로 할당해주는 것과 같은 일을 데이터 바인딩이라고 한다.

 

error 출력 도우미 메소드 추가
Errors 에 담겨있는 에러 들을 다 출력해보기

	// binding 에러 출력 도우미 메소드
	public void showErrors(Errors errors) {
		if(errors.hasErrors()) {
			System.out.println("에러개수: " + errors.getErrorCount());
			System.out.println("\t[field]\t|[code] ");
			List<FieldError> errList = errors.getFieldErrors();
			for(FieldError err : errList) {
				System.out.println("\t" + err.getField() + "\t|" + err.getCode());
			}
		} else {
			System.out.println("에러 없슴");
		}
	}

* Validator 클래스 작성
패키지 : com.lec.spring
클래스 : BoardValidator 
인터페이스 :  implements Validator

 

Validator의 메소드 supports, validate 

support() 작성

ublic class BoardValidator implements Validator {

	// 이 Validator 가 제공된 Class 의 인스턴스(clazz) 를 유효성 검사 할수 있나.
	@Override
	public boolean supports(Class<?> clazz) {
		System.out.println("supports(" + clazz.getName() + ")");
		// ↓ 검증할 객체의 클래스 타입인지 확인. 즉, WriteDTO <= clazz 가능 여부
		return WriteDTO.class.isAssignableFrom(clazz);
	}

validate() 작성

	// 주어진 객체(target) 에 유효성 검사를 수행하고
	// 유효성 검사에 오류가 있는 경우 주어진 객에체 이 오류들을 error 에 등록한다.
	@Override
	public void validate(Object target, Errors errors) {
		System.out.println("validate()");
		WriteDTO dto = (WriteDTO)target;
		
		Integer uid = dto.getUid();
		if(uid == null) {  // binding 실패하면 null 로 남아있을 것이다.
			System.out.println("uid 오류");
			// 에러등록 rejectValue(field, errorCode);
			errors.rejectValue("uid", "invalidUid");
		}
		
		String name = dto.getName();
		if(name == null || name.trim().isEmpty()) {
			System.out.println("name 오류: 반드시 한글자라도 입력해야 합니다");
			errors.rejectValue("name", "emptyName");
		}
		
		// ValidationUtils 사용
		// 단순히 빈 폼 데이터를 처리할때는 아래 와 같이 사용 가능
		// 두번째 매개변수 "subject" 은 반드시 target 클래스의 필드명 이어야 함
		// 게다가 Errors 에 등록될때도 동일한 field 명으로 등록된다. 
		ValidationUtils.rejectIfEmptyOrWhitespace(errors, "subject", "emptySubject");
	}

}

writeOk.do에 validator 객체 생성

		// validator 객체 생성
//		BoardValidator validator = new BoardValidator();
//		validator.validate(dto, result);

 

System.out.println("validate후 "); showErrors(result)

-> Binding 후 사용자가 validate() 를 호출하여 확인된 에러 추가

 

primitive 사용에 대한 고찰

binding 에러인데?  uid 값이 0 ?
binding 실패하면... 초기값으로 남아있기 때문에 
이러한 이유로..  primitive type 보단 wrapper 타입으로 설정한다.


ValidationUtils 사용
ValidationUtils 클래스
직전의 예제에서 데이터 검증을 위해서 Validator 인터페이스의 validate() 메소드를 사용하였습니다.
ValidationUtils  클래스는 validate()메소드를 좀더 편리하게 사용 할 수 있도록 고안된 클래스 입니다.

 

write.jsp, writeOk.jsp 수정

validate()에 추가

 

@Valid 와 @InitBinder  사용하기

validate() 를 스프링에서 호출하도록 설정
이전까지는 Validator 인터페이스를 구현한 클래스를 만들고, 
validate()메소드를 직접 호출하여 사용 하였습니다.

이번에는 직접 호출하지 않고, 스프링 프레임워크에서 호출하는 방법에 대해서 살펴 봅니다.

 

spring-boot-starter-validation 를 설정하면
@Valid 와 @InitBinder 를 사용하기 위한 hibernate-validator 가 이미 들어와있다.

 

컨트롤러에 @InitBinder 추가  
BoardController.java 에 추가

	// 이 컨트롤러의 클래스의 handler 에서 폼데이터를 바인딩할때 검증하는 객체 지정
	@InitBinder
	public void initBinder(WebDataBinder binder) {
		binder.setValidator(new BoardValidator());
	}

writeOk() 수정

	// BindingResult : spring validator 가 유효성 검사를 한 결과가 담긴 객체
	@RequestMapping("writeOk")
	public String writeOk(@ModelAttribute("w") @Valid WriteDTO dto,
			BindingResult result,	
			Model model,  // Model 이 BindingResult 보다 앞에 있으면 에러..
			RedirectAttributes redirectAttrs   // redirect: 시 넘겨줄 값들
			) {
		System.out.println("writeOk : " + dto.getUid() + ":" + dto.getName());	
		String page = "board/writeOk";
		
//		System.out.println("validate전 "); showErrors(result);
		
		// validator 객체 생성
//		BoardValidator validator = new BoardValidator();
//		validator.validate(dto, result);
		
		System.out.println("validate후 "); showErrors(result);
		if(result.hasErrors()) {   // 에러가 있었으면
			redirectAttrs.addFlashAttribute("w", dto);  // 원래 입력한 값 돌려주기.
			if(result.getFieldError("uid") != null) {
				redirectAttrs.addFlashAttribute("errUid", "uid 값은 0보다 큰 정수이어야 합니다");
			}
			if(result.getFieldError("name") != null) {
				redirectAttrs.addFlashAttribute("errName", "name 은 필수 입니다");
			}
			if(result.getFieldError("subject") != null) {
				redirectAttrs.addFlashAttribute("errSubject", "subject 는 필수 입니다");
			}
			page = "redirect:/board/write";   // 리턴값(String)  에 redirect:url <- 해당 url 로 redirect 한다
		}
		
		
		return page;
	}

결과 확인
supports() 가 호출됨을 알수 있다.
주어진 WriteDTO 객체가 BoardValidator 로 검증할수 있는지 여부를 먼저 supports() 로 체크하고 나서 validate() 를 수행하고 있는 것이다.

 

중요 : BindingResult 매개변수 순서

[스프링:에러] java.lang.IllegalStateException: Errors/BindingResult argument declared without preceding model attribute. Check your handler method signature!

Validate를 하는 메소드 내에서 BindingResult의 순서와 관련된 에러. BindingResult가 HttpServletRequest, HttpServletResponse, ModelMap보다 먼저 선언되어야 에러가 나지 않습니다.

 

에러시 원래 url 로 돌아가기. redirect: 사용

 

write.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>글 작성</title>
<style>
span { color: red;}
</style>
</head>
<body>
<form action="writeOk">
uid(<span>숫자</span>): 
	<input type="text" name="uid" value="${w.uid }"><span>${errUid }</span><br>
작성자(<span>*</span>): 
	<input type="text" name="name" value="${w.name }"><span>${errName }</span><br>
제목(<span>*</span>):
	<input type="text" name="subject" value="${w.subject }"><span>${errSubject }</span><br>
<input type="submit" value="등록"><br>
</form>
</body>
</html>

${errUid}

${errName}

'Spring' 카테고리의 다른 글

[SpringBoot] Security  (0) 2022.05.16
[SpringBoot] MyBatis  (0) 2022.05.13
[SpringBoot] Request Parameter  (0) 2022.05.12
[SpringBoot] RequestMapping  (0) 2022.05.12
[SpringBoot] MVC  (0) 2022.05.12