BackEnd/Spring MVC

[Spring] 검증2 - Bean Validation - 1

샤아이인 2022. 3. 7.

내가 공부한것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼겸 상세히 기록하고 얕은부분들은 가겹게 포스팅 하겠습니다.

 

1. Bean Validation - 소개

기존에 직접 구현하였던 검증과정은 상당하게 번거롭다. 특정 필드에 대하여 하나 하나 전부 검증과정을 거처야 하니 노가다에 가까웠다.

 

이를 해결하기 위해 등장한것이 Bean Validation 이다. 우선 다음 코드를 살펴보자.

public class Item {
      private Long id;

      @NotBlank
      private String itemName;

      @NotNull
      @Range(min = 1000, max = 1000000)
      private Integer price;
  
      @NotNull
      @Max(9999)
      private Integer quantity;

      //...
}
 

위와 같이 Bean Validation을 잘 활용하면, 애노테이션 하나로 검증 로직을 매우 편리하게 적용할 수 있다.

이런 검증 로직을 모든 프로젝트에 적용할 수 있게 공통화하고, 표준화 한 것이 바로 Bean Validation 이다.

 

Bean Validation은 특정 구현체가 아닌 인터페이스의 모음이다.

JPA가 표준 기술이고 이를 구현하는 많은 구현체들이 있는것과 비슷하다.

Bean Validation 또한 Bean Validation 2.0(JSR-380) 이라는 기술 표준을 말하는것 이다.

 

2. Bean Validation - 시작

Spring이 아닌, 순수하게 Validation 을 직접 테스트 코드를 통해 사용해보자.

 

● Item - Bean Validation 애노테이션 적용

import lombok.Data;
import org.hibernate.validator.constraints.Range;

import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

@Data
public class Item {

    private Long id;

    @NotBlank
    private String itemName;

    @NotNull
    @Range(min = 1000, max = 1000000)
    private Integer price;

    @NotNull
    @Max(9999)
    private Integer quantity;

    public Item() {
    }

    public Item(String itemName, Integer price, Integer quantity) {
        this.itemName = itemName;
        this.price = price;
        this.quantity = quantity;
    }
}
 

● 검증 애노테이션

@NotBlank : 빈값 + 공백만 있는 경우를 허용하지 않는다.

@NotNull : null 을 허용하지 않는다.

@Range(min = 1000, max = 1000000) : 범위 안의 값이어야 한다.

@Max(9999) : 최대 9999까지만 허용한다.

 

테스트 코드는 다음과 같이 작성하였다.

public class BeanValidationTest {

    @Test
    void beanValidation(){
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        Validator validator = factory.getValidator();

        Item item = new Item();
        item.setItemName(" "); // 공백
        item.setPrice(0);
        item.setQuantity(10000);

        Set<ConstraintViolation<Item>> violations = validator.validate(item);
        for (ConstraintViolation<Item> violation : violations) {
            System.out.println("violation = " + violation);
            System.out.println("violation = " + violation.getMessage());
        }
    }
}
 

결과는 다음과 같다.

검증기에 의해서 잘못 입력되있는 값들이 전부 검증되었다.

 

참고로 위에서는 검증기를 직접 생성하여 사용하였다. 다음과 같이 말이다.

ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
 

검증은 검증할 대상을 직접 검증기에 집어 넣어 결과를 받아서 사용한다.

Set<ConstraintViolation<Item>> violations = validator.validate(item);
 

ConstraintViolation 출력 결과를 보면, 검증 오류가 발생한 객체, 필드, 메시지 정보등 다양한 정보를 확인할 수 있다.

 

3. Bean Validation - 스프링 적용

addItem 코드를 살펴보면 다음과 같다.

@PostMapping("/add")
public String addItem(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {

    // 검증에 실패한 경우
    if(bindingResult.hasErrors()){
        log.info("errors= {}", bindingResult);
        return "validation/v3/addForm";
    }

    // 성공 로직
    Item savedItem = itemRepository.save(item);
    redirectAttributes.addAttribute("itemId", savedItem.getId());
    redirectAttributes.addAttribute("status", true);
    return "redirect:/validation/v3/items/{itemId}";
}
 

기존에 사용했던 ItemValidator가 제거되어있다.

하지만 실행해보면 애노테이션 기반의 Bean Validation이 정상 작동하기 때문에 검증절차가 진행됨을 알 수 있다. 다음과 같이 말이다.

● Spring MVC 는 어떻게 Bean Validator를 사용할까?

스프링 부트가 spring-boot-starter-validation 라이브러리를 넣으면 자동으로 Bean Validator를 인지하고 스프링에 통합한다.

 

● 스프링 부트는 자동으로 global Validator로 등록한다.

Spring Boot ValidationAutoConfiguration 클래스를 통해서 LocalValidatorFactoryBean과 MethodValidationPostProcessor를 자동으로 설정합니다.

 

LocalValidatorFactoryBean는 Spring에서 Validator를 사용하기 위해서 필요하고 MethodValidationPostProcessor는 메서드 파라미터 또는 리턴 값을 검증하기 위해서 사용됩니다.

 

또한 LocalValidatorFactoryBean는 글로벌 Validator로 등록됩니다.

이 글로벌 Validator는 @NotNull 같은 애노테이션을 보고 검증을 수행한다.

이렇게 글로벌 Validator가 적용되어 있기 때문에, 검증 대상에 @Valid , @Validated 만 적용하면 검증과정을 거치게 됩니다.

 

또한 AutoConfiguration로 자동 설정되기 때문에 별다른 설정 없이 Spring Boot에서 바로 Validation을 사용할 수 있습니다.

 

● 검증 순서

1. @ModelAttribute 각각의 필드에 타입 변환 시도

-- 1. 성공하면 다음으로

-- 2. 실패하면 typeMismatch 로 FieldError 추가

 

2. Validator 적용

 

● 바인딩에 성공한 필드만 Bean Validation 적용

바인딩에 성공한 필드만 Bean Validation이 적용된다. 이는 지극히 상식적이다.

예를 들어 price필드에 문자인 "qqqq"가 입력됬다고 해보자. 이는 바인딩에 실패하게 된다.

더 나아가 "qqqq"는 애당초 type이 맞기 않기 때문에, "가격은 1000원 이상 입니다" 와 같은 오류메시지를 보여줄 필요가 없다.

(일단 모델 객체에 바인딩 받는 값이 정상으로 들어와야 검증도 의미가 있다.)

 

@ModelAttribute => 각각의 필드 타입 변환시도 => 변환에 성공한 필드만 BeanValidation 적용

 

예)

- itemName 에 문자 "A" 입력 => 타입 변환 성공 => itemName 필드에 BeanValidation 적용

- price 에 문자 "A" 입력 => "A"를 숫자 타입 변환 시도 실패 => typeMismatch FieldError 추가 => price 필드는 BeanValidation 적용 X

 

4. Bean Validation - 에러 코드

Bean Validation이 기본으로 제공하는 오류 메시지를 좀 더 자세히 변경하고 싶으면 어떻게 하면 될까?

 

우선 직접 BindingResult에 등록되는 검증 오류 코드를 살펴보자.

대표사진 삭제

사진 설명을 입력하세요.

code에 보면 에러 코드들이 NotBlank로 전부 시작함을 알 수 있다.

 

이는 도메인을 만들때 itemName에 다음과 같이 애노테이션을 적용했기 때문이다.

@NotBlank
private String itemName;
 

마치 예전에 typeMismatch와 유사하다.

 

NotBlank 라는 오류 코드를 기반으로 MessageCodesResolver 를 통해 다양한 메시지 코드가 순서대로 생성된다.

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

이제 메시지 소스에 직접 원하는 메시지를 등록해 보자. 다음과 같이 추가해 주었다.

 

● errors.properties

#Bean Validation 추가
NotBlank={0} 공백X
Range={0}, {2} ~ {1} 허용
Max={0}, 최대 {1}
 

실행하면 다음과 같이 우리가 지정한 메시지 가 나오는것을 확인할 수 있다.

● BeanValidation 메시지 찾는 순서

1. 생성된 메시지 코드 순서대로 messageSource 에서 메시지를 찾는다.

2. 애노테이션의 message 속성 사용 => @NotBlank(message = "공백! {0}")

3. 라이브러리가 제공하는 기본 값 사용, 공백일 수 없습니다.

댓글