[spring] 스프링이 지원하는 Bean Validator 기능 사용 방법

woply·2022년 2월 11일
0

spring

목록 보기
16/20
post-thumbnail

📖 ✏️

이 글은 스프링을 이용해 애노테이션으로 검증 로직을 구현할 수 있는 Bean Validation 기능을 사용하는 방법을 학습하고, 정리한 포스팅이다.


스프링이 지원하는 Bean Validator 기능 사용 방법

아래 코드는 Bean Validator이 적용된 예제이다. /validation/v3/items/add URL POST 요청을 맵핑하는 addItem()를 보면 @Validated가 적용되어 있다.


1. Bean Validator 라이브러리 적용

Bean Validator를 사용하려면 build.gradle에 라이브러리를 추가해야 한다. 스프링 부트는 implementation 'org.springframework.boot:spring-boot-starter-validation' 라이브러리가 적용되어 있을 때, 자동으로 Bean Validator를 인식하고 스프링에 통합한다.

ValidationItemControllerV3

package hello.itemservice.web.validation;

import hello.itemservice.domain.item.Item;
import hello.itemservice.domain.item.ItemRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import java.util.List;

@Slf4j
@Controller
@RequestMapping("/validation/v3/items")
@RequiredArgsConstructor
public class ValidationItemControllerV3 {

    private final ItemRepository itemRepository;

    @GetMapping
    public String items(Model model) {
        List<Item> items = itemRepository.findAll();
        model.addAttribute("items", items);
        return "validation/v3/items";
    }

    @GetMapping("/{itemId}")
    public String item(@PathVariable long itemId, Model model) {
        Item item = itemRepository.findById(itemId);
        model.addAttribute("item", item);
        return "validation/v3/item";
    }

    @GetMapping("/add")
    public String addForm(Model model) {
        model.addAttribute("item", new Item());
        return "validation/v3/addForm";
    }

    @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}";
    }

    @GetMapping("/{itemId}/edit")
    public String editForm(@PathVariable Long itemId, Model model) {
        Item item = itemRepository.findById(itemId);
        model.addAttribute("item", item);
        return "validation/v3/editForm";
    }

    @PostMapping("/{itemId}/edit")
    public String edit(@PathVariable Long itemId, @ModelAttribute Item item) {
        itemRepository.update(itemId, item);
        return "redirect:/validation/v3/items/{itemId}";
    }
}

Bean Validator 애노테이션이 적용된 Item 클래스

package hello.itemservice.domain.item;

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(message = "공백 불가")
    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;
    }
}

실행해보면 애노테이션 기반의 Bean Validator가 정상적으로 동작하는 것을 확인할 수 있다.

스프링 부트는 LocalValidatorFactoryBean을 글로벌 Validator로 등록한다. 이 Validator가 @NotNull 같은 애노테이션을 보고 검증을 수행한다.

글로벌 Validator가 적용되어 있기 때문에 컨트롤러에는 @Valid 또는 @Validated만 적용하면 된다. 검증 오류가 발생하면 FieldError, ObjectError를 생성해서 BindingResult에 담는다.

참고
아래와 같이 글로벌 Validator를 직접 등록하면 스프링 부트는 Bean Validator를 글로벌 Validator로 등록할 수 없다. 따라서 애노테이션 기반의 Bean Validator가 동작하지 않는다.

글로벌 Validator를 직접 등록하는 방식

@SpringBootApplication
public class ItemServiceApplication implements WebMvcConfigurer {

    // 글로벌 검증기 추가 
    @Override public Validator getValidator() { 
    return new ItemValidator();
    }
    // ... 
} 

검증 기능은 @Validated 또는 @Valid를 모두 사용할 수 있다. @Validated는 스프링 전용 검증 애노테이션이고, @Valid는 자바 표준 검증 애노테이션이다. 검증 기능은 동일하지만 @Validated는 내부에 groups라는 기능을 포함한다.

2. Bean Validator의 동작 원리

검증 순서를 살펴보면, 먼저 @ModelAttribute가 각각의 필드에 타입 변환을 시도한다. 타입 변환에 성공하면 Validator를 적용하고, 실패하면 typeMismatchFieldError를 추가한다.

중요한 점은 바인딩에 성공한 필드만 Bean Validation를 적용한다는 것이다. Bean Validator는 바인딩에 실패한 필드를 적용하지 않는다. 잘 생각해버면 타입 변환에 성공해서 바인딩에 성공한 필드만이 Bean Validation을 적용하는 의미가 있다. 값이 존재하지 않으면 검증의 대상도 부재하기 때문이다.

예)

  • BeanValidation 성공 케이스
    itemName에 문자 "A" 입력 -> 타입 변환 성공 -> itemName필드에 BeanValidation 적용
  • BeanValidation 실패 케이스
    price 에 문자 "A" 입력 -> "A"를 숫자 타입 변환 시도 실패 -> typeMismatch FieldError 추가 -> price 필드 BeanValidation 미적용
profile
7년간 마케터로 일했고, 현재는 헤렌에서 백엔드 개발자로 일하고 있습니다. 고객 가치를 설계하는 개발자를 지향하며, 개발, 독서, 글쓰기를 좋아합니다. 업이 심오한 놀이이길 바라는 덕업일치 주의자입니다.

0개의 댓글