Spring Validation -(1)

2022. 3. 25. 15:25Spring/Spring 실습

 

자바 validation 개요

 

우리는 web에서 데이터를 입력할 때 다음과 같이 비밀번호가

비어있는지 아닌지 등에 관한 검사를 할 수 있다.

이런 검사를 사용할 때 <input type="text"   required="required">를 사용해서 할 수도 있을 것이다.

하지만 이는 f12를 눌러서 태그에서 required="requred" 부분만 삭제하면

데이터가 넘어가서  서버 DB에 원하지 않는 값,

이를테면 비밀번호 컬럼에  null or "공백" 값이 들어갈 수도 있다. 

물론 DB에서 컬럼에 null 허용을 안 하게 할 수도 있지만 이 경우 mybatis 실행 도중 에러가 발생할 것이다. 

 

 

 

만약 서버에서 파라미터 검증이 없다면 이대로 DB에 저장될 것이다.

어떤 사용자는 나이를 int형 어떤 사용자는 String으로,  

또는 이메일을 email 형식을 지키지 않고 보낼 수 있기 때문에 

반드시 입력값 검증을 해줘야한다. 

 

 

 

잘못된 값이 온다면 mybatis에서 에러가 나게 하는것보다 사용자한테  "너 잘못입력했어"라고 알려주자.

 

파라미터가 모두 올바르게 왔다면 그때서야 DB에 저장하도록 한다.

 

 

 

 

즉, 브라우저가 요청할 때 요청을 처리하는 메소드(@RequestMapping)에서

요청에 대한 파라미터들을 검증해야 할 것이다. 

비밀번호가 비어있지않은지, '특수문자+한/영' 으로

이루어져있는지 등에 대한 검사를 서버측에서 해야한다.

예를 들어 freeForm에서  제목,작성자,비밀번호,분류, 내용등이 있다고 하자.

모두 빈값이면 안된다 .

브라우저는 사용자가 저장버튼을 누르면 "/free/freeRegist.wow"로 요청을 한다

 

 

 

이제 요청처리 메소드에서 제목,작성자,비밀번호,분류,내용 등의 값이

비어있지 않은지 검사하는 코드를 작성해야한다.

@PostMapping("/free/freeRegist.wow")
	public String freeRegist(Model model, FreeBoardVO freeBoard) {
    	if ( checkRegist(freeBoard) ){
        	return "free/freeForm";   // 제목~~내용중 뭔가 입력안한게 있으면  다시 freeForm으로
        }
        
        // 제대로 입력 했으면 DB에 저장하는 코드
		try {
			freeBoardService.registBoard(freeBoard);
			ResultMessageVO resultMessageVO = new ResultMessageVO();
			resultMessageVO.messageSetting(true, "글 등록 성공", "글을 등록했습니다.", "/free/freeList.wow", "목록으로");
			model.addAttribute("resultMessageVO", resultMessageVO);
			return "common/message";
		} catch (BizNotEffectedException ebe) {
			ResultMessageVO resultMessageVO = new ResultMessageVO();
			resultMessageVO.messageSetting(false, "등록실패", "등록에 실패했습니다.", "/free/freeList.wow", "목록으로");
			model.addAttribute("resultMessageVO", resultMessageVO);
			return "common/message";
		}
	}

 

 

checkRegist 메소드는 다음과 같다.

private  static boolean checkRegist(FreeBoardVO freeBoard) {
		boolean isError=false;
		if(freeBoard.getBoTitle().isEmpty()) isError=true;
		if(freeBoard.getBoWriter().isEmpty()) isError=true;
		if(freeBoard.getBoCategory().isEmpty()) isError=true;
		if(!freeBoard.getBoContent().isEmpty()) isError=true;
		return isError;
	}

 

이런식으로 서버측에서 자바코드로 파라미터로 넘어온 데이터들을 검사해서 ,

입력한게  없으면 다시 입력하는 식으로 구현해야한다. 

하지만 이런 기능들을 하나하나 메소드로 직접 만들려니 상당히 힘들다.

지금은 단순히 값이 비어있냐 아니냐로만 체크를 했지만,

checkRegist()메소드에서  이메일형식이 맞는지, 전화번호 형식이 맞는지 등의 다양한 검사를

해야한다면 직접 검사하는 코드를 만들기는 상당히 어려울 겁니다.

다행히 spring이 이런 데이터검사를 지원해 줍니다. 이를  Validation이라고 합니다.

 

 

Java Bean Validation

이 Bean Validation은  JSR (자바 스펙 요구서)에서 정의 된 내용이다. 

Bean Validation 3.0도 나오긴 했는데 JSR에는 없는거 같다.

https://beanvalidation.org/3.0/참고.3.0부터는 많은것이 바뀐다. 

우리는 2.0을 사용할 것이다. 

 

Bean Validation은 단순히 데이터검증을 위해서 이런게 필요하다 등의 명세서 이고

이를 실제로 구현한 hibernate-validator가 필요합니다.

Bean Validaton 2.0의 대한 버전은 hibernate-validator는 6버전입니다.

 

 

 

 

Spring Validation 적용

 

lib 추가

pom.xml에  hibernate-validator를 추가해 줍시다. 

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.2.0.Final</version>
</dependency>

 

hibernate-validator만 추가하면 maven이 자동으로  bean validation도 추가하긴 합니다만,

따로 bean validation 버전을 지정하고 싶으면 같이 추가해줍시다.

<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>2.0.1.Final</version>
</dependency>

※bean validation 버전과 그 구현체인 hibernate의 버전이 다르면 제대로 동작하지 않을 수 있습니다.

 

 

Validation 적용방법은 간단하다. 

  • 1.검사 내용을 VO에 작성
  • 2.Validation을 적용하고 싶은 곳에  @Validated() 붙이기+ 검사결과 객체 
  • 3.검사 실패했을 때의 메세지 JSP에서 보여주기.
  • 4.Class를 이용해서 상황에 따라 검사하기   (class가 아니라 Class다) 

 

1번에서 VO에 사용할 여러가지 다양한 제약조건 @들은https://beanvalidation.org/2.0/spec/#builtinconstraints에서 확인할 수 있다.

3번에서 JSP에서 사용하는 form:form 태그에 관한 내용은

https://docs.spring.io/spring-framework/docs/4.2.x/spring-framework-reference/html/spring-form-tld.html에서 확인할 수 있다. 

 

 

1.검사 내용을 VO에 작성

public class FreeBoardVO {
	private int boNo;                       /*글 번호*/
	@NotEmpty(message = "글 제목은 필수야")
	private String boTitle;                 /*글 제목*/
	@NotEmpty(message = "글 분류는 필수야")
	private String boCategory;              /*글 분류 코드*/
	@Size(min = 1, max = 10)
	private String boWriter;                /*작성자명*/
	@Size(min = 4, message = "비밀번호는 4글자 이상을 입력해주세요")
	private String boPass;                  /*비밀번호*/
	
	@Size(min = 10, message = "글 내용은 10글자는 써야지.")
	private String boContent;               /*글 내용*/
	private String boIp;                    /*등록자 IP*/
	private int boHit;                      /*조회수*/
	private String boRegDate;               /*등록 일자*/
	private String boModDate;               /*수정 일자*/
	private String boDelYn;                 /*삭제 여부*/
	
	private String boCategoryNm;            
	/*boCategory로 comm_code테이블이랑 조인한 코드이름 */
}

@NotEmpty, @NotBlank,@NotNull,@Size 등등을 사용할 수 있다.

@별로 여러가지 속성이 있지만, 'message'는 공통된 속성으로서 밑에서 행할 검사에

실패했을 떄의 사용자에게 보여줄 메시지를 적을 수 있다.

 

 

2.Validation을 적용하고 싶은 곳에 @Validated() 붙이기 +검사결과 객체

위의 예시에서처럼 자유게시판에 글을 등록하고 싶을 때 

사용자가 freeForm.jsp 화면에서 입력한 값을 검사하고 싶다고 하자.

freeReigst()메소드에서  Spring이 forward하기전에,

사용자가 입력한 값이 저장되는 FreeBoardVO객체의 값을 검사해야한다.

이 때 다음과 같이  @Validated만 붙여주면 된다.

@PostMapping("/free/freeRegist.wow")
public String freeRegist(Model model,@Validated() @ModelAttribute("freeBoard") FreeBoardVO freeBoard) {

}

 

 

Spring은 이 메소드가 실행되기전에 freeBoard값이 제대로 되어있는지 검사 하려 할 것이다.

그리고 원하는 값이 제대로 왔는지 안 왔는지  검사를 할 것이다.

그 결과를 저장할 객체를 Spring에서 지원해주고,

검사 결과에 따라, 원하는 값이 제대로 안 들어오면 다시 freeForm화면으로 넘어가게 해야 한다.

다음과 같이 작성하면 된다. 

@PostMapping("/free/freeRegist.wow")
public String freeRegist(Model model,@Validated() @ModelAttribute("freeBoard")FreeBoardVO freeBoard
,BindingResult error) {
    if(error.hasErrors()) {
        return "free/freeForm";
    }
    //원래 하려던 일.. 여기서는 DB에 freeBoard 저장
 }

 

 

여기서 글제목이 empty인 경우, 비밀번호가 3글자인 경우 등은

모두 error.hasErrors()가 true가 됩니다.

즉, "free/freeForm" 으로 가서 다시한번 사용자가 제대로 글을 입력해야 합니다.

이 때 검사 결과를 저장하는 객체는 BindingResult인데 

이 검사 결과 객체는 검사 대상 객체(FreeBoardVO freeBoard) 바로 뒤에 작성해야한다.

즉, 다음과 같이 작성하면 에러가 난다.

@PostMapping("/free/freeRegist.wow")
public String freeRegist(@Validated()  @ModelAttribute("freeBoard") FreeBoardVO freeBoard
,Model model, BindingResult error) {
//FreeBoardVO와 BindingResult 사이에  파라미터가 있으면 안된다.

 

3.검사 실패 했을 때의 메세지  JSP에서 보여주기

freeForm.jsp에서 메세지를 보여주기 위해서는 기존의 form태그로는 안 된다. 

VO에서 @NotEmpty(message="")에 작성한 message를 보여줄 수 있는 태그를 사용해야한다.

이것이 Spring에서 제공해주는 <form:form>태그이다.

이 태그를 사용하기 위해선 먼저taglib를 추가해주어야 한다.

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

 

 

기존의 <form>태그와 비슷한 역할을 아지만 model에 담긴 데이터를 이용해서

validation에  관한 내용을 지원해줍니다.

(참고로 jsp에서 <form:form>로 사용하지만,

구글에 검색할 때는  <form:form> or 폼폼 태그라고 검색하지말고 Spring form 태그로 검색합시다.)

기본적인 사용방법은 다음과 같다.

<form:form action="freeRegist.wow" method="post" modelAttribute="freeBoard">
	
</form:form>

 

 

기존의 form태그와 비슷하지만 modelAttribute 속성이 추가되었다.

Controller단에서 model객체에 담은 freeBoard를 활용한다.   

기존의 form태그 대신 <form:form>태그를 적용하고 input 태그나 hidden 태그도 그에 맞춰 바꿔보자.

 

 <input type='date'> 를  <form:date> 같이 쓰고 싶은데 없습니다.  어떻게 해야되나요?

Spring이 <form: date> 처럼 지원해주지 않는 경우에는

그냥 <input type='date'> 처럼 기존의 html태그를 작성하시면 됩니다.

또 지원해주는 경우에도 굳이 <form:input>으로바꾸지않고 그냥 <input type='text'>로 사용해도 됩니다. 

<form:input path="boTitle" cssClass="form-control input-sm" />
<input type="text" name="boTitle" class="form-control input-sm" value="${freeBoard.boTitle}" />

위 2개는 같다.  value의 값을 알아서 <form:input>태그가 화면에  보여준다.

 

 

 

freeForm.jsp

<%@page import="com.study.code.vo.CodeVO"%>
<%@page import="com.study.code.service.CommCodeServiceImpl"%>
<%@page import="com.study.code.service.ICommCodeService"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8" trimDirectiveWhitespaces="true"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<!DOCTYPE html>
<html lang="ko">
<head>
<%@include file="/WEB-INF/inc/header.jsp"%>
</head>
<body>
<%@ include file="/WEB-INF/inc/top.jsp"%>
<div class="container">
    <div class="page-header">
        <h3>
            자유게시판 - <small>글 등록</small>
        </h3>
    </div>
    <form:form action="freeRegist.wow" method="post"
        modelAttribute="freeBoard">

        <table class="table table-striped table-bordered">
            <colgroup>
                <col width="20%" />
                <col />
            </colgroup>
            <tr>
                <th>제목</th>
                <td><form:input path="boTitle" cssClass="form-control input-sm" />
                    <form:errors path="boTitle"></form:errors>
                </td>
            </tr>
            <tr>
                <th>작성자</th>
                <td><form:input path="boWriter" cssClass="form-control input-sm" />
                    <form:errors path="boWriter"></form:errors>
                </td>
            </tr>
            <tr>
                <th>비밀번호</th>
                <td><form:password path="boPass" cssClass="form-control input-sm" title="알파벳과 숫자로 4글자 이상 입력" />
                    <form:errors path="boPass"></form:errors>
                    <span class="text-danger"> <span
                        class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
                        수정 또는 삭제시에 필요합니다.
                </span></td>
            </tr>
            <tr>
                <th>분류</th>
                <td>
                <form:select path="boCategory" cssClass="form-control input-sm">
                    <form:option value="">--선택하세요--</form:option>
                    <form:options items="${cateList }" itemLabel="commNm" itemValue="commCd"/>
                    </form:select>
                <form:errors path="boCategory"></form:errors>
                </td>
            </tr>
            <tr>
                <th>IP</th>
                <td><%=request.getRemoteAddr()%> <input type="hidden"
                    name="boIp" value="<%=request.getRemoteAddr()%>"></td>
                    <!-- form:form태그에서 기존의 input type=text 쓸수있음 -->
                    <!-- input type=date도 사용가능,  form태그가 제공안하는 건 
                      기존의 input으로 쓰세요-->
            </tr>
            <tr>
                <th>내용</th>
                <td>
                    <form:textarea path="boContent" cssClass="form-control" rows="10"/>
                    <form:errors path="boContent"></form:errors>
                </td>
            </tr>
            <tr>
                <td colspan="2">
                    <div class="pull-left">
                        <a href="freeList.wow" class="btn btn-default btn-sm"> <span
                            class="glyphicon glyphicon-list" aria-hidden="true"></span>
                            &nbsp;&nbsp;목록
                        </a>
                    </div>
                    <div class="pull-right">
                        <button type="submit" class="btn btn-sm btn-primary">
                            <span class="glyphicon glyphicon-save" aria-hidden="true"></span>
                            &nbsp;&nbsp;저장
                        </button>
                    </div>
                </td>
            </tr>
        </table>
    </form:form>

</div>
<!-- container -->
</body>
</html>

 

 

여기까지 작성하면 잘 작동할 거 같지만  freeForm.wow로 요청 시 에러가 난다.

왜냐하면 JSP에서 <form:form modelAttribute="freeBoard">태그로 바꾸었다. 

freeForm.wow로 요청해서 freeForm.jsp로 가든, freeRegist.wow로 요청해서 freeForm.jsp로 가든

freeForm.jsp입장에서는 model에 "freeBoard" 라는 이름의 FreeBoardVO데이터가 담겨있어야 한다.

freeRegist.wow 요청에서는 freeForm.jsp로 갈 때 model에 "freeBoard" 가 담기지만

freeForm.wow 요청에서는  freeForm.jsp로 갈 때 model에 "freeBoard" 가 담기지 않는다.

@GetMapping("/free/freeForm.wow")
public String freeForm(Model model) {
    return "free/freeForm";
}

 

 

그래서 freeForm.wow로 요청할 때 파라미터가 없어도 model에

"freeBoard"가 담기도록 freeForm메소드를 수정해주면 된다.

@GetMapping("/free/freeForm.wow")
public String freeForm(Model model, @ModelAttribute("freeBoard") FreeBoardVO freeBoard) {
    //FreeBoardVO freeBoard=new FreeBoardVO();
    //model.addAttribute("freeBoard", freeBoard);     
    return "free/freeForm";
}

 

 

여기까지 했으면 다음과 같이 나온다.

freeForm.wow로 요청했을 때 freeForm.jsp 화면

 

 

 

아무것도 입력하지 않았을 때 boTitle, boWriter 등이 VO에서 정의한 @NotEmpty 등의 조건에 걸려서

@PostMapping("/free/freeRegist.wow")
public String freeRegist(Model model, @Validated() @ModelAttribute("freeBoard")FreeBoardVO freeBoard
        ,BindingResult error) {
    if(error.hasErrors()) {
        return "free/freeForm";
    }
    //원래 코드
}

에 의해 freeForm.jsp로 포워딩 됐다.  즉 freeForm.wow에서 그냥 freeForm.jsp로 갔을 때랑

freeRegist.wow에서  freeBoard검사했는데 에러가 있어서 freeForm.jsp로 갔을때를 구별해야한다.

이 때 freeForm.jsp에서는 <form:errors>태그에 의해서 VO에서 설정한 message가 보이게 된다.

 

 

여기까지 데이터 검증에 대해 해보았다.  다시한번 Validation 적용 방법을 보자.

  • 1.검사 내용을 VO에 작성
  • 2.Validation을 적용하고 싶은 곳에  @Validated() 붙이기+ 검사결과 객체 
  • 3.검사 실패했을 때의 메세지 JSP에서 보여주기.
  • 4.Class를 이용해서 상황에 따라 검사하기

여기까지 3번에 해당된다.

 

 

 

4.Class를 이용해서 상황에 따라 검사하기.

참고로 여기서 말하는 Class는 자바의 Class  클래스를 의미한다. (자바 Reflection)

지금까지한건 freeForm,freeRegist 상황에서의 validation 검사였다.

브라우저(client)에서 freeReigst.wow로 올 때 freeBorad에는 boNo필드를 검사 할 필요가 없었다. 

하지만 freeEdit,freeModfiy 에서는 boNo필드를 검사해야한다. 

boNo가지고 DB에서 해당 글만 업데이트해야된다.

그래서 boNo값이 반드시 넘어오는지 검사를 하고 싶다.

(물론, edit->modify 상황에서 사용자가 직접 글 번호를 입력하지는 않는다.

다음과 같이 FreeBoardVO에  제약조건을 걸어주자.

public class FreeBoardVO {
	@Positive(message = "글번호는 양수여야합니다.") //int는 @NotNull,@NotEmpty,@NotBlank등은 의미 없음. 기본값이 0이기 때문
	private int boNo;                       /*글 번호*/

 

 

Modify에서는 boNo가 파라미터로 넘어가 해당 검사를 통과할 것이다.

하지만 Regist에서는 boNo가 파라미터로 넘어가지 않는다. 그래서 통과하지 못할 것이다.

그런데 Regist에서는 boNo를 파라미터로 넘길 필요가 없다.

DB에 삽입 할 때 sequence를 이용하기 때문에 파라미터로 넘길 이유가 없기 때문이다.

그렇다면 boNo필드를 Modify상황에서는 검사하고, Regist에서는 검사하지 않아야 한다.

이를 Spring에서는 Class 클래스를 Validtion 관련 어노테이션에 매개변수로 줘서 구별한다.

 

다음과 같이 interface를 만들자.

 interface만 valdiation groiuping에 사용될 수 있다. class는 안됨)

 

Modify.java

package com.study.common.valid;
public interface Modify {
}

 

 

이제 FreeBoardVO, FreeBoardController에 적용해보자.

Default의 import는 javax.validation.groups.Default;  이다.

 

FreeBoardVO

public class FreeBoardVO {
	//int 에는 notEmpty, notBlank 안됨
	@Positive(message = "글번호는 필수에욤, 0이면 안되요"
			,groups = {Modify.class})
	private int boNo;                       /*글 번호*/
	
	@NotEmpty(message = "글 제목은 필수야", groups = {Default.class})
	private String boTitle;                 /*글 제목*/
	@NotEmpty(message = "글 분류는 필수야")
	private String boCategory;              /*글 분류 코드*/
    
    
    // 기타 다른 필드

 

FreeBoardController

    @PostMapping("/free/freeModify.wow")
    public String freeModify(Model model
        ,@ModelAttribute("freeBoard")
        @Validated(value = {Modify.class,Default.class})FreeBoardVO freeBoard
        ,BindingResult error ) {  
    }
    
    
    @PostMapping("/free/freeRegist.wow")
    public String freeRegist(Model model
        ,@ModelAttribute("freeBoard")@Validated(Default.class) //@Validated()랑 같다.
        FreeBoardVO freeBoard
        ,BindingResult error) {

    }

 

기본적으로 VO에 groups를 지정을 안해주면 Default.class로  groups를 지정한거랑 같다.

freeModify에서는 groups를 Modify.class, Default.class로 지정했기 때문에

boNo와 그외 필드를 검사하고 freeRegist에서는 Default.class만 지정했기때문에

boNo는 검사하지않는다.