Validation

 

기존 클라이언트에서 전송한 데이터 오류 발생 시 문제점

 

뷰 템플릿에서 중복으로 처리할 것이 많고 숫자 타입에 문자가 입력되는 등의 타입 오류 시 컨트롤러에 진입하기 전에 예외 발생한다.

타입 오류 발생 시에 이전에 입력한 값을 보여주기 힘들다.

 

 

BindingResult

 

스프링이 제공하는 인터페이스(Error 인터페이스 상속)로 @ModelAttribute에 데이터 바인딩 오류가 발생해도 컨트롤러가 호출된다.

addError() 메서드의 파라미터로 오류 정보를 담은 FiledError(), ObjectError()를 생성한다.

FiledError, ObjectError의 생성자는 errorCode, arguments를 가지는데 이는 오류 메시지를 생성할 때 사용된다.

 

@PostMapping("/add")
public String createItem(@ModelAttribute Item item, BindingResult bindingResult) {
    if (!StringUtils.hasText(item.getItemName())) {
                bindingResult.addError(new FieldError("item", "itemName",
        item.getItemName(), false, new String[]{"required.item.itemName"}, null,
        null));
}

errors.properties 파일을 별도로 생성 (메시지 방식과 동일함)

<!-- errors.properties -->
required.item.itemName=상품명을 입력하세요.

 

rejectValue() 를 사용하면 FieldError를 직접 생성하지 않고 오류를 관리할 수 있다.

내부적으로 MessageCodeResolver가 동작하면서 errorCode를 보고 errors.properties에서 오류 메시지를 찾는데

객체, 필드명, errorCode를 조합해서 구체적인 것에서 덜 구체적인 순으로 조회를 한다.

 

//void rejectValue(@Nullable String field, String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage);
if (!StringUtils.hasText(item.getItemName())) {
	/**
    *	우선 순위
    *	1. required.item.itemName
    *	2. required.itemName
    *	3. required.java.lang.String
    *	4. required
    */
	bindingResult.rejectValue("itemName", "required");
}

 

타임리프

 

타임리프는 BindingResult를 활용해서 편리하게 검증 오류를 표현하는 기능 제공한다.

  • #fields : BindingResult 가 제공하는 검증 오류에 접근할 수 있다. (th:if="${#fields.hasGlobalErrors()}")
  • th:errors : 해당 필드에 오류가 있는 경우에만 태그를 출력한다. (th:if 편의 버전)
  • th:errorclass : th:field 에서 지정한 필드에 오류가 있는 경우에만 class 정보를 추가한다.
  • th:filed : 평상시 model 객체의 값을 사용, 오류 발생 시 FieldError에 보관한 값(rejectedValue)을 사용

 


 

Validator

BindingResult의 rejectValue()로 편해지긴 했지만 컨트롤러에서 작성하는 검증 로직이 매우 복잡하다.

Validator를 사용해서 검증 로직을 별도의 클래스로 분리 가능한데, Validator를 사용해서 컨트롤러에서 검증 로직을 분리시켜도 검증 코드를 하나하나 작성하는 건 변하지 않는다.

Bean Validation을 사용하면 검증 로직을 애노테이션으로 깔끔하게 해결 가능하다.

 

Bean Validation

Bean Validation 2.0(JSR-380) 표준 기술로, 실무에서는 보통 하이버네이트 validator(구현체)를 사용한다.

  • build.gradle 라이브러리 추가
implementation 'org.springframework.boot:spring-boot-starter-validation'

 

'spring-boot-starter-validation' 라이브러리를 추가하면 스프링 부트는 자동으로 Bean Validator를 스프링에 통합해준다.

필드 위에 검증 애노테이션을 입력만 하면 Bean Validation이 오류 발생 시 FeildError를 자동 생성한다.

스프링 부트는 Bean Validation을 글로벌 Validator로 사용하기 때문에 @ModelAttribute 앞에 @Validated만 추가하면 적용이 된다.

 

@Data
public class Item {
    private Long id;
    
    @NotBlank
    private String itemName;
    
    @NotNull
    @Range(min = 1000, max = 1000000)
    private Integer price;
}
@PostMapping("/add")
    public String addItem(@Validated @ModelAttribute Item item, BindingResult
bindingResult) {
	...
}

 

먼저 @ModelAttribute 각각의 필드에 타입 변환이 되면 Bean Validation이 적용되고 타입 변환에 실패할 경우 해당 필드는 typeMismatch(errorCode)로 FiledError가 생성된다. (나머지 필드는 Bean Validation 검증 시작)

 

Bean Validation은 검증 오류 발생 시 애노테이션명을 errorCode로 사용해서 FieldError 생성하기 때문에 errors.properties에 해당 애노테이션명(errorCode)로 오류 메시지를 추가해서 활용 가능하다.

 

NotBlank.item.itemName
NotBlank.itemName
NotBlank.java.lang.String
NotBlank

Range.item.price
Range.price
Range.java.lang.Integer
Range

처음 바인딩 되는 {0}은 필드명, {1}, {2}.. 는 애노테이션마다 다르다. (@Range의 경우 가격 범위 바인딩)

//Bean Validation 추가

NotBlank={0} 공백X
Range={0}, {2} ~ {1} 허용

 

ObjectError의 경우는 bean Validation 적용이 복잡해서 bindingResult.reject() 로 직접 생성해주는 것이 좋다. 

Bean Validation은 각각의 경우에 따라 검증 방식을 나눌 수 있는 groups 기능을 제공하지만 별도의 폼 객체(saveForm, updateForm.. )를 만들어서 사용하는 것이 좋다.

 

 

HttpMessageConverter(@RequestBody)에도 적용이 가능한데 FieldError 객체를 JSON으로 변환해서 전달한다. (필요한 데이터만 뽑아서 별도의 API 스펙으로 객체를 만들어서 반환)

@ModelAttribute는 @Valiated 적용 시 각각의 필드에 세밀하게 적용이 되어 특정 필드에 타입 오류가 발생해도 나머지 필드는 정상 처리가 된다.

반면, HttpMessageConverter전체 객체 단위로 적용이 되기 때문에 하나라도 문제가 생기면 예외가 발생하고 컨트롤러도 호출되지 않는다.

+ Recent posts