Spring Validation

MisCaminos·2021년 4월 3일
0

Server & Web

목록 보기
21/23
post-thumbnail

MVC pattern에서는 form을 통해 client로 부터 data를 받아온다. Form에 입력된 data를 business layer에서 처리하거나 persistence layer쪽(DBMS쪽)으로 전송하기전에 data를 validate하는 절차를 거쳐서 data를 검증할 수 있다.

간단하게 입력 data의 에러를 javascript를 통해 client에게 inform할수도있지만, Controller에서 재검증하는 방법도 있다. 정상적인 form을 우회해서 전송된 데이터를 검증하기위해 Controller에서 재검증을 진행한다.

두 가지 재검증 방법을 실습해보았다:

1. Validator 클래스

Validator를 implement하는 validator 클래스를 통해 재검증 조건 설립 후 Controller에서 재검증

org.springframework.validation.Validator interface를 상속받는 VOValidator.java 클래스를 생성해서 여기에서 override한 validate() 메소드를 Controller에서

2. Validation Annotation

Annotation을 사용해서 더욱 간결한 코드로 Controller에서 재검증

javax.validation.constraints package의 클래스를 재검증하려는 data를 담는 VO 클래스로 import해서 data 재검증 조건을 세울수 있다. 그리고 재검증을 진행하는 Controller 클래스에서 javax.validation.Valid 클래스를 import해서 form에서 넘어오는 data를 바로 검증한다.

spring boot에서 gradle로 project build를 진행했기때문에
build.gradle에 아래와 같은 dependency를 추가했다.

implementation 'org.springframework.boot:spring-boot-starter-validation' 

1. Validator 클래스 사용방식

CalcVO.java:

package com.example.validator;

public class CalcVO {
	  private String menu;
	  private int price;
	  private int count;
	  private int payment;
	  public String getMenu() {
	    return menu;
	  }
	  public void setMenu(String menu) {
	    this.menu = menu;
	  }
	  public int getPrice() {
	    return price;
	  }
	  public void setPrice(int price) {
	    this.price = price;
	  }
	  public int getCount() {
	    return count;
	  }
	  public void setCount(int count) {
	    this.count = count;
	  }
	  public int getPayment() {
	    return payment;
	  }
	  public void setPayment(int payment) {
	    this.payment = payment;
	  }
}

CalcValidator.java:

package com.example.validator;

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

//controller에서 form의 데이터 유효성 검증
//Validator interface 상속받아 supports, validate method override해서 사용가능
public class CalcValidator implements Validator {

	@Override
	public boolean supports(Class<?> clazz) {
		//검증할 객체의 클래스 타입 정보 명시
		//<?>는 generic type. 어떤 클래스를 검증할 것인지 사용 시 지정.
		return CalcVO.class.isAssignableFrom(clazz);
	}

	@Override
	public void validate(Object target, Errors errors) {
		
		CalcVO calcVO = (CalcVO)target;
		//form submit시, Object 타입으로 from에서 정보를 받아옴.
		//그 중 menu, price, count사용
		
		String menu = calcVO.getMenu();
		if(menu == null || menu.trim().isEmpty()) {
			System.out.println("menu 등록이 누락 되었습니다.");
			errors.rejectValue("menu", "error"); //임의값 지정, 에러발생 나타냄
		}
		
		int price = calcVO.getPrice();
		if(price<1000 || price >=1000000) {
			System.out.println("금액은 1000원 이상 천만원 이하여야합니다");
			errors.rejectValue("price","error");
		}
		
		int count = calcVO.getCount();
		if(count <=0 || count >=1000) {
			System.out.println("수량은 1개이상 100개 이하여야합니다");
			errors.rejectValue("count","error");
		}
	}
}

CalcController.java:

package com.example.validator;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class CalcController {
	public CalcController() {
		System.out.println("-----> CalcController created");
	}
	
	@GetMapping("/calc")
	public String calc() {
		return "/calc/form"; 
	}
	
	@PostMapping("/calc")
	public String calc(CalcVO vo, BindingResult result, Model model) {
		CalcValidator validator = new CalcValidator();
		validator.validate(vo, result); //검증 (서버단에서의 검증)
		
		if(result.hasErrors()) {
			return "/calc/form"; //오류로 다시 form으로 되돌아 감.
		}else {
			//에러 미 발생이면 정상 처리될 페이지로 데이터를 보낸다
			int payment = vo.getPrice() * vo.getCount();
			model.addAttribute("payment", payment);
			
			return "/calc/proc";
		}
	}
}

form.jsp:

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
  <title>form.jsp</title>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>
</head>
<body>
 
<div class="container">
  <h2>form.jsp</h2>
  <form class="form-horizontal" method="post" action="./calc">
    <div class="form-group">
      <label class="control-label col-sm-2" for="menu">메뉴명:</label>
      <div class="col-sm-8">
        <input type="text" class="form-control" autofocus="autofocus" id="menu" value="김밥" name="menu">
      </div>
    </div>
    <div class="form-group">
      <label class="control-label col-sm-2" for="price">가격:</label>
      <div class="col-sm-8">          
        <input type="number" class="form-control" id="price" name="price" value="3000">
      </div>
    </div>
    <div class="form-group">
      <label class="control-label col-sm-2" for="count">수량:</label>
      <div class="col-sm-8">          
        <input type="number" class="form-control" id="count" name="count" value="2">
      </div>
    </div>
    <div class="form-group">        
      <div class="col-sm-offset-2 col-sm-10">
        <button type="submit" class="btn btn-default">처리</button>
      </div>
    </div>
  </form>
</div>
 
</body>
</html>

proc.jsp:

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
  <title>proc.jsp</title>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>
</head>
<body> 
 
<div class="container">
  <h2>메뉴명</h2>
  <div class="panel panel-default">
    <div class="panel-body">${param.menu }</div>
  </div>
  <h2>가격</h2>
  <div class="panel panel-default">
    <div class="panel-body">${param.price }</div>
  </div>
  <h2>수량</h2>
  <div class="panel panel-default">
    <div class="panel-body">${param.count }</div>
  </div>
  <h2>결제</h2>
  <div class="panel panel-default">
    <div class="panel-body">${payment }</div>
  </div>
</div>
</body>
</html>

2. Validation Annotation 사용방식

CalcVOAnnotation.java:

package com.example.validator;

import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Size;

public class CalcVOAnnotation {
	
	/**
	 * 이렇게 annotation으로 명시된 CalcVOAnnotation을 사용하는 경우, 
	 * CalcValidator와 같이 validation내용을 구현하는 클래스가 필요없음.
	 */
	
	// 우선순위: @NotEmpty -> @Size
	@NotEmpty(message = "메뉴명은 필수 입력입니다(Not empty).")
	@Size(min = 2, max = 30, message = "메뉴명은 2자이상 30자 미만입니다.")
	private String menu;
	@Max(value = 1000000, message = "금액은 100만원 이하여야 합니다.")
	@Min(value = 1000, message = "금액은 1000원 이상이어야 합니다.")
	private int price;
	@Max(value = 100, message = "수량은 100개 이하여야 합니다.")
	@Min(value = 1, message = "수량은 1개 이상이어야 합니다.")
	private int count;
	private int payment;

	public String getMenu() {
		return menu;
	}

	public void setMenu(String menu) {
		this.menu = menu;
	}

	public int getPrice() {
		return price;
	}

	public void setPrice(int price) {
		this.price = price;
	}

	public int getCount() {
		return count;
	}

	public void setCount(int count) {
		this.count = count;
	}

	public int getPayment() {
		return payment;
	}

	public void setPayment(int payment) {
		this.payment = payment;
	}
}

CalcController3.java:

package com.example.validator;

import java.util.HashMap;
import java.util.Map;

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.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class CalcController3 {
	public CalcController3() {
		System.out.println("---->CalcController3 created.");
	}

	// http://localhost:8000/calc3
	@GetMapping("/calc3")
	public String calc() {
		return "/calc/form3";
	}

	// 에러가 발생했을때 자동으로 이전 폼으로 이동
	@PostMapping("/calc3")
	public String calc(@Valid CalcVOAnnotation calcVO, BindingResult result, Model model) {

		Map<String, String> errors = new HashMap<String, String>();

		if (result.hasErrors()) { // 에러 발생시
			if (result.getFieldError("menu") != null) {
				System.out.println("menu: " + result.getFieldError("menu").getDefaultMessage());
				errors.put("menu", "menu가 등록이 누락되었습니다.");
			}

			if (result.getFieldError("price") != null) {
				System.out.println("price: " + result.getFieldError("price").getDefaultMessage());
				errors.put("price", "금액은 1000원이상 천만원 이하여야합니다");
			}

			if (result.getFieldError("count") != null) {
				System.out.println("count: " + result.getFieldError("count").getDefaultMessage());
				errors.put("count", "수량은 1개이상 천개 이하여야합니다.");
			}

			model.addAllAttributes(errors);
			return "/calc/form3";
		} else { // 에러 미발생
			int payment = calcVO.getPrice() * calcVO.getCount();
			model.addAttribute("payment", payment);
			return "/calc/proc";
		}
	}
}

form3.jsp:

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
  <title>form3.jsp</title>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>
</head>
<body>
 
<div class="container">
  <h2>form3.jsp</h2>
  <form class="form-horizontal" method="post" action="calc3">
    <div class="form-group">
      <label class="control-label col-sm-2" for="menu">메뉴명:</label>
      <div class="col-sm-8">
        <input type="text" class="form-control" autofocus="autofocus" id="menu" value="김밥" name="menu">
      </div>      
    </div>
    <div class="form-group">
    <label class="control-label col-sm-10" style="color:purple;" >${menu}</label>
    </div>
    <div class="form-group">
      <label class="control-label col-sm-2" for="price">가격:</label>
      <div class="col-sm-8">          
        <input type="number" class="form-control" id="price" name="price" value="3000">
      </div>
    </div>
    <div class="form-group">
    <label class="control-label col-sm-10" style="color:purple;">${price}</label>
    </div>
    <div class="form-group">
      <label class="control-label col-sm-2" for="count">수량:</label>
      <div class="col-sm-8">          
        <input type="number" class="form-control" id="count" name="count" value="2">
      </div>
    </div>
    <div class="form-group">
    <label class="control-label col-sm-10" style="color:purple;" >${count}</label>
    </div>
    <div class="form-group">        
      <div class="col-sm-offset-2 col-sm-10">
        <button type="submit" class="btn btn-default">처리</button>
      </div>
    </div>
  </form>
</div>
</body>
</html>

References:

  1. Spring Validation Examples - Spring MVC from Validator from JournalDev

  2. Java Bean Validation Basics from Baeldung

  3. Form 값의 검증, Validation 사용 from lectureblue

profile
Learning to code and analyze data

0개의 댓글