Validator 인터페이스와 JSR-380을 연동한 유효성 검사
앞서 실습에서 Validator 인터페이스를 사용하여 유효성 검사를 진행해 보았습니다.
그러나 앞서 작성한 도서 ID와 도서명에 대해서는 사용자 애너테이션을 적용했음에도
오류 메시지가 출력되지 않은 것을 확인했습니다.
이를 해결하기 위해서 Validator 인터페이스와 JSR-380을 연동하여 유효성 검사를
진행해보겠습니다.
BookValidator.java
com.springmvc.validator 패키지에 BookValidator 클래스를 생성해줍니다.
package com.springmvc.validator;
import java.util.HashSet;
import java.util.Set;
import javax.validation.ConstraintViolation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import com.springmvc.domain.Book;
public class BookValidator implements Validator{
@Autowired
private javax.validation.Validator beanValidator;
private Set<Validator> springValidators;
public BookValidator() {
springValidators = new HashSet<Validator>();
}
public void setSpringValidators(Set<Validator> springValidators) {
this.springValidators = springValidators;
}
public boolean supports(Class<?> clazz) {
return Book.class.isAssignableFrom(clazz);
}
public void validate(Object target, Errors errors) {
Set<ConstraintViolation<Object>> violations = beanValidator.validate(target);
for (ConstraintViolation<Object> violation : violations) {
String propertyPath = violation.getPropertyPath().toString();
String message = violation.getMessage();
errors.rejectValue(propertyPath, "",message);
}
for (Validator validator: springValidators) {
validator.validate(target, errors);
}
}
}
bean validation 인스턴스를 선언합니다. (JSR-380 validator)
spring validation 인스턴스를 선언합니다. (Validator 인터페이스)
BookValidator 클래스의 생성자를 작성해주고
Book 클래스의 유효성 검사 메소드를 작성해줍니다.
여기서 bean validation오류와 spring validation 오류를 저장해줍니다.
Book.java
유효성 검사에 따른 오류 메시지를 가져오도록 내용을 수정해줍니다.
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 com.springmvc.validator.BookId;
import org.springframework.web.multipart.MultipartFile;
public class Book {
@BookId
@Pattern(regexp="ISBN[1-9]+", message="{Pattern.NewBook.bookId}")
private String bookId;
@Size(min=4, max=50, message="{Size.NewBook.name}")
private String name;
@Min(value=0, message="{Min.NewBook.unitPrice}")
@Digits(integer=8, fraction=2, message="Digits.NewBook.unitPrice}")
@NotNull(message="{NotNull.NewBook.unitPrice}")
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;
}
}
BookId.java
마찬가지로 BookId 클래스도 수정해줍니다.
package com.springmvc.validator;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
@Constraint(validatedBy=BookIdValidator.class)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface BookId {
String message() default "{BookId.NewBook.bookId}";
Class<?>[] groups() default {};
Class<?>[] payload() default {};
}
BookController.java
UnitsInStockValidator 클래스를 BookValidator 클래스로 수정해줍니다.
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;
import com.springmvc.validator.BookValidator;
import com.springmvc.validator.UnitsInStockValidator;
@Controller
@RequestMapping("/books")
public class BookController {
@Autowired
private BookService bookService;
@Autowired
private UnitsInStockValidator unitsInStockValidator;
@Autowired
private BookValidator bookValidator;
@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.setValidator(bookValidator);
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;
}
}
servlet-context.xml
유효성 검사 관련 빈을 설정해줍니다.
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->
<!-- Enables the Spring MVC @Controller programming model -->
<annotation-driven enable-matrix-variables="true" validator="validator"/>
<!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory -->
<resources mapping="/resources/**" location="/resources/" />
<!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<beans:property name="prefix" value="/WEB-INF/views/" />
<beans:property name="suffix" value=".jsp" />
</beans:bean>
<context:component-scan base-package="com.springmvc.*" />
<beans:bean id="multipartResolver"
class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<beans:property name="maxUploadSize" value="10240000"/>
</beans:bean>
<beans:bean id="messageSource"
class="org.springframework.context.support.ResourceBundleMessageSource">
<beans:property name="basename" value="messages"/>
<beans:property name="defaultEncoding" value="UTF-8"/>
</beans:bean>
<beans:bean id="localeResolver"
class="org.springframework.web.servlet.i18n.SessionLocaleResolver">
<beans:property name="defaultLocale" value="ko"/>
</beans:bean>
<interceptors>
<beans:bean class="com.springmvc.interceptor.MonitoringInterceptor"/>
<beans:bean class="com.springmvc.interceptor.AuditingInterceptor"/>
<beans:bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
<beans:property name="paramName" value="language"/>
</beans:bean>
</interceptors>
<beans:bean id="validator"
class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
<beans:property name="validationMessageSource" ref="messageSource"/>
</beans:bean>
<beans:bean id="unitsInStockValidator"
class="com.springmvc.validator.UnitsInStockValidator"/>
<beans:bean id="bookValidator" class="com.springmvc.validator.BookValidator">
<beans:property name="springValidators">
<beans:set>
<beans:ref bean="unitsInStockValidator"/>
</beans:set>
</beans:property>
</beans:bean>
</beans:beans>
<beans:annotation-driven> 요소에 등록한 LocalValidatorFactoryBean 빈 객체의 id 이름을 이용해서
validator="id 이름"으로 설정해줘야 합니다.
JSR-380 유효성 검사를 위해 LocalValidatorFactoryBean 클래스를 빈으로 등록해줍니다.
Validator 인터페이스의 구현체인 UnitsInStockValidator 빈 객체를 등록합니다.
JSR-380과 Validator 인터페이스를 서로 연동하려고 생성한 BookValidator 빈 객체를 등록해줍니다.
실행 결과
/books/add를 입력하고 도서 등록 페이지에서 앞서 입력한 것과 똑같이 입력해주고
출력되는 오류 메시지를 확인해봅니다.
'SPRING' 카테고리의 다른 글
[SPRING]#57 도서 쇼핑몰 구현 (RESTful 웹 서비스2) (0) | 2024.02.18 |
---|---|
[SPRING]#56 도서 쇼핑몰 구현 (RESTful 웹 서비스1) (0) | 2024.02.18 |
[SPRING]#54 도서 쇼핑몰 구현 (유효성 검사7) (0) | 2024.02.17 |
[SPRING]#53 도서 쇼핑몰 구현 (유효성 검사6) (0) | 2024.02.17 |
[SPRING]#52 도서 쇼핑몰 구현 (유효성 검사5) (0) | 2024.02.16 |