티스토리 뷰

Chapter13. 유효성 검사 : 도서 등록 페이지의 오류 메시지 출력하기

 

유효성 검사는 폼 페이지에서 입력 항목의 데이터 값이 서버로 전송되기 전에 정해진 규정으로 정확히 입력되었는지 계산 결과 등이 타당한지 검사하는 것이다.

 

 

13.3 사용자 정의 애너테이션으로 유효성 검사

속성 값의 중복 여부를 체크하는 유효성 검사는 JSR-380 제약 사항으로는 불가능하다.

그러므로 제약 사항을 사용자가 정의하여 사용하면 된다.

사용자 정의 제약 사항을 이용한 유효성 검사는 속성 값의 중복 여부를 비롯해 다양한 제약 사항을 만들어 사용할 수 있어 유용하다.

 

 

사용자 정의 애너테이션을 이용한 처리 과정

 

사용자 정의 애너테이션을 @interface를 사용해 생성한다.

유효성 검사 클래스의 구현체를 생성한다.

@Valid를 이용해 유효성 검사를 하고 뷰 페이지에서 <form:errors>태그로 오류 메시지를 출력한다.

 

 

 

 

 

13.3.1 사용자 정의 애너테이션 생성

사용자 정의 애너테이션은 JSR-380 처럼 도메인 클래스의 멤버 변수에 선언할 수 있는 제약 사항이다.

내장된 제약 사항이 아니기때문에 사용자가 직접 생성해 사용할 수 있다.

 

 

아래는 사용자 정의 애너테이션을 생성하는 형식이다.

@Constraint(validatedBy=유효성 검사 클래스.class)
@Target(속성
@Retention(속성
@Documented
public @interface 사용자 정의 애너테이션 이름 {
    String message() default "출력할 오류 메시지";
    Class<?>[] groups() default {};
    Class<?>[] payload() default {};
}

 

* @interface는 애너테이션을 만드는 키워드이다.

* @Constraint , @Target , @Retention , @Documented 은 애너테이션 정책, 속성을 설정한다.

* @Documented는 자바 문서에 문서화 여부를 결정한다.

* @Retention은 애너테이션의 지속 시간을 설정한다.

* @Target은 필드, 메서드, 클래스 등 애너테이션을 작성한다.

* <?>는 데이터타입을 지정하지 않은 것이다. 그러므로 어떤 데이터타입이 들어갈지 모른다. 처음 들어간 데이터 타입을 사용한다.

 

 

 

사용자 정의 애너테이션의 필수 속성

속성 설명
message 유효성 검사에서 오류가 발생하면 반환되는 기본 메시지이다.
groups 특정 유효성 검사를 그룹으로 설정한다.
payload 사용자가 추가한 정보를 전달하는 값이다.

 

 

 

@Retention의 속성 : 라이프 사이클 (어떧ㄹ떄 사용이 되고 운영이되는지)

속성 설명
Source 소스 코드까지만 유지한다. 즉, 컴파이랗면 해당 애너테이션 정보는 사라진다.
Class 컴파일한 .class 파일에 유지한다. 즉, 런타임을 할 때 클래스를 메모리로 읽어 오면 해당 정보는 사라진다.
Runtime 런타임을 할 때도 .class 파일에 유지한다. 사용자 정의 애너테이션을 만들 때 주로 사용한다.

 

* .class은 소스파일로 이것을 기계어로 바꿔준다.

* Runtime을 사용하는 이유는 언제 시작하고 끝나는지 표기를 해야하기 때문이다.

 

 

 

 

@Target의 속성 : 목표 (어디에서 사용할지 / 사용처지정)

속성 애너테이션 적용 시점
TYPE class, interface, enum
FIELD 클래스의 멤버 변수
METHOD 메서드
PARAMETER 메서드 인자
CONSTRUCTOR 생성자
LOCAL_VARIABLE 로컬 변수
ANNOTATION_TYPE 애너테이션 타입에만 적용
PACKAGE 패키지
TYPE_PARAMETER 제네릭 타입 변수 (ex) MyClass<T>)
TYPE_USE 어떤 타입에도 적용 (ex) extends, implements, 객체 생성할 때 등)

 

 

 

 

13.3.2 ConstraintValidator 인터페이스의 구현체 생성

사용자 정의 애너테이션의 유효성 검사 클래스는 javax.validation.ConstraintValidator 인터페이스의 구현체를 생성한다.

이러한 구현체를 생성하려면 initialize()와 isValid() 메서드를 구현해야 한다.

 

 

 

ConstraintValidator 인터페이스의 메서드

유형 설명
void initialize(A constraintAnnotation) 사용자 정의 애너테이션과 관련 정보를 읽어 초기화한다. 이때 A는 사용자 정의 제약 사항을 설정한다.
boolean isValid(T value, ConstraintValidatorContext contxt) 유효성 검사 로직을 수행한다. value는 유효성 검사를 위한 도메인 클래스의 변수 값이고, context는 제약 사항을 평가하는 컨텍스트이다.

 

 

 

 

13.3.3 사용자 정의 애너테이션을 이용하여 유효성 검사하기

 

 

 

 

1. messages.properties 파일에 메시지를 추가한다.

 

messages.properties

Pattern =\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 =\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 =\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uAC00\uACA9\uC785\uB2C8\uB2E4(0\uC774\uC0C1\uC758 \uC218\uB97C \uC785\uB825\uD558\uC138\uC694)
Digits =\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 = \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uAC00\uACA9\uC785\uB2C8\uB2E4(\uAC00\uACA9\uC744 \uC785\uB825\uD558\uC138\uC694).
BookId.NewBook.bookId = \uB3C4\uC11CID\uAC00 \uC774\uBBF8 \uC874\uC7AC\uD569\uB2C8\uB2E4.

 

 

 

2. Book 클래스에 bookId 속성에 대해 사용자 정의 제약 사항의 애너테이션을 선언한다. (@BookId)

 

 

Book.java

package com.springmvc.domain;

...

public class Book 
{
	@BookId
	
	@Pattern(regexp="ISBN[1-9]+", message="{Pattern.NewBook.bookId}")
	private String bookId;
	
	....

 

 

 

 

3. com.springmvc.validator 패키지를 생성하고 패키지에 BookId 클래스를 생성하고 내용을 작성한다.

 

 

BookId.java

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 {};  
}

 

 

 

BookIdValidator.java

package com.springmvc.validator;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

import org.springframework.beans.factory.annotation.Autowired;

import com.springmvc.domain.Book;
import com.springmvc.exception.BookIdException;
import com.springmvc.service.BookService;

public class BookIdValidator implements ConstraintValidator<BookId, String>{
	
	@Autowired
	private BookService bookService;
	
	public void initialize(BookId constraintAnnotatiion) {
		
	}
	
	public boolean isValid(String value, ConstraintValidatorContext context) {
		Book book;
		
		try {
			book = bookService.getBookById(value);
		}
		catch(BookIdException e) {
			return true;
		}
		if(book != null) {
			return false;
		}
		return true;
	}
}

 

 

 

BookController.java

 

@PostMapping("/add")
public String submitAddNewBook(@Valid @ModelAttribute("NewBook") Book book, BindingResult result, HttpServletRequest request) {

    if(result.hasErrors()) {
        return "addBook";
    }

    MultipartFile bookImage = book.getBookImage();

 

 

 

addBook.jsp

<div class="form-group row">
    <label class="col-sm-2 control-label">
    <!-- <label class="col-sm-2 control-label">도서ID</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>

 

 

 

 

 

 


13.4 Validator 인터페이스로 유효성 검사

 

 

 

13.4.1 유효성 검사 과정

 

 

 

13.4.2 Validator 인터페이스의 구현체 생성

 

 

Vaildator 인터페이스의 메서드

메서드 설명
boolean supports(Class<?> clazz) 주어진 객체(class)에 대해 유효성 검사를 수행할 수 있는지 검사할 목적으로 사용된다.
void validate(Object target, Errors errors) 주어진 객체(target)에 대해 유효성 검사를 수행하고 오류가 발생하면 주어진 Errors 타입의 errors 객체에 오류 관련 정보를 저장한다.

 

 

 

Errors 객체의 주요 메서드

메서드 설명
void rejectValue(String field, String errorCode, String defaultMessage) 설명된 field가 유효성 검사를 할 떄 오류를 발생시키면 설정된 errorCode와 함꼐 거부한다.
void reject(String errorCode, String defaultMessage) 유효성 검사를 할 때 오류가 발생하면 설정된 errorCode를 사용하여 도메인 객체에 대한 전역 오류로 사용한다.

 

 

 

 

13.4.3 @InitBinder를 선언한 메서드 추가

 

 

 

 

13.4.4 Validator 인터페이스를 사용하영 유효성 검사하기

 

messages.porperties

Pattern =\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 =\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 =\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uAC00\uACA9\uC785\uB2C8\uB2E4(0\uC774\uC0C1\uC758 \uC218\uB97C \uC785\uB825\uD558\uC138\uC694)
Digits =\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 = \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uAC00\uACA9\uC785\uB2C8\uB2E4(\uAC00\uACA9\uC744 \uC785\uB825\uD558\uC138\uC694).
BookId.NewBook.bookId = \uB3C4\uC11CID\uAC00 \uC774\uBBF8 \uC874\uC7AC\uD569\uB2C8\uB2E4.
UnitsInStockValidator.message = \uAC00\uACA9\uC774 10000\uC6D0 \uC774\uC0C1\uC778 \uACBD\uC6B0\uC5D0\uB294 99\uAC1C \uC774\uC0C1\uC744 \uB4F1\uB85D\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.

 

 

 

UnitsInStockVaildator.java

package com.springmvc.validator;

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

import com.springmvc.domain.Book;

@Component
public class UnitsInStockValidator implements Validator {

	public boolean supports(Class<?> clazz){
		return Book.class.isAssignableFrom(clazz);	
	}
	
	public void validate(Object target, Errors errors) {
		Book book = (Book) target;
		if (book.getUnitPrice() >= 10000 && book.getUnitsInStock() > 99) {
			errors.rejectValue("unitsInStock", "UnitsInStockValidator.message");
		}
	}
}

 

 

BookController.java

package com.springmvc.controller;

...

@Controller
@RequestMapping("/books")
public class BookController {
	@Autowired
	private BookService bookService;

	
	@Autowired //unitsInStockValidator 의 인스턴스 선언 p.368 private
	UnitsInStockValidator unitsInStockValidator;
	
	...
	
	@InitBinder
	public void initBinder(WebDataBinder binder) {
		binder.setValidator(bookValidator);  //생성한 unitsInStockValidator 설정되어 있던걸 bookValidator로 수정
		binder.setAllowedFields("bookId","name","unitPrice","author","description","publisher","category","unitsInStock","totlaPages","releaseDate","condition","bookImage");
	}
	
	...

}

 

@PostMapping("/add")
public String submitAddNewBook(@Valid @ModelAttribute("NewBook") Book book, BindingResult result, HttpServletRequest request) {

    if(result.hasErrors()) {
        return "addBook";
    }

 

 

addBook.jsp

<div class="form-group row">
    <label class="col-sm-2 control-label">
    <!--    <label class="col-sm-2 control-label">재고수</label>    -->  
        <spring:message code="addBook.form.unitsInStock.label" />
    </label>
    <div class="col-sm-3">
        <form:input path="unitsInStock" class="form-control" />
    </div>
    <div class="col-sm-6">
        <form:errors path="unitsInStock" cssClass="text-danger" />
    </div>
</div>

 

 

 

 

 

 

 

 

 

13.4.5 Validator 인터페이스와 JSR-380 을 연동해서 유효성 검사하기

 

 

BookValidator.java

package com.springmvc.validator;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

import org.springframework.beans.factory.annotation.Autowired;

import com.springmvc.domain.Book;
import com.springmvc.exception.BookIdException;
import com.springmvc.service.BookService;

public class BookIdValidator implements ConstraintValidator<BookId, String>{
	
	@Autowired
	private BookService bookService;
	
	public void initialize(BookId constraintAnnotatiion) {
		
	}
	
	public boolean isValid(String value, ConstraintValidatorContext context) {
		Book book;
		
		try {
			book = bookService.getBookById(value);
		}
		catch(BookIdException e) {
			return true;
		}
		if(book != null) {
			return false;
		}
		return true;
	}
}

 

 

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 org.springframework.web.multipart.MultipartFile;

import com.springmvc.validator.BookId;

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();
	}

 

 

 

BookId.java

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

package com.springmvc.controller;

....

@Controller
@RequestMapping("/books")
public class BookController {
	@Autowired
	private BookService bookService;
	
	@Autowired
	private BookValidator bookValidator;
	//13.4.5 위의 unitsInStockValidator을 BookValidator 클래스로 수정
	
    ...
	
	@InitBinder
	public void initBinder(WebDataBinder binder) {
		binder.setValidator(bookValidator);  //생성한 unitsInStockValidator 설정되어 있던걸 bookValidator로 수정
		binder.setAllowedFields("bookId","name","unitPrice","author","description","publisher","category","unitsInStock","totlaPages","releaseDate","condition","bookImage");
	}

 

@PostMapping("/add")
public String submitAddNewBook(@Valid @ModelAttribute("NewBook") Book book, BindingResult result, HttpServletRequest request) {

    if(result.hasErrors()) {
        return "addBook";
    }

    MultipartFile bookImage = book.getBookImage();

 

 

 

 

servlet-context.xml

<annotation-driven enable-matrix-variables="true" validator="validator"/>

 

.....	
	<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>

 

Pattern =\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 =\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 =\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uAC00\uACA9\uC785\uB2C8\uB2E4(0\uC774\uC0C1\uC758 \uC218\uB97C \uC785\uB825\uD558\uC138\uC694)
Digits =\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 = \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uAC00\uACA9\uC785\uB2C8\uB2E4(\uAC00\uACA9\uC744 \uC785\uB825\uD558\uC138\uC694).
BookId.NewBook.bookId = \uB3C4\uC11CID\uAC00 \uC774\uBBF8 \uC874\uC7AC\uD569\uB2C8\uB2E4.
UnitsInStockValidator.message = \uAC00\uACA9\uC774 10000\uC6D0 \uC774\uC0C1\uC778 \uACBD\uC6B0\uC5D0\uB294 99\uAC1C \uC774\uC0C1\uC744 \uB4F1\uB85D\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.
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 5\uC790\uAE4C\uC9C0 \uC785\uB825\uD574\uC8FC\uC138\uC694).

 

 

 

 

 

 

 

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday