RE-Heat 개발자 일지

스프링 MVC 2편 - [5] 검증2- Bean Validation(상편) 본문

백엔드/스프링

스프링 MVC 2편 - [5] 검증2- Bean Validation(상편)

RE-Heat 2023. 7. 19. 08:18

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2/dashboard

 

스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - 인프런 | 강의

웹 애플리케이션 개발에 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. MVC 2편에서는 MVC 1편의 핵심 원리와 구조 위에 실무 웹 개발에 필요한 모든 활용 기술들을 학습할 수 있

www.inflearn.com

인프런 김영한님의 스프링 MVC 2편 강의를 토대로 정리한 내용입니다.

 

 

[1] Bean Validation - 소개

이전까지 검증기능을 작성하는 건 너무 길고 번거로웠다. 그래서 스프링에선 JAVA 코드 대신 애노테이션만으로 쉽게 검증할 수 있도록 돕는 API를 제공한다. 

 

■ Bean Validation이란?

Bean Validation 2.0이라는 기술 표준으로, 검증 애노테이션과 여러 인터페이스의 모음이다. 

 


[2] Bean Validation - 시작

■ 의존 관계 추가

 

추가되는 라이브러리

① jakarta.validation-api : Bean Validation 인터페이스

② hibernate-validator : 구현체

 

Item

@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까지만 허용한다.

 

@NotNull은 javax.validation.constrains.NotNull

@Range는 org.hibernate.validator.constraints.Range로 

java.validation 은 표준 인터페이스인 반면 org.hibernate.validator는 하이버네이트 validator 구현체를 사용할 때만 제공되는 기능이다. 하지만 실무에선 대부분 하이버네이트 validator를 사용하므로 자유롭게 써도 된다.

 

BeanValidationTest

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

  1] 검증 대상(item)을 직접 검증기에 넣고 그 결과를 받는다.

  2] Set에는 ConstraintViolation이라는 검증 오류가 담기므로 결과가 비어있으면 검증 오류는 없는 것이다.

=> 스프링과 통합되지 않을 땐 검증기를 위 코드처럼 생성한다. 스프링과 통합하면 이 코드를 직접 작성하진 않는다

 

실행결과

검증오류 발생한 객체, 필드, 메시지 정보 등 다양한 정보를 확인할 수 있다.

 

참고]

@NotBlank(message = "공백 X") 이런 식으로 원하는 오류 메시지를 직접 설정할 수 있다.

 


[3] Bean Validation - 스프링 적용

 

ValidationItemControllerV3

@PostMapping("/add")
public String addItem(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
    //특정 필드가 아닌 복합 룰 검증
    if (item.getPrice() != null && item.getQuantity() != null) {
        int resultPrice = item.getPrice() * item.getQuantity();
        if (resultPrice < 10000) {
            bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
        }
    }

    //검증에 실패하면 다시 입력 폼으로
    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);

@Validated를 넣어주는 방식으로 Bean Validator를 사용한다.

 

Item & 실행화면

 

 

■ 스프링 MVC는 어떻게 Bean Validator를 사용할까?

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

 

■ 스프링 부트는 자동으로 글로벌 Validator로 등록한다.

LocalValidatorFactoryBean을 글로벌 Validator로 등록한다. 이 Validator는 @NotNull 같은 애노테이션을 보고 검증을 수행한다. 이런 식으로 글로벌 Validator가 적용돼 있기 때문에, @Validated @valid만 적용하면 된다.

검증 오류가 발생하면 FileError, ObjectError를 생성해서 BindingResult에 담아준다.

 

주의] 글로벌 Validator를 직접 등록하면 스프링 부트는 Bean Validator를 글로벌 Validator로 등록하지 않는다.

 

■ 검증순서

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

   1] 성공하면 다음으로

   2] 실패하면 typeMismatch로 FieldError 추가

② Validator 적용

 

바인딩에 성공한 필드만 Bean Validation 적용. 타입 변환에 실패해서 바인딩에 실패한 필드는 애초에 BeanValidation 적용이 의미가 없으므로 typeMismatch로 간다.

 

ex) 

1] itmeName에 문자 'A' 입력 → 타입 변환 성공 → itemName 필드에 BeanValidation 적용

2] Price에 문자 'A' 입력 → 'A' 숫자 타입 변환 시도 실패 → typeMismatch Field Error 추가 → price 필드는 BeanValidation 적용 X

 

 

[4] Bean Validation - 에러 코드

Bean Validation이 기본으로 제공하는 오류 메시지를 좀 더 자세하게 변경하는 방법은?

 

Bean Validation도 MessageCodesResolver를 통해 다양한 메시지 코드가 순서대로 생성된다.

@NotBlank

1. code + "." + object name + "." + field => NotBlank.item.itemName

2. code + "." + field => NotBlank.itemName

3. code + "." + field type => NotBlank.java.lang.String

4. code => NotBlank

 

스프링 MVC 2편 검증 1 - Validation의 DefaultMessageCodesResolver 기본 메시지 생성 규칙과 같다.

그러므로

errors.properties에 자세한 메시지를 입력하면 BeanValidation의 오류 메시지도 바꿀 수 있다.

 

실행화면 & errors.properties

단계를 나눠 itemName에 더 자세한 내용을 적어뒀다.

 

■ BeanValidation 메시지 찾는 순서

① 생성된 메시지 코드 순서대로 messageSource에서 메시지 찾기

② 애노테이션의 message 속성 이용

    @NotBlank(message = "공백! {0}")

③ 라이브러리가 제공하는 기본 값 사용

 


[5] Bean Validation - 오브젝트 오류

필드가 아닌 오브젝트 오류는 어떻게 처리할 수 있을까?

 

방법 1] @ScriptAssert() 사용

실행화면 & Item

간편해 보이지만, 제약이 많고 복잡하다. 또 실무에선 검증 기능이 해당 객체의 범위를 넘어설 때도 꽤 있는데, 그럴 때 대응이 어렵다는 단점이 있다.

 

방법 2] 오브젝트 오류 관련 부분만 직접 자바 코드로 작성하기

//@PostMapping("/add")
public String addItem(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
    //특정 필드가 아닌 복합 룰 검증
    if (item.getPrice() != null && item.getQuantity() != null) {
        int resultPrice = item.getPrice() * item.getQuantity();
        if (resultPrice < 10000) {
            bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
        }
    }

=> Object 관련 전체 예외 코드를 다시 추가해 주는 방식

@ScriptAssert()는 제약이 많으므로 오브젝트에 한해 직접 자바 코드로 작성하는 것을 권장한다.

 


[6] Bean Validation - 수정에 적용

크게 달라지는 부분은 없다. 파라미터에 @Validated를 추가하고 BindingResult를 매개변수에 넣으면 된다.

아울러 editForm.html도 새 style을 적용하고 오류 메시지를 출력할 곳을 추가해 주면 된다. 

 

ValidationItemControllerV3

@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId, @Validated @ModelAttribute Item item, BindingResult bindingResult) {
    //특정 필드가 아닌 복합 룰 검증
    if (item.getPrice() != null && item.getQuantity() != null) {
        int resultPrice = item.getPrice() * item.getQuantity();
        if (resultPrice < 10000) {
            bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
        }
    }

    //검증에 실패하면 다시 입력 폼으로
    if (bindingResult.hasErrors()) {
        log.info("errors={}" + bindingResult);
        return "validation/v3/editForm";
    }

    itemRepository.update(itemId, item);
    return "redirect:/validation/v3/items/{itemId}";
}