SPRING/Spring MVC

[SpringMVC] 검증(Validation)(2) - BindingResult ( FieldError )

IT록흐 2023. 8. 28. 17:14
반응형
 

[SpringMVC] 요청데이터 검증(Validation)하기(1)

클라이언트로부터 요청(Request)이 들어오면, 요청 데이터는 가장 먼저 Controller에 도착한다. 그러므로 Controller에서 적합한 데이터인지 검사해야 한다. 적합하지 않은 데이터가 서버 내부로직으로

lordofkangs.tistory.com

 

 

지난 포스팅에서 HashMap으로 요청데이터 검증을 구현해보았다. 이번 포스팅에서는 Spring에서 제공하는 BindingResult 객체를 이용하여 요청데이터 검증을 구현해 보겠다. 

 

BindingResult 객체는 오류 메시지를 담는 그릇이라고 보면 된다. HashMap 자료구조에 오류 메시지를 담으면 Controller 외부에서 발생하는 오류를 핸들링하지 못한다. BindingResult는 Spring이 제공하는 객체로 Controller 외부에서 발생하는 오류 메시지도 담을 수 있다. 

 

 

BindingResult

 

컨트롤러 예시

@Slf4j
@Controller
public class ValidationItemController {

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

 

 

요청이 들어와 Controller메소드가 호출되면 @ModelAttribute로 선언된 객체에 요청 데이터가 담긴다. 그리고 바로 옆에 BindingResult 파라미터를 위치시킨다. 이런 구도는 Item으로 들어오는 데이터의 오류 메시지를 bindingResult에 담겠다는 의도를 Spring에게 전달하는 것이다.  

 

 

가격은 int형이다. 만약 가격란에 aaa 문자열을 넣고 전달하면 어떻게 될까?

 

    @PostMapping("/add") 
    public String addItem(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
    	// 검증 실패 로직
	// BindingResult에 오류 메시지가 담겨 있는지 확인하기
        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}";
    }

 

@ModelAttribute로 선언된 Item 객체에 요청 데이터를 넣는 일은 Spring이 담당한다. 그러므로 그 과정에서 발생한 오류 메시지도 Spring이 BindingReusult에 담아야 한다. 그럼 우리는 위 코드처럼, BindingResult에 오류메시지가 담겨져 있는지 로그를 찍어서 확인해보자. 

 

 

화면

 

콘솔

 

 

화면에 오류페이지로 전환되지 않았다.

 

로그를 보면 BindingResult에 오류 메시지가 담겨져 있음을 확인할 수 있다. 단순한 입력 실수에 Exception 처리를 하여 오류페이지를 띄우는 것이 아니라,  오류 내용을 담은 뷰를 만들어 클라이언트에게 전달하는 것이다. 이렇듯,  Spring은 Controller 외부에서 발생한 오류를 BindingResult에 담고 Controller 로직을 실행시킨다. 

 

그럼 이제,

바인딩 과정에서 발생하는 오류는 Spring이 담았으니 유효성 검사에서 발생하는 오류를 담아보자.

 

 

FieldError, ObjectError

 

BindingResult는 오류 메시지를 담는 그릇이다. Controller가 데이터를 검증해서 오류를 발견하면, 그 내용을 BindingResult에 담아야 한다.

 

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

        // 1. 상품명이 공백인 경우
        if(!StringUtils.hasText(item.getItemName())){
            bindingResult.addError(new FieldError("item","itemName","상품이름은 필수 입니다."));
        }

        // 2. 가격이 범위 밖인 경우
        if(item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000){
            bindingResult.addError(new FieldError("item","price","가격은 1,000 ~ 1,000,000 까지 허용합니다. "));
        }

        // 3. 수량이 범위 밖인 경우
        if(item.getQuantity() == null || item.getQuantity() >= 9999){
            bindingResult.addError(new FieldError("item","quantity","수량은 최대 9,999까지 혀용합니다. "));
        }

        // 4. 두 개 항목 이상의 복합 룰 검증
        if(item.getPrice() != null && item.getQuantity() != null){
            int resultPrice = item.getPrice()*item.getQuantity();
            if(resultPrice < 10000){
                bindingResult.addError(new ObjectError("item","가격 * 수량의 합은 10,000원 이상이어야 합니다. 현재값 = " + resultPrice));
            }
        }
        
        // 중략 ...


    }

 

 

위 코드를 보면 4가지 검증 로직이 있다.

 

오류가 발생하면 1번, 2번, 3번은 오류내용을 FieldError 객체에 담고 4번은 ObjectError 객체에 담는다. BindingResult 객체는 @ModelAttribute로 선언된 객체와 바인딩되어 있다. 그러므로 하나의 필드를 타켓팅하여 오류 메시지를 전달할 수 있는데, 이때 FieldError 객체를 사용한다. FieldError 생성자 파라미터로 바인딩된 객체 이름, 필드이름, 오류메시지를 넘기면 된다.

 

ObjectError는 바인딩된 객체 전체에 해당하는 오류메시지를 담을 때 사용한다. 필드 하나의 검증이 아닌 필드 두 개 이상이 복합되어 있는 검증에서 오류가 발생한 경우, 전역 오류 메시지로 분류된다. 이는 특정 필드가 아닌 바인딩된 객체 전체에 관한 오류 메시지로 ObjectError 객체에 담긴다. ObjectError 생성자 파라미터로 바인딩된 객체 이름 오류메시지를 전달하면 된다. 

 

위에서 살펴본 FieldlError와 ObjectError는 몇 가지 문제점이 있다.

 

가장 대표적인 문제는 오류메시지가 정적으로 바인딩 된다는 점이다. 정적으로 바인됭 되기에 지정된 메시지 외에는 다른 메시지로 변환하기가 쉽지 않다. 또 다른 문제는 화면에서 데이터 유지가 되지 않는다는 점이다.

 

 

가격란에 3을 입력했지만 오류가 발생하며 3이 사라져버렸다. @ModelAttirbute는 Item 객체에 담긴 데이터를 자동으로 Model에 저장하기에 화면이 새로 그려져도 데이터가 유지되어야 한다. 그런데 3이 사라졌다. 오류가 발생한 필드는 BindingResult에 담긴 오류메시지로 대체 되면서 기존 데이터도 사라지는 것이다. 

 

이런 문제를 해결하기 위해,

FieldError와 ObjectError는 또 하나의 생성자를 더 제공하고 있다.

 

 

보다시피 또 다른 FieldlError와 ObjectError 생성자는 굉장히 길다랗다.  

 

생성자 파라미터를 하나씩 살펴보자. 

 

objectName - 바인딩된 객체 이름

field - 바인딩 된 객체의 필드 이름

rejectedValue - 에러를 일으킨 데이터 ( 기존의 데이터 유지 가능 )

bindingFailure - 바인딩 과정에서 일어난 에러이면 true, 유효성 검사에서 일어난 에러이면 false 

codes - String 배열 형태로 오류메시지 저장 ( 다수의 오류 메시지 저장 가능 )

arguments - 오류 메시지에 전달할 파라미터 정보

defaultMessage - 디폴트로 출력할 오류 메시지 

 

그럼 이를 코드로 확인해보자.

 

application.properties 

spring.messages.basename=errors

 

application.properties에 위 설정을 추가한다.

 

메시지는 어느 곳에서나 반복적으로 사용된다. 그러므로 메시지는 파일로 만들어 관리하는 것이 편리하다. 스프링부트가 메시지 파일을 인식하려면 spring.messages.basename에 파일명을 설정해야 한다.

 

errors.properties

required.item.itemName=상품 이름은 필수입니다. 
required.default.message=필수 정보를 입력해주세요.
range.item.price=가격은 {0} ~ {1} 까지 허용합니다. 
max.item.quantity=수량은 최대 {0} 까지 허용합니다.

 

errors.properties에 에러메시지를 추가 해보았다.

 

{0}, {1}은 arugement로 파라미터로 넘어온 데이터가 입력되는 자리이다. 이처럼 동적인 메시지 생성도 가능하다. 

 

Controller

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

	// 중략...

    if(!StringUtils.hasText(item.getItemName())){
        bindingResult.addError(new FieldError("item","itemName",item.getItemName(),false,new String[]{"required.item.itemName","required.default.message"},null,null));
    }

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

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

    // 복합 룰 검증
    if(item.getPrice() != null && item.getQuantity() != null){
        int resultPrice = item.getPrice()*item.getQuantity();
        if(resultPrice < 10000){
            bindingResult.addError(new ObjectError("item",new String[]{"totalPriceMin"},new Object[]{10000,resultPrice},null));
        }
    }
    
    // 중략 ...
        
}

 

BindingResult는 오류메시지를 담는 그릇이다. 오류메시지는 FieldError, ObjectError에 저장된다. 

 

코드 하나만 살펴보자.

 

new FieldError("item","price",item.getPrice(),false,new String[]{"range.item.price"},new Object[]{1000,1000000},null)

 

FieldError의 내용은 다음과 같다.

 

item 객체의 price 필드에서 오류가 발생했다.

오류 데이터는 item.getPrice()이다.

바인딩 에러가 아닌 유효성 검사 과정에서 발생한 에러이다.

오류 메시지는 range.item.price이다.

메시지로 넘어갈 파라미터 데이터는 1000, 1000000이다.

디폴트 메시지는 없다. 

 

 

 

 

오류 메시지가 정상적으로 출력됨을 확인할 수 있다. 

 

오류 메시지가 동적으로 생성되기에 복잡하게 코드를 수정할 필요없이 error.properties만 수정하면 된다. 원하는 메시지가 없어도 두번째, 세번째로 저장된 메시지를 출력하면 되고 없으면 디폴트 메시지를 출력하면 된다.  또한 가격란을 보면, 에러가 발생했음에도 데이터가 유지되고 있다.

 

 

정리하면,

 

Request로 들어온 요청데이터가 객체에 바인딩되는 과정에서 오류가 발생하면 로직은 더이상 진행되지 못하고 Exception이 발생한다. 단순히 입력을 잘못한 것 뿐인데, 오류페이지가 떠버리는 상황이 발생하는 것이다. 그래서 Spring은 Exception이 발생하지 않도록 한 가지 객체를 제공하는데, 그것이 BindingResult이다. 

 

Binding 과정에서 에러가 발생하면, Spring은 오류내용을 BindingResult에 담고 Controller 로직을 진행시킨다. Controller에서는 유효성 검사를 진행하고 오류가 발생하면 BindingResult에 담는다. Controller 로직이 종료되면 Model에 담긴 데이터가 View 템플릿으로 이동하여 화면에 그려진다. Spring은 BindingResult에 담긴 데이터도 Model에 담아 같이 전송한다. 이로써 뷰템플릿은 오류내용을 화면에 출력할 수 있게 된다. BindingResult는 오류를 담는 그릇이고 FieldError, ObjectError는 오류내용을 담고 있다. FieldError, ObjectError의 생성자에 따라 구체적인 오류 메시지 설정이 가능해진다. 

 

이처럼 Request에서 들어온 요청데이터를 검증하는 과정에서 Exception이 발생하지 않고,  오류내용을 원만하게 클라이언트에게 전달할 수 있도록 하는 객체가 BindingResult이다. 그러나 위 코드에서 보면 알 수 있듯이, FieldError, ObjectError를 직접 사용하면 코드가 복잡해지고 길어진다. 이를 단순화 할 수 있도록 BindingResult는 메소드를 제공하고 있다. 이는 다음 포스팅에서 다루어 보겠다.

 

 


 

참고자료

 

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

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

www.inflearn.com

 

 

 

 

 

 

 

 

 

 

 

반응형