Spring Validation 3 검증 로직 분리

강정우·2023년 12월 14일
0

Spring-boot

목록 보기
42/73
post-thumbnail

검증로직 분리(검증기 만들기)

컨트롤러의 검증로직

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

    if (!StringUtils.hasText(item.getItemName())) {
        bindingResult.rejectValue("itemName", "required");
    }
    if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1_000_000) {
        bindingResult.rejectValue("price", "range", new Object[]{1000, 1_000_000}, null);
    }
    if (item.getQuantity() == null || item.getQuantity() >= 999) {
        bindingResult.rejectValue("quantity", "max", new Object[]{9999}, null);
    }
    if (item.getPrice() != null && item.getQuantity() != null) {
        int resultPrice = item.getPrice() * item.getQuantity();
        if (resultPrice < 10_000) {
            bindingResult.reject("totalPriceMin", new Object[]{10_000, resultPrice}, null);
        }
    }

    if (bindingResult.hasErrors()) {
        log.info("errors={}", bindingResult);
        return "validation/v2/addForm";
    }

    Item savedItem = itemRepository.save(item);
    redirectAttributes.addAttribute("itemId", savedItem.getId());
    redirectAttributes.addAttribute("status", true);
    return "redirect:/validation/v2/items/{itemId}";
}
  • 위 코드는 컨트롤러의 1개의 api에 대한 코드를 가져온 것이다. 굉장히 길진 않지만 해당 코드에서 75%를 넘게 검증로직이 자리한 것을 볼 수 있다.
    그리고 모든 form데이터가 넘어오는 post마다 이짓거리를 하면 중복되는 코드도 굉장히 많을 것이다.

  • 그래서 이제 컨트롤러의 역할을 쪼개보자.

spring Validator

  • spring에서 제공하는 Validator를 사용(구현)하여 Item 객체를 검증하는 메서드를 만들어보자.
    우선 Validator 인터페이스를 보면 2가지 메서드를 구현해야하는데
@Component
public class ItemValidator implements Validator {
    @Override
    public boolean supports(Class<?> clazz) {
        return Item.class.isAssignableFrom(clazz);
    }
    
    @Override
    public void validate(Object target, Errors errors) {
        Item item = (Item) target;
        
        if (!StringUtils.hasText(item.getItemName())) {
            errors.rejectValue("itemName", "required");
        }
        if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1_000_000) {
            errors.rejectValue("price", "range", new Object[]{1000, 1_000_000}, null);
        }
        if (item.getQuantity() == null || item.getQuantity() >= 999) {
            errors.rejectValue("quantity", "max", new Object[]{9999}, null);
        }
        if (item.getPrice() != null && item.getQuantity() != null) {
            int resultPrice = item.getPrice() * item.getQuantity();
            if (resultPrice < 10_000) {
                errors.reject("totalPriceMin", new Object[]{10_000, resultPrice}, null);
            }
        }
    }
}
  1. support()
    • isAssignableFrom() 메서드로 jsisInstanceof 와 같은 역할을 한다.
    • 또한 하위 자식 타입까지 포괄할 수 있기 때문에 더 안정적이다.
  1. validate()

    • 실제 validate를 하는 로직이 들어간다.
    • 인터페이스가 넘어오는 값을 당연하게 Object로 해놨기 때문에 원하는 객체로 casting 해야한다.
    • Errors는 바로 bindingResult의 부모 class이다.
  2. `@Component``

    • 컴포넌트를 등록하여 componentScan에 걸리도록 하였고 이를 컨트롤러에서 아래 코드처럼 의존성을 주입해서 갖다가 쓰면 된다.
    ...
    @RequiredArgsConstructor
    public class ValidationItemControllerV2 {
        private final ItemRepository itemRepository;
        private final ItemValidator itemValidator;
        ...
    • 그래서 우선 support()로 지원하는 개체인지 확인하고 validate() 함수에 넣어주면 된다.

@Validate

  • 그런데 사실 우리가 따지고보면 굳이 spring에서 제공하는 validator 인터페이스를 구현하여 사용할 필요는 없다.
    그럼에도 불구하고 사용하는 것은 이걸 사용하면 spring이 또 뭔가 마법을 부려주기 때문이다.
@InitBinder
public void init(WebDataBinder dataBinder) {
 log.info("init binder {}", dataBinder);
 dataBinder.addValidators(itemValidator);
}
  • WebDataBinder 이는 해당 컨트롤러가 호출될 때 마다 자동으로 새로 만들어진다.
    WebDataBinder 는 스프링의 파라미터 바인딩의 역할을 해주고 검증 기능도 내부에 포함한다

  • 그리고 해당 코드를 실행하여 검증기를 항상 생성하여 갖고있다.

@PostMapping("/add")
public String addItemV6(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
    if (bindingResult.hasErrors()) {
        log.info("errors={}", bindingResult);
        return "validation/v2/addForm";
    }

    Item savedItem = itemRepository.save(item);
    redirectAttributes.addAttribute("itemId", savedItem.getId());
    redirectAttributes.addAttribute("status", true);
    return "redirect:/validation/v2/items/{itemId}";
}
  • 그러면 spring이 알아서 자동으로 @Validated 어노테이션을 보고 검증을 자동으로 돌려 해당 결과값을 bindingResult에 넣어둔다.

  • 동작과정
    @Validated는 검증기를 실행하라는 애노테이션이다.
    이 애노테이션이 붙으면 앞서 WebDataBinder에 등록한 검증기를 찾아서 실행한다.
    그런데 여러 검증기를 등록한다면 그 중에 어떤 검증기가 실행되어야 할지 구분이 필요하다.
    바로 이때 supports() 가 사용된다. 여기서는 바로뒤 @ModelAttribute가 Item이니까 "supports(Item.class)"가 호출되고, 결과가 true 이므로 spring validator를 implement한 "ItemValidator" 의 validate() 가 호출된다.

글로벌 설정

  • 만약 해당 검증기를 컨트롤러 전역에 적용하고 싶다면 xxxApplication으로 가서
@SpringBootApplication
public class ItemServiceApplication implements WebMvcConfigurer {

	public static void main(String[] args) {
		SpringApplication.run(ItemServiceApplication.class, args);
	}

	@Override
	public Validator getValidator() {
		return new ItemValidator();
	}
}
  • WebMvcConfigurer를 구현하고 getValidator() 메서드를 오버라이드 해주면 된다.
    그러게 된다면 각 컨트롤러에서 dataBinderaddValidator()를 하여 검증기를 안 넣어줘도 된다.

  • 그런데 사실 글로벌설정을 할 일은 거의 없을 것이다.
    왜냐하면 BeanValidator 때문인데 이는 다음 포스팅에 적도록 하겠다.

참고로 검증시 @Validated, @Valid 둘다 사용가능하다.
javax.validation.@Valid를 사용하려면 build.gradle 의존관계 추가가 필요하다.
implementation 'org.springframework.boot:spring-boot-starter-validation' 요거.
@Validated는 스프링 전용 검증 애노테이션이고,
@Valid는 자바 표준 검증 애노테이션이다.

profile
智(지)! 德(덕)! 體(체)!

0개의 댓글