검증 V2

V1

  • 스프링의 검증은 데이터 바인딩 과정과 밀접하게 연관되어 있다. 데이터 바인딩은 요청 데이터를 객체로 변환화는 과정인데 이 과정에서 데이터를 검증함으로써 애플리케이션의 안정성과 데이터 무결성을 보장하게 된다.

  • 스프링에서는 크게 두 가지로 구분해서 검증이 이루어진다.

  1. 스프링은 데이터 바인딩 시 검증 로직을 자동으로 실행하도록 설계되었으며 BindingResult를 통해 오류 정보 및 검증 결과를 저장하고 관리한다.

    • DataBinderError 발생 → BindingResultError 추가

  2. 컨트롤러에서 사용자가 직접 BindingResult를 통해 오류 데이터를 추가하고 검증을 진행할 수 있다.

    • ControllerBindingResult → 오류 검증 → Error 추가

컨트롤러

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

    //검증 로직
    //필드 오류
    if (!StringUtils.hasText(item.getItemName())) {
        bindingResult.addError(new FieldError("item", "itemName", "상품 이름은 필수 입니다."));
    }
    if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1_000_000) {
        bindingResult.addError(new FieldError("item", "price", "가격은 1,000 ~ 1,000,000 까지 허용합니다."));
    }
    if (item.getQuantity() == null || item.getQuantity() >= 9999) {
        bindingResult.addError(new FieldError("item", "quantity", "수량은 최대 9,999 까지 허용합니다."));
    }

    //특정 필드가 아닌 복합 룰 검증
    //객체 오류
    if (item.getPrice() != null && item.getQuantity() != null) {
        int resultPrice = item.getPrice() * item.getQuantity();
        if (resultPrice < 10000) {
            bindingResult.addError(new ObjectError("item", "가격 * 수량의 합은 10,000원 이상이어야 합니다. 현재 값 = " + resultPrice));
        }
    }

    //검증에 실패하면 다시 입력 폼으로
    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}";
}
  • BindingResult bindingResult의 위치가 중요하다. @ModelAttribute Item item 다음에 와야 한다.

  • model.addAttribute()를 해주지 않아도 뷰에 넘어간다.

  • FieldError(objectName, field, defaultMessage), ObjectError(objectName, defaultMessage)

    • objectName@ModelAttribute의 이름이다. ("item")

  • addError(ObjectError error) API는 필수 값 누락, 길이 제한 등 어떤 조건이 맞지 않을 경우 오류를 추가할 수 있는 API로써 인자 값으로 ObjectErrorFieldError 객체를 받을 수 있다.

  • 스프링은 바인딩 오류 시 내부적으로 BindingResultaddError(ObjectError error) API를 사용해서 오류 정보를 저장하고 있으며 이때 FieldError 객체를 생성해서 전달한다.

  • 이렇게 추가된 FieldError, ObjectErrorBindingResulterrors 속성에 저장된다.

객체 오류와 필드 오류

  • 스프링은 오류를 추가할 때 객체 오류(또는 글로벌 오류)와 필드 오류로 구분하도록 API를 설계했다.

  • 객체 오류는 말 그대로 객체 수준에서 오류를 표현한다는 의미이고, 필드 오류는 객체보다 좀 더 구체적인 필드 수준에서 오류를 표현한다는 의미이다.

  • 오류는 사용자 또는 클라이언트에게 이해하기 쉬운 문장으로 설명해야 하며 상황에 맞게 구체적인 오류와 종합적인 오류를 잘 조합해서 표현해야 한다.

img_4.png

HTML

타임리프는 스프링의 BindingResult를 활용해서 편리하게 검증 오류를 표현할 수 있는 기능을 제공한다.

  • #fields

    • BindingResult가 제공하는 검증 오류에 접근할 수 있다.

  • th:errors

    • 해당 필드에 오류가 있는 경우 태그를 출력한다. (th:if의 편의 버전)

  • th:errorclass

    • th:field에서 지정한 필드에 오류가 있으면 class 정보를 추가한다.


V2

  • BindingResult는 스프링이 제공하는 검증 오류를 보관하는 객체다. @ModelAttribute에 데이터 바인딩 시 오류가 발생해도 컨트롤러는 호출되기 때문에 화이트라벨 에러 화면을 보여주지 않는다.

  • BindingResultErrors가 있는데 둘 다 인터페이스이고 BindingResultErrors를 상속받고 있다.

  • Errors 인터페이스는 단순한 오류 저장과 조회 기능을 제공하고 BindingResult는 메시지 코드 해석과 세부적인 오류 관리 기능 등 추가적인 기능들을 제공하며, 컨트롤러에서 데이터 바인딩과 검증을 동시에 처리해야 하는 상황에서 주로 사용된다.

img.png

컨트롤러

FieldError는 두 가지 생성자가 있다.

img_1.png
img_2.png

파라미터 목록

  • objectName : 오류가 발생한 객체 이름

  • field : 오류가 발생한 필드 이름

  • rejectedValue : 사용자가 입력한 값 (거절된 값)

  • bindingFailure : 타입 오류 같은 바인딩 실패인지, 검증 실패인지 구분 값

  • codes : 메시지 코드 (MessageSource에서 사용)

  • arguments : 메시지에서 사용하는 인자 (MessageSource에서 사용)

  • defaultMessage : 기본 오류 메시지

FieldErrorrejectedValue에 오류 발생 시 사용자가 입력한 값을 저장하는데 th:field=*{...}에서 정상 상황이면 모델 객체의 값을 사용하고 오류가 발생하면 FieldError에 보관한 값을 사용해서 값을 출력해준다.

스프링의 BindingResult는 세 가지 기본 전략을 가진다.

  1. 스프링은 데이터 바인딩 시 발생하는 모든 오류 정보를 자동으로 BindingResulterrors 속성에 저장한다.

  2. 사용자가 BindingResult의 오류 정보를 활용하기 위해서는 컨트롤러 메서드 매개변수로 지정해야 한다.

    • @ModelAttribute 객체 바로 뒤에 위치해야 하며, 매개변수로 지정하게 되면 객체 바인딩 오류가 나더라도 컨트롤러는 정상적으로 실행된다.

    • 만약 바인딩 오류가 발생했고, BindingResult를 메서드에 선언하지 않으면 스프링은 MethodArgumentNotValidException 예외를 발생시키고 컨트롤러는 실행되지 않는다.

    • 가장 기본이 되는 바인딩 오류는 타입 불일치 오류이다. 즉 요청 데이터와 필드 타입이 서로 맞지 않아 바인딩이 실패하는데 이때 내부적으로 TypeMismatchException 예외가 발생한다.

  3. BindingResult 인터페이스의 API를 사용해서 추가적인 검증을 진행하거나 검증 결과를 클라이언트에게 전달할 수 있다.

img_3.png

V3

  • DataBinder의 바인딩시 발생한 오류나 BindingResult의 유효성 검증 오류가 발생했을 때 MessageSource를 사용해서 오류 메시지를 사용자에게 제공할 수 있다.

  • 이 방식은 유효성 검증에 필요한 오류 메시지를 외부 파일에서 검색 및 관리할 수 있다. 즉 오류 메시지를 MessageSource에게 위임하는 것이다.

application.properties

errors.properties

컨트롤러


V4

  • BindingResult가 제공하는 rejectValue()reject()를 사용하여 더욱 단순화할 수 있다.

컨트롤러

img_5.png
img_6.png
  • reject() - 객체 오류, rejectValue() : 필드 오류

  • field : 오류 필드 이름

  • errorCode : 오류 코드

  • errorArgs : 메시지에 사용될 인자 목록

  • defaultMessgae : 기본 오류 메시지

BindingResult@ModelAttribute바로 뒤에 오기 때문에 어떤 객체를 대상으로 검증하는지 target을 이미 알고 있다. 그래서 target에 대한 정보는 없어도 된다.

rejectValue()를 사용하고 부터는 errorCode를 range만으로 해결됐다. 비밀은 MessageCodesResolver에 있다.

MessageCodesResolver

  • MessageCodesResolver는 검증 오류 발생 시 오류 메시지의 메시지 코드를 생성하는 인터페이스이다.

  • 유효성 검증 시 필드 오류 또는 글로벌 오류가 발생하면 이 오류들을 MessageSource와 연동하여 해당 오류 메시지를 찾기 위한 메시지 코드 목록을 생성한다.

  • 오류 코드를 만들 때는 객체명과 필드명까지 생각해서 자세하게 만들 수도 있고 범용적으로 쓸 수 있게 단순하게 만들 수도 있다.

  • reject() 또는 rejectValue() API가 실행되면 내부적으로 MessageCodesResolver가 오류 코드를 생성하고 그 오류 코드를 MessageSource가 참조해서 오류 메시지를 검색한다.

img_7.png

스프링은 기본 구현체인 DefaultMessageCodesResolver를 제공하며 메시지 생성 규칙이 있다.

객체 오류 (예: 오류 코드: required, object name: item)

  1. code + . + object name -> required.item

  2. code → required

필드 오류 (예: 오류 코드: typeMissMatch, object name: "user", field: "age", field type: int)

  1. code + . + object name + field → "typeMissMatch.user.age"

  2. code + . + field → "typeMissMatch.age"

  3. code + . + field type → "typeMissMatch.int"

  4. code → "typeMissMatch

동작 방식

  • rejectValue(), reject()는 내부에서 MessageCodesResolver를 사용하는데 여기에서 메시지 코드들을 생성한다.

  • FieldError, ObjectError의 생성자는 오류 코드를 하나가 아니라 여러 오류 코드를 가질 수 있고 MessageCodesResolver를 통해서 생성된 순서대로 오류 코드를 보관한다.

타임리프 화면이 렌더링 될 때 th:errors가 실행되는데 이 때 오류가 있다면 생성된 오류 메시지 코드를 순서대로 돌아가면서 메시지를 찾는다. 없으면 기본 오류 메시지를 적용한다.

errors.properties 추가

Level이 낮을수록 덜 구체적인 것이고 높을수록 구체적인 것이다. 메시지에 1번이 없으면 2번을 찾고, 2번이 없으면 3번을 찾는다. 크게 중요하지 않은 오류 메시지는 재활용할 수 있다.

DefaultBindingErrorProcessor

  • 검증 오류 코드는 개발자가 직접 설정한 오류 코드(rejectValue()를 직접 호출)스프링이 직접 검증 오류에 추가한 경우로 나뉜다.

  • 만약 price 필드에 문자를 입력하면 스프링은 타입 오류가 발생하는데 DefaultBindingErrorProcessor 클래스에 의해 MessageCodesResolver를 통하면서 typeMissMatch라는 오류 코드로 4가지 메시지 코드가 입력된다.

errors.properties 추가

위 설정이 없다면 스프링이 직접 만든 사용자 친화적이지 못한 기본 생성된 메시지가 나온다. 설정을 하면 지정한 메시지가 그대로 출력된다.

MessageSourceResolvable

img_8.png

  • MessageSource가 메시지를 찾을 때 오류 코드를 제공하는 인터페이스로, 순차적으로 메시지를 탐색하고 적절한 메시지를 찾아 반환한다.

  • 기본 구현체로 DefaultMessageSourceResolvable 클래스가 있으며 ObjectError 클래스의 부모 클래스이다.

  • ObjectError로부터 오류 코드, 메시지 인자, 기본 메시지를 전달 받는다.

오류 메시지가 처리되는 전체적인 과정은 대략 다음과 같다.

img_9.png

V5

검증 로직 분리: 스프링은 검증을 체계적으로 제공하기 위해 Validator 인터페이스를 제공한다.

img_10.png
  • supports() : 해당 Validator가 특정 객체 타입을 지원하는지 확인하는 메서드

  • validate()

    • 실제 유효성 검사를 수행하는 메서드이며 유효성 검사에 실패한 경우 Errors 객체를 사용하여 오류를 추가한다.

    • 주어진 대상 객체가 supports() 메서드에서 true를 반환하는 클래스여야만 검증할 수 있다.

사용 예 - ItemValidator

사용 예 - 컨트롤러


V6

Validator 인터페이스를 사용해서 검증기를 만들면 스프링의 추가적인 도움을 받을 수 있다.

컨트롤러

  • WebDataBinder에 검증기를 추가하면 해당 컨트롤러에서 검증기를 자동으로 적용할 수 있다.

  • @InitBinder는 해당 컨트롤러에만 영향을 준다.

동작 방식

  • @Validated는 검증기를 실행하라는 뜻의 어노테이션이다.

  • WebDataBinder에 등록된 검증기를 찾아서 실행하는데 여러 검증기를 등록한다면 구분이 필요한데 이때 supports()가 사용된다.

Last updated