SPRING/Spring MVC

[SpringMVC] 검증(Validation)(4) - BindingResult ( @Validated )

IT록흐 2023. 8. 28. 23:21
반응형

 

 

[SpringMVC] 검증(Validation)(3) - BindingResult ( rejectValue 메소드 )

[SpringMVC] 요청데이터 검증(Validation)하기(2) - BindingResult [SpringMVC] 요청데이터 검증(Validation)하기(1) 클라이언트로부터 요청(Request)이 들어오면, 요청 데이터는 가장 먼저 Controller에 도착한다. 그러

lordofkangs.tistory.com

 

 

지난 포스팅에서 BindingResult의 rejectValue 메소드로 코드를 단순화 해보았다.

 

Controller

@PostMapping("/add") 
public String addItem(@ModelAttribute Item item, BindingResult bindingResult) {
 

	// [ 검증 로직 ]
 
    if(!StringUtils.hasText(item.getItemName())){
        bindingResult.rejectValue("itemName","required");
    }
 
    if(item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000){
        bindingResult.rejectValue("price","range",new Object[]{1000,100000000},null);
    }
 
    if(item.getQuantity() == null || item.getQuantity() >= 9999){
        bindingResult.rejectValue("quantity","max",new Object[]{9999},null);
 
    }
    
    // 복합 룰 검증
    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/v2/addForm";
    }
    
    
    // [ 비즈니스 로직 ]
    
    //중략...
 
 
}

 

상대적으로 코드는 단순화 되었지만 여전히 Controller가 복잡하다. 

 

Controller는 비즈니스 로직을 처리하는 클래스인데 검증 로직도 컨트롤러에 포함되어 있기에 그렇다. 그럼 검증 로직을 분리해보자. 

 

ItemValidator 클래스

public class ItemValidator {

    public void validate(Object target, BindingResult bindingResult) {
        Item item = (Item) target;

        ValidationUtils.rejectIfEmptyOrWhitespace(bindingResult,"itemName","required");

        if(item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000){
            bindingResult.rejectValue("price","range",new Object[]{1000,100000000},null);
        }

        if(item.getQuantity() == null || item.getQuantity() >= 9999){
            bindingResult.rejectValue("quantity","max",new Object[]{9999},null);

        }

        // 틀정 필드가 아닌 복합 룰 검증
        if(item.getPrice() != null && item.getQuantity() != null){
            int resultPrice = item.getPrice()*item.getQuantity();
            if(resultPrice < 10000){
                bindingResult.reject("totalPriceMin", new Object[]{10000,resultPrice},null);
            }
        }

    }
}

 

 

ItemValidator 클래스를 생성하여 Controller에 있던 검증 로직을 분리해보았다. 그럼 Controller는 아래 코드와 같이 간단해진다. 

 

Controller 클래스

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

    //검증 메소드 호출
    itemValidator.validate(item,bindingResult);
    
    // 검증에 실패 시 화면 이동
    if(bindingResult.hasErrors()){
        log.info("errors={}",bindingResult); 
        return "validation/v2/addForm";
    }
    
    // 검증 성공 시 비즈니스 로직
    
    // 중략 ...
   

}

 

Controller가 확실히 간단해졌다. 여기서 어노테이션을 사용하면 더 간단해진다.

 

 

@Validated

 

Controller 클래스

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

    // 검증에 실패하면 다시 입력폼으로 이동
    if(bindingResult.hasErrors()){
        log.info("errors={}",bindingResult);
        return "validation/v2/addForm";
    }

    // 검증 성공 로직
    
    // [ 비즈니스 로직 ]


}

 

@Validated는 Spring 전용 검증 어노테이션이다. @Validated는 Spring에게 검증기 실행을 요구하는 어노테이션이다. 그럼 개발자는 검증기 구현체를 Bean으로 생성하여 등록해야 한다. 

 

 

ItemValidator 클래스

@Component
public class ItemValidator implements Validator {
    @Override
    public boolean supports(Class<?> clazz) {
        // == 보다 isAssignableFrom 사용 ( 자식 클래스도 모두 가능 )
        return Item.class.isAssignableFrom(clazz); //Item을 검증할 수 있는 Validator임을 표시
    }

    @Override
    public void validate(Object target, Errors errors) {
        Item item = (Item) target;

        ValidationUtils.rejectIfEmptyOrWhitespace(errors,"itemName","required");

        if(item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000){
            errors.rejectValue("price","range",new Object[]{1000,100000000},null);
        }

        if(item.getQuantity() == null || item.getQuantity() >= 9999){
            errors.rejectValue("quantity","max",new Object[]{9999},null);

        }

        // 틀정 필드가 아닌 복합 룰 검증
        if(item.getPrice() != null && item.getQuantity() != null){
            int resultPrice = item.getPrice()*item.getQuantity();
            if(resultPrice < 10000){
                errors.reject("totalPriceMin", new Object[]{10000,resultPrice},null);
            }
        }
    }
}

 

Validator 인터페이스의 구현체 ItemValidator 클래스를 생성하고 @Component를 선언하여 Bean으로 등록한다. 그럼 두 가지 메소드를 재정의 해야한다. 

 

1. supports 메소드

 

검증기는 여러가지 검증기 구현체가 존재한다.

 

Spring은 @Validated가 선언된 클래스를 검증하려면 어떤 구현체를 사용해야 하는지 탐색해야 한다. Spring은 각 검증기의 supports 메소드를 호출한다.  supports 메소드는 @Validated가 선언된 클래스를 검증할 수 있는 검증기인가를 확인하는 메소드이다. ItemValidator는 Item 클래스를 검증하는 검증기이므로 true를 반환한다. 반환 시, Item 클래스의 자식클래스도 검증 가능하도록 Item.class.isAssignableFrom(clazz) 형태로 반환한다. '==' 연산자는 Item 클래스만 true를 반환하지만 isAssignableFrom은 자식클래스도 true를 반환한다.

 

2. validate 메소드 

 

supports 메소드에서 true가 나오면, Spring은 해당 구현체의 validate 메소드를 호출한다. validate 메소드에는 개발자가 작성한 유효성 검사용 검증로직이 들어있다. Spring은 유효성 검사로직을 실행하며 Item에 주입된 데이터를 검사한다. 

 

validate 파라미터는 Object target과 Errors errors가 있다. target은 @Validated가 선언된 파라미터에 넘어갈 객체를 의미한다. 객체에는 데이터가 들어있고 유효성 검사를 하면 발생한 오류내용은 errors에 저장된다. Errors 인터페이스의 구현체가 BindingResult이다. 그러므로 errors는 BindingResult 객체라고 생각하면 된다. 

 

이와 같이, 검증 메소드가 모두 실행되면, 이제 Controller 로직이 실행된다. 

 

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

    // 검증에 실패하면 다시 입력폼으로 이동
    if(bindingResult.hasErrors()){
        log.info("errors={}",bindingResult);
        return "validation/v2/addForm";
    }

    // 검증 성공 
    
    // [ 비지니스 로직 ]

}

 

BindingResult 객체에 오류 내용이 담겨 있으면 검증이 실패했다는 의미이다. 검증이 실패하면 실패화면의 경로를 return한다. 검증과정에서 오류가 없으면 비즈니스 로직을 실행한다. 

 

 

정리하면,

 

BindingResult의 rejectValue 메소드를 사용하여 코드를 상대적으로 단순화하였다. 그러나 Controller에 검증로직과 비즈니스 로직이 섞이면서 코드가 복잡해졌다. 이를 위해, Validator 검증기의 구현체를 생성하여 검증로직을 분리하였다. Controller 파라미터의 Item 객체를 검증하려면 Spring에게 Item을 검증할거라는 표시를 남겨야 한다. 이때 사용하는 어노테이션이 @Validated이다. @Validate가 선언된 파라미터에 주입된 객체는 Spring이 자동으로 검증기를 돌려 검증한다. 검증기 구현체가 다양히 있으므로 각 검증기의 supports 메소드를 호출하여 검증기를 탐색하고, 탐색된 검증기의 validate 메소드를 호출하여 검증로직을 실행한다. 검증 과정에서 오류가 발생하면 BindingResult에 담긴다. 검증이 완료되고 Controller 로직이 실행되면, BindingResult에 오류내용이 담겨있는지 여부에 따라 로직을 분기하여 실행하면 된다. 

 

@Validated를 사용하고 검증 로직을 분리하면서 Controller가 정말 단순해졌다. 그러나 개발자가 Validator 구현체를 직접 작성해야 한다는 불편이 있다. 개발자가 Validator를 직접 구현하지 않고도 유효성 검사를 진행할 수 있는 기능을 Spring은 제공하는데, 그것이 Bean Validation이다. 이는 다음 포스팅에서 다루어 보겠다. 

 

 


 

참고자료

 

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

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

www.inflearn.com

 

반응형