JSR-380을 이용하여 유효성 검사하기
앞서 배운 JSR-380 제약 사항을 실습 진행 중인 도서 쇼핑몰에 적응해보도록 하겠습니다.
pom.xml
유효성 검사 관련 의존 라이브러리를 추가해줍니다.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.springmvc</groupId>
<artifactId>controller</artifactId>
<name>BookMarket</name>
<packaging>war</packaging>
<version>1.0.0-BUILD-SNAPSHOT</version>
<properties>
<java-version>11</java-version>
<org.springframework-version>5.3.19</org.springframework-version>
<org.aspectj-version>1.9.9.1</org.aspectj-version>
<org.slf4j-version>1.7.36</org.slf4j-version>
<security.version>5.6.3</security.version>
<commons-fileupload-version>1.4</commons-fileupload-version>
<commons-io-version>2.11.0</commons-io-version>
</properties>
<dependencies>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${org.springframework-version}</version>
<exclusions>
<!-- Exclude Commons Logging in favor of SLF4j -->
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${org.springframework-version}</version>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>${security.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>${security.version}</version>
</dependency>
<!-- File Upload -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>${commons-fileupload-version}</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${commons-io-version}</version>
</dependency>
<!-- AspectJ -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>${org.aspectj-version}</version>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${org.slf4j-version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>${org.slf4j-version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>${org.slf4j-version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.15</version>
<exclusions>
<exclusion>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
</exclusion>
<exclusion>
<groupId>javax.jms</groupId>
<artifactId>jms</artifactId>
</exclusion>
<exclusion>
<groupId>com.sun.jdmk</groupId>
<artifactId>jmxtools</artifactId>
</exclusion>
<exclusion>
<groupId>com.sun.jmx</groupId>
<artifactId>jmxri</artifactId>
</exclusion>
</exclusions>
<scope>runtime</scope>
</dependency>
<!-- @Inject -->
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>
<!-- Servlet -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<!-- Validation -->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.4.2.Final</version>
</dependency>
<!-- Test -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.7</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-eclipse-plugin</artifactId>
<version>2.9</version>
<configuration>
<additionalProjectnatures>
<projectnature>org.springframework.ide.eclipse.core.springnature</projectnature>
</additionalProjectnatures>
<additionalBuildcommands>
<buildcommand>org.springframework.ide.eclipse.core.springbuilder</buildcommand>
</additionalBuildcommands>
<downloadSources>true</downloadSources>
<downloadJavadocs>true</downloadJavadocs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.5.1</version>
<configuration>
<source>1.6</source>
<target>1.6</target>
<compilerArgument>-Xlint:all</compilerArgument>
<showWarnings>true</showWarnings>
<showDeprecation>true</showDeprecation>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.2.1</version>
<configuration>
<mainClass>org.test.int1.Main</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
messages.properties
/src/main/resources 폴더에 메시지 리소스 파일을 다음과 같이 만들어주고 내용을 추가해줍니다.
Pattern.NewBook.bookId = \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uB3C4\uC11CID\uC785\uB2C8\uB2E4(\uC22B\uC790\uB85C \uC870\uD569\uD558\uACE0 ISBN\uC73C\uB85C \uC2DC\uC791\uD558\uC138\uC694).
Size.NewBook.name = \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uB3C4\uC11C\uBA85\uC785\uB2C8\uB2E4(\uCD5C\uC18C 4\uC790\uC5D0\uC11C \uCD5C\uB300 50\uC790\uAE4C\uC9C0 \uC785\uB825\uD558\uC138\uC694).
Min.NewBook.unitPrice = \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uAC00\uACA9\uC785\uB2C8\uB2E4(0\uC774\uC0C1\uC758 \uC218\uB97C \uC785\uB825\uD558\uC138\uC694).
Digits.NewBook.unitPrice = \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uAC00\uACA9\uC785\uB2C8\uB2E4(\uC18C\uC218\uC810 2\uC790\uB9AC\uAE4C\uC9C0, 8\uC790\uB9AC\uAE4C\uC9C0 \uC785\uB825\uD558\uC138\uC694).
NotNull.NewBook.unitPrice = \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uAC00\uACA9\uC785\uB2C8\uB2E4(\uAC00\uACA9\uC744 \uC785\uB825\uD558\uC138\uC694).
유효성 검사 시 출력할 코드와 메시지를 작성해줍니다.
코드 설정 방법은 'JSR-380 애너테이션 이름.커맨드 객체 이름.필드 이름' 으로 정의합니다.
Book.java
Book 클래스 필드에 대해서 JSR-380을 선언해줍니다.
package com.springmvc.domain;
import javax.validation.constraints.Digits;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import org.springframework.web.multipart.MultipartFile;
public class Book {
@Pattern(regexp="ISBN[1-9]+")
private String bookId;
@Size(min=4, max=50)
private String name;
@Min(value=0)
@Digits(integer=8, fraction=2)
@NotNull
private int unitPrice;
private String author;
private String description;
private String publisher;
private String category;
private long unitsInStock;
private String releaseDate;
private String condition;
private MultipartFile bookImage;
public Book() {
super();
// TODO Auto-generated constructor stub
}
public Book(String bookId, String name, int unitPrice) {
super();
this.bookId = bookId;
this.name = name;
this.unitPrice = unitPrice;
}
public String getBookId() {
return bookId;
}
public void setBookId(String bookId) {
this.bookId = bookId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getUnitPrice() {
return unitPrice;
}
public void setUnitPrice(int unitPrice) {
this.unitPrice = unitPrice;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getPublisher() {
return publisher;
}
public void setPublisher(String publisher) {
this.publisher = publisher;
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
public long getUnitsInStock() {
return unitsInStock;
}
public void setUnitsInStock(long unitsInStock) {
this.unitsInStock = unitsInStock;
}
public String getReleaseDate() {
return releaseDate;
}
public void setReleaseDate(String releaseDate) {
this.releaseDate = releaseDate;
}
public String getCondition() {
return condition;
}
public void setCondition(String condition) {
this.condition = condition;
}
public MultipartFile getBookImage() {
return bookImage;
}
public void setBookImage(MultipartFile bookImage) {
this.bookImage = bookImage;
}
}
@Pattern을 bookId의 제약사항으로 설정해주었습니다.
유효성 검사 시 bookId 값이 ISBN[1-9] + 패턴과 일치하지 않는다면 메시지 리소스 파일에
선언되어 있던 Pattern.NewBook.bookId에 설정된 메시지를 출력하게 됩니다.
@Size를 name의 제약사항으로 설정해주었습니다.
최소 4자 이상, 최대 50자 이하의 문자열 크기를 가지게 하도록 하였습니다.
만약 유효성 검사 시 위 조건에 해당하지 않으면 리소스 파일에 저장된
Size.NewBook.bookId 메시지를 출력하게 됩니다.
@Min, @Digits, @NotNull을 unitPrice의 제약사항으로 설정하였습니다.
@Min은 최솟값을 0으로 설정하였고 유효성 검사 시 조건에 만족하지 않는다면
Min.NewBook.unitPrice의 메시지를 출력하게 됩니다.
@Digits는 정수 8자리와 소수점 2자리를 가지게 하도록 하였습니다.
유효성 검사 시 조건에 만족하지 않는다면
Digits.NewBook.unitPrice의 메시지를 출력하게 됩니다.
@NotNull은 Null이 아닌 값을 갖도록 합니다.
유효성 검사 시 조건에 만족하지 않는다면
NotNull.NewBook.unitPrice의 메시지를 출력하게 됩니다.
BookController.java
submitAddNewBook() 메소드의 매개변수 중 커맨드 객체에 @Valid를 선언하고 오류 처리를 해줍니다.
package com.springmvc.controller;
import java.io.File;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.MatrixVariable;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.ModelAndView;
import com.springmvc.domain.Book;
import com.springmvc.exception.BookIdException;
import com.springmvc.exception.CategoryException;
import com.springmvc.service.BookService;
@Controller
@RequestMapping("/books")
public class BookController {
@Autowired
private BookService bookService;
@RequestMapping
public String requestBookList(Model model) {
List<Book> list = bookService.getAllBookList();
model.addAttribute("bookList", list);
return "books";
}
@RequestMapping("/all")
public ModelAndView requestAllBooks() {
ModelAndView modelAndView = new ModelAndView();
List<Book> list = bookService.getAllBookList();
modelAndView.addObject("bookList", list);
modelAndView.setViewName("books");
return modelAndView;
}
@RequestMapping("/{category}")
public String requestBooksByCategory(@PathVariable("category") String bookCategory, Model model) {
List<Book> booksByCategory = bookService.getBookListByCategory(bookCategory);
if(booksByCategory==null || booksByCategory.isEmpty()) {
throw new CategoryException();
}
model.addAttribute("bookList", booksByCategory);
return "books";
}
@RequestMapping("/filter/{bookFilter}")
public String requestBooksByFilter(
@MatrixVariable(pathVar="bookFilter") Map<String, List<String>> bookFilter, Model model) {
Set<Book> booksByFilter = bookService.getBookListByFilter(bookFilter);
model.addAttribute("bookList", booksByFilter);
return "books";
}
@RequestMapping("/book")
public String requestBookById (
@RequestParam("id") String bookId, Model model) {
Book bookById = bookService.getBookById(bookId);
model.addAttribute("book", bookById);
return "book";
}
@RequestMapping("/add")
public String requestAddBookForm(@ModelAttribute("NewBook") Book book) {
return "addBook";
}
@PostMapping("/add")
public String submitAddNewBook(@Valid @ModelAttribute("NewBook") Book book, BindingResult result) {
if(result.hasErrors())
return "addBook";
MultipartFile bookImage = book.getBookImage();
String saveName = bookImage.getOriginalFilename();
File saveFile = new File("C:\\upload",saveName);
if(bookImage!=null && !bookImage.isEmpty()) {
try {
bookImage.transferTo(saveFile);
} catch(Exception e) {
throw new RuntimeException("도서 이미지 업로드가 실패하였습니다",e);
}
}
bookService.setNewBook(book);
return "redirect:/books";
}
@ModelAttribute
public void addAttribute(Model model) {
model.addAttribute("addTitle", "신규 도서 등록");
}
@InitBinder
public void initBinder(WebDataBinder binder) {
binder.setAllowedFields("bookId","name","unitPrice","author","description","publisher","category","unitsInStock","totalPages","releaseDate","condition","bookImage");
}
@ExceptionHandler(value= {BookIdException.class})
public ModelAndView handleError(HttpServletRequest req, BookIdException exception) {
ModelAndView mav = new ModelAndView();
mav.addObject("invalidBookId",exception.getBookId());
mav.addObject("exception",exception);
mav.addObject("url",req.getRequestURL()+"?"+req.getQueryString());
mav.setViewName("errorBook");
return mav;
}
}
submitAddNewBook() 메소드는 사용자 입력 값을 NewBook 객체로 매핑 시 유효성 검사를 진행하도록 합니다.
그에 대한 결과 값은 BindingResult 타입의 result 객체에 담깁니다.
만약 유효성 검사 시 오류가 발생하여 result 객체에 있으면 addBook 이름을 가진 뷰 페이지를 반환하도록 합니다.
addBook.jsp
오류 메시지가 출력될 부분을 추가해줍니다.
<%@ page contentType="text/html; charset=utf-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<html>
<head>
<link href="<c:url value="/resources/css/bootstrap.min.css"/>" rel="stylesheet">
<title>도서 등록</title>
</head>
<body>
<nav class="navbar navbar-expand navbar-dark bg-dark">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="./home">Home</a>
</div>
</div>
</nav>
<div class="jumbotron">
<div class="container">
<h1 class="display-3">
<spring:message code="addBook.form.title.label"/>
</h1>
</div>
</div>
<div class="container">
<div class="float-right">
<form:form action="${pageContext.request.contextPath }/logout" method="POST">
<input type="submit" class="btn btn-sm btn-success" value="Logout" />
</form:form>
</div>
<div class="float-right" style="padding-right:30px">
<a href="?language=ko">Korean</a>|<a href="?language=en">English</a>
</div>
<br><br>
<form:form modelAttribute="NewBook"
action="./add?${_csrf.parameterName }=${_csrf.token }"
class="form-horizontal"
enctype="multipart/form-data">
<fieldset>
<legend><spring:message code="addBook.form.subtitle.label"/></legend>
<div class="form-group row">
<label class="col-sm-2 control-label">
<spring:message code="addBook.form.bookId.label"/>
</label>
<div class="col-sm-3">
<form:input path="bookId" class="form-control"/>
</div>
<div class="col-sm-6">
<form:errors path="bookId" cssClass="text-danger"/>
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 control-label">
<spring:message code="addBook.form.name.label"/>
</label>
<div class="col-sm-3">
<form:input path="name" class="form-control"/>
</div>
<div class="col-sm-6">
<form:errors path="name" cssClass="text-danger"/>
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 control-label">
<spring:message code="addBook.form.unitPrice.label"/>
</label>
<div class="col-sm-3">
<form:input path="unitPrice" class="form-control"/>
</div>
<div class="col-sm-6">
<form:errors path="unitPrice" cssClass="text-danger"/>
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 control-label">
<spring:message code="addBook.form.author.label"/>
</label>
<div class="col-sm-3">
<form:input path="author" class="form-control"/>
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 control-label">
<spring:message code="addBook.form.description.label"/>
</label>
<div class="col-sm-5">
<form:textarea path="description" cols="50" rows="2" class="form-control"/>
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 control-label">
<spring:message code="addBook.form.publisher.label"/>
</label>
<div class="col-sm-3">
<form:input path="publisher" class="form-control"/>
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 control-label">
<spring:message code="addBook.form.category.label"/>
</label>
<div class="col-sm-3">
<form:input path="category" class="form-control"/>
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 control-label">
<spring:message code="addBook.form.unitsInStock.label"/>
</label>
<div class="col-sm-3">
<form:input path="unitsInStock" class="form-control"/>
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 control-label">
<spring:message code="addBook.form.releaseDate.label"/>
</label>
<div class="col-sm-3">
<form:input path="releaseDate" class="form-control"/>
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 control-label">
<spring:message code="addBook.form.condition.label"/>
</label>
<div class="col-sm-3">
<form:radiobutton path="condition" value="New"/>New
<form:radiobutton path="condition" value="Old"/>Old
<form:radiobutton path="condition" value="E-Book"/>E-Book
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 control-label">
<spring:message code="addBook.form.bookImage.label"/>
</label>
<div class="col-sm-7">
<form:input path="bookImage" type="file" class="form-control"/>
</div>
</div>
<div class="form-group row">
<div class="col-sm-offset-2 col-sm-10">
<input type="submit" class="btn btn-primary" value="<spring:message code="addBook.form.button.label"/>" />
</div>
</div>
</fieldset>
</form:form>
<hr>
<footer>
<p>© BookMarket</p>
</footer>
</div>
</body>
</html>
커맨드 객체인 NewBook의 bookId, name, unitPrice의 입력 값에 대하여 유효성 검사를 진행하고
오류가 발생하였다면 <form:errors> 태그를 사용해서 출력되도록 하였습니다.
실행 결과
도서 등록 페이지로 들어가서 필드 값을 입력하고 유효성 검사에 따른 메시지가 출력되는지
확인해보도록 하겠습니다.
'SPRING' 카테고리의 다른 글
[SPRING]#52 도서 쇼핑몰 구현 (유효성 검사5) (0) | 2024.02.16 |
---|---|
[SPRING]#51 도서 쇼핑몰 구현 (유효성 검사4) (0) | 2024.02.13 |
[SPRING]#49 도서 쇼핑몰 구현 (유효성 검사2) (0) | 2024.01.23 |
[SPRING]#48 도서 쇼핑몰 구현 (유효성 검사1) (0) | 2024.01.21 |
[SPRING]#47 도서 쇼핑몰 구현 (다국어 처리4) (0) | 2024.01.21 |