Validation

뚝딱이·2022년 8월 12일
0

스프링 MVC

목록 보기
14/23

웹 애플리케이션에서 상품을 등록할 때 예를 들어 숫자를 적어야하는 수량에 문자를 적으면 어떻게 될까. 우리가 이제껏 만든 간단한 프로젝트에서는 예외를 만들어주지 않아 이러한 검증오류가 발생하면 오류화면으로 바로 이동하도록 했다.

이렇게 되면 사용자는 처음부터 해당 폼으로 다시 이동해서 입력을 해야 한다.
아마도 이런 서비스라면 사용자는 금방 떠나버릴 것이다. 웹 서비스는 폼 입력시 오류가 발생하면, 고객이 입력한 데이터를 유지한 상태로 어떤 오류가 발생했는지 친절하게 알려주어야 한다.

컨트롤러의 중요한 역할중 하나는 HTTP 요청이 정상인지 검증하는 것이다. 그리고 정상 로직보다 이런 검증 로직을 잘 개발하는 것이 어쩌면 더 어려울 수 있다.

참고

  • 클라이언트 검증은 조작할 수 있으므로 보안에 취약하다.
  • 서버만으로 검증하면, 즉각적인 고객 사용성이 부족해진다.
  • 둘을 적절히 섞어서 사용하되, 최종적으로 서버 검증은 필수
  • API 방식을 사용하면 API 스펙을 잘 정의해서 검증 오류를 API 응답 결과에 잘 남겨주어야 함
  • 먼저 검증을 직접 구현해보고, 뒤에서 스프링과 타임리프가 제공하는 검증 기능을 활용해보자.

v1

검증 직접 처리

상품저장 로직이다. 위와 같은 로직일 때 상품 저장에 실패하면 어떻게 동작하도록 설계해야할까.

위와 같이 상품저장에 실패했을 때 검증오류 결과를 포함한 Model을 상품 등록 폼에 전달해서 재요청을 받도록하자.

그렇다면 위에서 이러한 검증이 컨트롤러에서 이루어져야한다고 배웠으니 컨트롤러를 수정해보자.

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

        // 검증 오류 결과를 보관할 객체
        Map<String, String> errors = new LinkedHashMap<>();

        //검증로직
        if (!StringUtils.hasText(item.getItemName())) {//상품이름이 없을 때
            errors.put("itemName", "상품 이름은 필수입니다.");
        }

        if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
            errors.put("price", "가격은 1,000원 ~ 1,000,000원까지 허용합니다. ");
        }

        if (item.getQuantity() == null || item.getQuantity() >= 9999) {
            errors.put("quantity", "수량은 최대 9,999개 까지 허용됩니다.");
        }

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

        //검증에 실패하면 다시 입력 폼으로
        if(!errors.isEmpty()){
            log.info("errors = {} ", errors);
            model.addAttribute("errors", errors);
            return "validation/v1/addForm"; //다시 addForm으로 보내도 입력했던 내용이 남아있다. 왜 ?  ModelAttribute 때문에 !
        }

        //성공 로직
        Item savedItem = itemRepository.save(item);
        redirectAttributes.addAttribute("itemId", savedItem.getId());
        redirectAttributes.addAttribute("status", true);
        return "redirect:/validation/v1/items/{itemId}";
    }

먼저 검증 오류정보를 받을 Map<String, String> errors = new HashMap<>();을 생성한다.

검증시 오류가 발생하면 errors 에 담아둔다. 이때 어떤 필드에서 오류가 발생했는지 구분하기 위해 오류가 발생한 필드명을 key 로 사용한다. 이후 뷰에서 이 데이터를 사용해서 고객에게 친절한 오류 메시지를 출력할 수 있다.

만약 검증에서 오류 메시지가 하나라도 있으면 오류 메시지를 출력하기 위해 model 에 errors 를 담고, 입력 폼이 있는 뷰 템플릿으로 보낸다.

<div class="field-error" th:if="${errors?.containsKey('price')}"
th:text="${errors['price']}">
 가격 오류
 </div>
<div th:if="${errors?.containsKey('globalError')}">
 <p class="field-error" th:text="${errors['globalError']}">전체 오류 메시지</p>
</div>

오류 메시지는 errors 에 내용이 있을 때만 출력하면 된다. 타임리프의 th:if 를 사용하면 조건에 만족할 때만 해당 HTML 태그를 출력할 수 있다.

th:class="${errors?.containsKey('quantity')} ? 'form-control 
field-error' : 'form-control'"
<input type="text" th:classappend="${errors?.containsKey('itemName')} ? 'fielderror'
: _"
 class="form-control">

classappend 를 사용해서 해당 필드에 오류가 있으면 field-error 라는 클래스 정보를 더해서 폼의 색깔을 빨간색으로 강조한다. 만약 값이 없으면 _ (No-Operation)을 사용해서 아무것도 하지 않는다.

위의 두가지 방법은 오류시의 폼의 색을 변경하는 방법이다.

참고
Safe Navigation Operator 만약 여기에서 errors 가 null 이라면 어떻게 될까?
생각해보면 등록폼에 진입한 시점에는 errors 가 없다.
따라서 errors.containsKey() 를 호출하는 순간 NullPointerException 이 발생한다.

errors?. 은 errors 가 null 일때 NullPointerException 이 발생하는 대신, null 을 반환하는 문법이다.
th:if 에서 null 은 실패로 처리되므로 오류 메시지가 출력되지 않는다.

이렇게 검증 부분을 만들어보았지만, 아직 남은 문제들이 많다.

  • 템플릿에서 중복 처리가 많다. 뭔가 비슷하다.
  • 타입 오류 처리가 안된다. 숫자 타입에 문자가 들어오면 오류가 발생한다. 그런데 이러한 오류는 스프링MVC에서 컨트롤러에 진입하기도 전에 예외가 발생하기 때문에, 컨트롤러가 호출되지도 않고, 400 예외가 발생하면서 오류 페이지를 띄워준다.
  • Item 의 price 에 문자를 입력하는 것 처럼 타입 오류가 발생해도 고객이 입력한 문자를 화면에 남겨야 한다. 만약 컨트롤러가 호출된다고 가정해도 Item 의 price 는 Integer 이므로 문자를 보관할 수가 없다. 결국 문자는 바인딩이 불가능하므로 고객이 입력한 문자가 사라지게 되고, 고객은 본인이 어떤 내용을 입력해서 오류가 발생했는지 이해하기 어렵다.
  • 결국 고객이 입력한 값도 어딘가에 별도로 관리가 되어야 한다.

v2

BindingResult

    @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() > 1000000) {
            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("itme","가격 * 수량의 합은 10,000원 이상이어야 합니다. 현재 값 = " + resultPrice ));
            }
        }

        //검증에 실패하면 다시 입력 폼으로
        if(bindingResult.hasErrors()){//스프링이 뷰로 보내주기 때문에 따로 model.addAttribute할 필요가 없음
            log.info("errors = {} ", bindingResult);
            return "validation/v2/addForm"; //다시 addForm으로 보내도 입력했던 내용이 남아있다. 왜 ?  ModelAttribute 때문에 !
        }

        //성공 로직
        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 다음에 와야 한다. 바인딩할 객체 뒤에 와서 bindingresult가 인식가능하다.

public FieldError(String objectName, String field, String defaultMessage) {}
필드에 오류가 있으면 FieldError 객체를 생성해서 bindingResult 에 담아두면 된다.

  • objectName : @ModelAttribute 이름
  • field : 오류가 발생한 필드 이름
  • defaultMessage : 오류 기본 메시지

public ObjectError(String objectName, String defaultMessage) {}
특정 필드를 넘어서는 오류가 있으면 ObjectError 객체를 생성해서 bindingResult 에 담아두면 된다.

  • objectName : @ModelAttribute 의 이름
  • defaultMessage : 오류 기본 메시지

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

  • #fields : #fields 로 BindingResult 가 제공하는 검증 오류에 접근할 수 있다.
  • th:errors : 해당 필드에 오류가 있는 경우에 태그를 출력한다. th:if 의 편의 버전이다.
  • th:errorclass : th:field 에서 지정한 필드에 오류가 있으면 class 정보를 추가한다.

글로벌 오류 처리

<div th:if="${#fields.hasGlobalErrors()}">
 <p class="field-error" th:each="err : ${#fields.globalErrors()}" th:text="$
{err}">전체 오류 메시지</p>
</div>

반복문을 이용하여 글로벌 오류들을 출력할 수 있다.

필드 오류 처리

<input type="text" id="itemName" th:field="*{itemName}"
 th:errorclass="field-error" class="form-control" placeholder="이름을
입력하세요">
<div class="field-error" th:errors="*{itemName}">
 상품명 오류
</div>

BindingResult 가 있으면 @ModelAttribute 에 데이터 바인딩 시 오류가 발생해도 컨트롤러가 호출된다

예) @ModelAttribute에 바인딩 시 타입 오류가 발생하면?
BindingResult 가 없으면 400 오류가 발생하면서 컨트롤러가 호출되지 않고, 오류 페이지로 이동한다.
BindingResult 가 있으면 오류 정보( FieldError )를 BindingResult 에 담아서 컨트롤러를 정상 호출한다.

따라서 타입 오류로 인해 바로 오류화면으로 넘어가는 문제를 해결할 수 있다.

BindingResult에 검증 오류를 적용하는 3가지 방법

  • @ModelAttribute 의 객체에 타입 오류 등으로 바인딩이 실패하는 경우 스프링이 FieldError 생성해서 BindingResult 에 넣어준다.
  • 개발자가 직접 넣어준다.
  • Validator 사용

BindingResult 는 검증할 대상 바로 다음에 와야한다. 순서가 중요하다. 예를 들어서 @ModelAttribute Item item , 바로 다음에 BindingResult 가 와야 한다.

BindingResult 는 Model에 자동으로 포함된다. 따라서 addAttribute 해줄 필요가 없다.

BindingResult와 Errors

org.springframework.validation.Errors
org.springframework.validation.BindingResult

BindingResult 는 인터페이스이고, Errors 인터페이스를 상속받고 있다.
실제 넘어오는 구현체는 BeanPropertyBindingResult 라는 것인데, 둘다 구현하고 있으므로 BindingResult 대신에 Errors 를 사용해도 된다. Errors 인터페이스는 단순한 오류 저장과 조회 기능을 제공한다. BindingResult 는 여기에 더해서 추가적인 기능들을 제공한다. addError() 도 BindingResult 가 제공하므로 여기서는 BindingResult 를 사용하자. 주로 관례상 BindingResult 를 많이 사용한다.

FieldError, ObjectError

FieldError 는 두 가지 생성자를 제공한다.

public FieldError(String objectName, String field, String defaultMessage);
public FieldError(String objectName, String field, @Nullable Object 
rejectedValue, boolean bindingFailure, @Nullable String[] codes, @Nullable
Object[] arguments, @Nullable String defaultMessage)
  • objectName : 오류가 발생한 객체 이름
  • field : 오류 필드
  • rejectedValue : 사용자가 입력한 값(거절된 값)
  • bindingFailure : 타입 오류 같은 바인딩 실패인지, 검증 실패인지 구분 값 - 데이터가 넘어왔는지
  • codes : 메시지 코드
  • arguments : 메시지에서 사용하는 인자
  • defaultMessage : 기본 오류 메시지

new FieldError("item", "price", item.getPrice(), false, null, null, "가격은 1,000 ~ 1,000,000 까지 허용합니다.")
이전에는 오류가 생겨도 ModelAttribute 덕에 사용자 입력 값을 유지했지만, 사용자의 입력 데이터가 컨트롤러의 @ModelAttribute 에 바인딩되는 시점에 오류가 발생하면 모델 객체에 사용자 입력 값을 유지하기 어렵다. 타입 오류가 발생하는 경우가 예시이다.
따라서 오류가 발생한 경우 사용자 입력 값을 보관하는 별도의 방법이 필요하다. 그리고 이렇게 보관한 사용자 입력 값을 검증 오류 발생시 화면에 다시 출력하면 된다.
FieldError 는 오류 발생시 사용자 입력 값을 저장하는 기능을 제공한다.
여기서 rejectedValue 가 바로 오류 발생시 사용자 입력 값을 저장하는 필드다.
bindingFailure 는 타입 오류 같은 바인딩이 실패했는지 여부를 적어주면 된다. 여기서는 바인딩이 실패한 것은 아니기 때문에 false 를 사용한다.

th:field="*{price}"
정상 상황에는 모델 객체의 값을 사용하지만, 오류가 발생하면 FieldError 에서 보관한 값을 사용해서 값을 출력한다.

스프링의 바인딩 오류 처리
타입 오류로 바인딩에 실패하면 스프링은 FieldError 를 생성하면서 사용자가 입력한 값을 넣어둔다. 이때는 바인딩 실패가 일어난것이므로 FieldError를 생성할 때 bindingFailure가 true로 입력된다. 그리고 해당 오류를 BindingResult 에 담아서 컨트롤러를 호출한다. 따라서 타입 오류 같은 바인딩 실패시에도 사용자의 오류 메시지를 정상 출력할 수 있다.


오류 코드와 메시지 처리

error메시지는 한번 만들어놓으면 여러곳에서 같은 것을 쓸수도 있고 재사용이 가능하며, 변경에 용이해야한다. 따라서 이전의 messages.properties를 만들었던 것 처럼 errors.properties를 만들어 관리하자.

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

  • codes : required.item.itemName 를 사용해서 메시지 코드를 지정한다. 메시지 코드는 하나가 아니라 배열로 여러 값을 전달할 수 있는데, 순서대로 매칭해서 처음 매칭되는 메시지가 사용된다.
  • arguments : Object[]{1000, 1000000} 를 사용해서 코드의 {0} , {1} 로 치환할 값을 전달한다.

하지만 위와 같이 FieldError와 ObjectError는 다루기도 어렵고 복잡하다.

컨트롤러에서 BindingResult 는 검증해야 할 객체인 target 바로 다음에 온이 때문에 target을 알고 있다. 따라서 이를 이용하여 rejectValue() , reject()를 사용해 더 편리하게 만들어 보자. FieldError , ObjectError 를 직접 생성하지 않고, 깔끔하게 검증 오류를 다룰 수 있다.

bindingResult.rejectValue("itemName", "required");

위와 같이 사용하자. 근데 이상한 점이 하나있다. 이전에서는 errors.properties에서 맞는 메세지를 가져오기 위해 error code를 넣어줬는데 위의 코드에선 보이지 않고, 필드명과 오류코드의 일부분만 보인다. 어떻게 한건지 아래에서 알아보자.

rejectValue()

void rejectValue(@Nullable String field, String errorCode,
@Nullable Object[] errorArgs, @Nullable String defaultMessage);
  • field : 오류 필드명
  • errorCode : 오류 코드
  • errorArgs : 오류 메시지에서 {0} 을 치환하기 위한 값
  • defaultMessage : 오류 메시지를 찾을 수 없을 때 사용하는 기본 메시지

FieldError() 를 직접 다룰 때는 오류 코드를 ange.item.price 와 같이 모두 입력했다. 그런데 rejectValue() 를 사용하고 부터는 오류 코드를 range 로 간단하게 입력했다. 그래도 오류 메시지를 잘찾아서 출력한다. 이것이 가능한 이유는 MessageCodesResolver 때문인데, 이는 아래에서 더 자세히 알아보자.

MessageCodesResolver

검증 오류 코드로 메시지 코드들을 생성한다.
MessageCodesResolver 인터페이스이고 DefaultMessageCodesResolver 는 기본 구현체이다.
주로 ObjectError , FieldError와 함께 사용한다.

객체 오류

객체 오류의 경우 다음 순서로 2가지 생성

1.: code + "." + object name
2.: code
예) 오류 코드: required, object name: item
1.: required.item
2.: required

필드 오류

필드 오류의 경우 다음 순서로 4가지 메시지 코드 생성

1.: code + "." + object name + "." + field
2.: code + "." + field
3.: code + "." + field type
4.: code
예) 오류 코드: typeMismatch, object name "user", field "age", field type: int
1. "typeMismatch.user.age"
2. "typeMismatch.age"
3. "typeMismatch.int"
4. "typeMismatch"

동작 방식
rejectValue() , reject() 는 내부에서 MessageCodesResolver 를 사용한다. 여기에서 메시지 코드들을 생성한다.
FieldError , ObjectError 의 생성자를 보면, 오류 코드를 하나가 아니라 여러 오류 코드를 가질 수 있다.
MessageCodesResolver 를 통해서 생성된 순서대로 오류 코드를 보관한다.
이 부분을 BindingResult 의 로그를 통해서 확인해보자.
codes [range.item.price, range.price, range.java.lang.Integer, range]

타임리프 화면을 렌더링 할 때 th:errors 가 실행된다. 만약 이때 오류가 있다면 생성된 오류 메시지 코드를 순서대로 돌아가면서 메시지를 찾는다. 그리고 없으면 디폴트 메시지를 출력한다

MessageCodesResolver 는 required.item.itemName 처럼 구체적인 것을 먼저 만들어주고, required 처럼 덜 구체적인 것을 가장 나중에 만든다. 크게 중요하지 않은 메시지는 범용성 있는 requried 같은 메시지로 끝내고, 정말 중요한 메시지는 꼭
필요할 때 구체적으로 적어서 사용하는 방식이 더 효과적이기 때문에 이러한 방식을 사용한다.

1. "typeMismatch.user.age"
2. "typeMismatch.age"
3. "typeMismatch.int"
4. "typeMismatch"

위와 같이 생성되었다고 했을 때, 구체적인 것에서 덜 구체적인 순서대로 찾는다. 메시지에 1번이 없으면 2번을 찾고, 2번이 없으면 3번을 찾는다. 4번까지 찾았는데도 없을 경우 defaultMessage를 사용한다. 하지만 defaultMessage 조차도 없다면 오류가 난다. 이렇게 되면 만약에 크게 중요하지 않은 오류 메시지는 기존에 정의된 것을 그냥 재활용 하면 된다

if (!StringUtils.hasText(item.getItemName())) {
 bindingResult.rejectValue("itemName", "required", "기본: 상품 이름은
필수입니다.");
}

위와 같았던 코드를 한줄로 줄일 수 있다. 제공하는 기능은 Empty , 공백 같은 단순한 기능이다.

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

정리
1. rejectValue() 호출
2. MessageCodesResolver 를 사용해서 검증 오류 코드로 메시지 코드들을 생성
3. new FieldError() 를 생성하면서 메시지 코드들을 보관
4. th:erros 에서 메시지 코드들로 메시지를 순서대로 메시지에서 찾고, 노출

이제 메시지 코드 전략을 정리해보자.

숫자가 들어가야할 필드에 문자가 들어간다면, 로그에 BindingResult 에 FieldError 가 담겨있고, 다음과 같은 메시지 코드들이 생성된 것을 확인할 수 있다.

codes[typeMismatch.item.price,typeMismatch.price,typeMismatch.java.lang.Integer,typeMismatch]

타입오류가 발생했기 때문에 typeMismatch라는 오류 코드를 사용한것을 확인할 수 있다. 그리고 이러한 오류가 날 경우 아래와 같은 오류 메시지가 출력되는데, 우리는 typeMismatch 오류코드에 대한 오류메시지를 errors.properties에 작성하지 않았으므로 이는 defaultMessage임을 알 수 있다. 따라서 errors.properties에 typeMismatch 오류 코드로 오류메시지를 작성하면 개발자가 작성한 오류메시지를 출력할 수 있다.

Failed to convert property value of type java.lang.String to required type java.lang.Integer for property price; nested exception is 
java.lang.NumberFormatException: For input string: "A"

Validator 분리

우리는 컨트롤러에서 검증이 이루어져야하기때문에 컨트롤러에 너무 방대한 양의 검증정보를 집어넣었다. 이로인해 컨트롤러가 하는 중심 로직에 비해 검증로직이 너무 많아 컨트롤러의 역할이 제대로 분배되지 않는다. 역할을 나누기 위해 검증 로직만 관리하는 클래스를 만들어 따로 관리하자.

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

public interface Validator {
boolean supports(Class<?> clazz);
void validate(Object target, Errors errors);
}
  • supports() {} : 해당 검증기를 지원하는 여부 확인(뒤에서 설명)
  • validate(Object target, Errors errors) : 검증 대상 객체와 BindingResult
@Component
//스프링 빈에 등록
public class ItemValidator implements Validator {


    @Override
    public boolean supports(Class<?> clazz) {
        return Item.class.isAssignableFrom(clazz);
        //item=clazz
        //item == subItem
        //==을 쓰는 것보단 나은게, 자식 클래스도 다 사용가능하기 때문
    }

    @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,1000000},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.validate(item, bindingResult); 한 줄로 검증 로직을 표현할 수 있다.

WebDataBinder 는 스프링의 파라미터 바인딩의 역할을 해주고 검증 기능도 내부에 포함한다.

@InitBinder
public void init(WebDataBinder dataBinder) {
 log.info("init binder {}", dataBinder); dataBinder.addValidators(itemValidator);
}

이렇게 WebDataBinder 에 검증기를 추가하면 해당 컨트롤러에서는 검증기를 자동으로 적용할 수 있다. @InitBinder 해당 컨트롤러에만 영향을 준다.

위와 같이 설정하면 컨트롤러에선
public String addItemV6(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
와 같이 validator를 호출하지 않고 어노테이션만으로 간편하게 사용할 수 있다.

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

글로벌 설정은 main 메서드가 있는 클래스에 WebMvcConfigurer 인터페이스를 받아 아래 메서드를 추가 해주면된다. 글로벌 설정을 하면 모든 컨트롤러에서 해당 검증기가 실행된다. 따라서 기존 컨트롤러에 있는 @InitBinder를 제거해도 정상실행한다. 하지만 이러한 글로벌 설정은 잘 사용하지 않기 때문에 참고정도로만 알아두자.

 @Override
 public Validator getValidator() {
 return new ItemValidator();
 }

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


출처 : 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술

profile
백엔드 개발자 지망생

0개의 댓글