본문 바로가기
Backend, Server/Spring MVC

[Spring MVC] 검증 Validation - (1)

by ggyongi 2022. 2. 1.
반응형

검증이란 HTTP 요청이 정상인지 검증하는 것.

검증 로직을 처음부터 만들어보면서 어떻게 발전시킬 수 있는지 알아보자.

 

V1 - 직접 검증 개발하기

아래와 같이 코드를 통해 검증 로직을 직접 작성하는 방식의 검증 방법을 만들어보자.

더보기

에러들을 보관할 해쉬맵을 생성하고,

검증 로직을 실행하여 에러 발생 시 해쉬맵에 보관

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

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

        //검증 로직
        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("error = {}", errors);
            model.addAttribute("errors", errors);
            return "validation/v1/addForm";
        }

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

 

이때 오류가 있다면 사용자에게도 이러한 오류를 보여주도록 만들어야 한다.

그러기 위해 errors는 모델에 넣어주고, 뷰템플릿에선는 다음과 같이 errors를 꺼내 사용한다.

사용법: $[errors['키 이름']}

기타 타임리프 문법에 대한 설명은 검증에 초점을 맞추기 위해 생략.

더보기
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <link th:href="@{/css/bootstrap.min.css}"
          href="../css/bootstrap.min.css" rel="stylesheet">
    <style>
        .container {
            max-width: 560px;
        }
        .field-error {
            border-color: #dc3545;
            color: #dc3545;
        }
    </style>
</head>
<body>

<div class="container">

    <div class="py-5 text-center">
        <h2 th:text="#{page.addItem}">상품 등록</h2>
    </div>

    <form action="item.html" th:action th:object="${item}" method="post">

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

        </div>

        <div>
            <label for="itemName" th:text="#{label.item.itemName}">상품명</label>
            <input type="text" id="itemName" th:field="*{itemName}"
                   th:class="${errors?.containsKey('itemName')} ? 'form-control field-error' : 'form-control' "
                   class="form-control" placeholder="이름을 입력하세요">
            <div class="field-error" th:if="${errors?.containsKey('itemName')}" th:text="${errors['itemName']}">
             상품명 오류
            </div>

        </div>
        <div>
            <label for="price" th:text="#{label.item.price}">가격</label>
            <input type="text" id="price" th:field="*{price}"
                   th:class="${errors?.containsKey('price')} ? 'form-control field-error' : 'form-control' "
                   class="form-control" placeholder="가격을 입력하세요">
            <div class="field-error" th:if="${errors?.containsKey('price')}" th:text="${errors['price']}">
                가격 오류
            </div>
        </div>
        <div>
            <label for="quantity" th:text="#{label.item.quantity}">수량</label>
            <input type="text" id="quantity" th:field="*{quantity}"
                   th:class="${errors?.containsKey('quantity')} ? 'form-control field-error' : 'form-control' "
                   class="form-control" placeholder="수량을 입력하세요">
            <div class="field-error" th:if="${errors?.containsKey('quantity')}" th:text="${errors['quantity']}">
                수량 오류
            </div>
        </div>

        <hr class="my-4">

        <div class="row">
            <div class="col">
                <button class="w-100 btn btn-primary btn-lg" type="submit" th:text="#{button.save}">상품 등록</button>
            </div>
            <div class="col">
                <button class="w-100 btn btn-secondary btn-lg"
                        onclick="location.href='items.html'"
                        th:onclick="|location.href='@{/validation/v1/items}'|"
                        type="button" th:text="#{button.cancel}">취소</button>
            </div>
        </div>

    </form>

</div> <!-- /container -->
</body>
</html>

버전 1에는 다음과 같은 문제점이 있음

- 뷰템플릿에서 중복 처리가 많다, 즉 좀 귀찮다.

- 타입 오류 처리가 안됨. 숫자 타입인 price에 문자타입이 입력되면 컨트롤러 자체가 실행되기도 전에 오류가 발생하기 때문에 검증이 되기도 전에 오류 페이지로 넘어간다. 이때 역시 사용자에게 타입을 잘못 입력했다고 알려줄 수 있어야 한다.

- 근데 만약 컨트롤러가 정상 호출된다고 해도, price에는 문자타입을 보관할 수 없는데, 어떤 방식으로 사용자에게 이를 알려줄 것인가가 문제다.

- 결론적으로 많은 한계점이 존재

 

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


        //검증에 실패하면 다시 입력 폼으로
        if (bindingResult.hasErrors()) {
            log.info("error = {}", 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 : 오류의 내용을 보관해주는 스프링에서 제공해주는 객체

(** 반드시 @ModelAttribue 객체 뒤에 와야 한다. 타겟을 알아야 하기 때문에! **)

.addError()를 통해 에러를 보관해줄 수 있다. 

이때 FieldError 객체와 ObjectError 객체를 생성해서 담는 것을 볼 수 있다.

 

FieldError : 특정 필드에 연관된 에러

FieldError의 생성자 

public FieldError(String objectName, String field, String defaultMessage) {}

objectName : @ModelAttribue 이름

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

defaultMessage : 오류 기본 메시지

 

ObjectError : 특정 필드에 국한되지 않는, 글로벌 오류

ObjectError의 생성자

public ObjectError(String objectName, String defaultMessage) {}

objectName : @ModelAttribute 의 이름

defaultMessage : 오류 기본 메시지

 

아니 그래서, 뭐가 좋다는 건데? 뷰 템플릿을 살펴보자.

 

버전업된 뷰 템플릿:

더보기
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <link th:href="@{/css/bootstrap.min.css}"
          href="../css/bootstrap.min.css" rel="stylesheet">
    <style>
        .container {
            max-width: 560px;
        }
        .field-error {
            border-color: #dc3545;
            color: #dc3545;
        }
    </style>
</head>
<body>

<div class="container">

    <div class="py-5 text-center">
        <h2 th:text="#{page.addItem}">상품 등록</h2>
    </div>

    <form action="item.html" th:action th:object="${item}" method="post">

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

        </div>

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

        </div>
        <div>
            <label for="price" th:text="#{label.item.price}">가격</label>
            <input type="text" id="price" th:field="*{price}"
                   th:errorclass="field-error" class="form-control" placeholder="가격을 입력하세요">
            <div class="field-error" th:errors="*{price}">
                가격 오류
            </div>
        </div>
        <div>
            <label for="quantity" th:text="#{label.item.quantity}">수량</label>
            <input type="text" id="quantity" th:field="*{quantity}"
                   th:errorclass="field-error" class="form-control" placeholder="수량을 입력하세요">
            <div class="field-error" th:errors="*{quantity}">
                수량 오류
            </div>
        </div>

        <hr class="my-4">

        <div class="row">
            <div class="col">
                <button class="w-100 btn btn-primary btn-lg" type="submit" th:text="#{button.save}">상품 등록</button>
            </div>
            <div class="col">
                <button class="w-100 btn btn-secondary btn-lg"
                        onclick="location.href='items.html'"
                        th:onclick="|location.href='@{/validation/v2/items}'|"
                        type="button" th:text="#{button.cancel}">취소</button>
            </div>
        </div>

    </form>

</div> <!-- /container -->
</body>
</html>

#fields : #fields 로 BindingResult 가 제공하는 검증 오류에 접근할 수 있다.

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

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

 

결론 : v1에 비해 v2에서는 bindingResult 덕분에 뷰템플릿 코드가 훨씬 간결해졌다.

 

bindingResult의 장점은 이 뿐만이 아니다.

타입 오류 발생 시에도 오류 페이지를 띄우는 것이 아니라 오류 정보(FieldError)를 bindingResult에 담고 컨트롤러를 정상 호출한다. 

 

근데 이때 오류가 발생하면 기존값이 다 지워진다. 

FieldError와 ObjectError 생성시 다른 생성자를 사용하면 그 문제를 해결할 수 있다.

 

 

V2의 업그레이드1

개선코드는 아래와 같다.

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

        //검증 로직
        if (!StringUtils.hasText(item.getItemName())) {
            bindingResult.addError(new FieldError("item", "itemName", item.getItemName(), false, null, null, "상품 이름은 필수입니다."));
        }

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

        if (item.getQuantity() == null || item.getQuantity() > 9999) {
            bindingResult.addError(new FieldError("item", "quantity", item.getQuantity(), false, null, null, "수량은 최대 9,999 까지 허용합니다."));
        }

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


        //검증에 실패하면 다시 입력 폼으로
        if (bindingResult.hasErrors()) {
            log.info("error = {}", 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}";
    }

FieldError와 ObjectError의 또 다른 생성자는 다음과 같다.

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 : 기본 오류 메시지

 

ObjectError 역시 위와 유사한 생성자를 제공한다.

 

아무튼 위와 같은 생성자를 사용하면, 파라미터로 rejectedValue, 즉 사용자가 무엇을 입력했었는 지도 함께 저장해준다.

그럼 그 값을 이용하여 사용자의 입력값을 유지한다.

 

*그럼 타입 오류 발생 시에는?

타입 오류 발생 시에는 FieldError가 자동으로 생성되고, 그 잘못된 값을 저장해놓는다. 그리고 이를 bindingResult에 담은 뒤에 컨트롤러를 호출한다.

 

* 화면에 보여지는 방식을 좀 더 자세히!

th:field="*{price}"

타임리프의 th:field 는 매우 똑똑하게 동작하는데, 정상 상황에는 모델 객체의 값을 사용하지만, 해당 필드에 관련하여 오류가 존재한다면 FieldError의 rejectedValue에서 보관한 값을 사용해서 값을 출력한다.

 

근데 여기서 궁금점이 생길 수 있다. 

인강을 통해 실행했던 예제에서 만약 price 입력안에 qq같은 문자 데이터를 입력하게 되면,

bindingResult에는 price 필드에 관한 오류가 총 2개 생길 것이다.

하나는 타입오류로 인해 스프링이 만들어준 FieldError이고

하나는 내가 작성한 로직에 위반하여 내가 만들어준 FieldError이다. 

이때 각각의 FieldError에는 rejectedValue를 다르게 담고 있다. 하나는 qq, 하나는 null

그러면 타임 리프의 th:field는 오류 발생 시 둘 중에 무엇을 가져와서 화면에 보여주는가?

QNA를 참고하니 먼저 설정해준 FieldError의 값을 가져온다고 한다. 즉 여기선 타입 오류로 인한 에러가 먼저니까 화면에도 그 값이 뿌려진다.

 

결론적으로 코드 개선을 통해 내가 입력한 데이터가 모두 잘 유지되도록 하였다. 근데 여전히 개선점은 존재한다.

price에 관한 오류가 2개인 경우 그 2개에 대한 오류 메시지가 모두 화면에 출력된다.

 

 

 

V2의 업그레이드2

오류 메시지를 체계적으로 다뤄보자.

 

메시지는 지난 번에 배운대로 다음과 같이 설정을 추가할 수 있다.

//application.properties

spring.messages.basename=messages,errors

 

메시지 사용하도록 개선한 코드는 아래와 같다.

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

        //검증 로직
        if (!StringUtils.hasText(item.getItemName())) {
            bindingResult.addError(new FieldError("item", "itemName", item.getItemName(), false, new String[]{"required.item.itemName"}, 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));
            }
        }


        //검증에 실패하면 다시 입력 폼으로
        if (bindingResult.hasErrors()) {
            log.info("error = {}", 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}";
    }

FieldError와 ObjectError의 파라미터인 codes, arguments에 적절한 값을 넣어주면 된다.

codes는 메시지 코드를 지정해주면 된다. 배열로 여러 값을 전달할 수 있고, 순서대로 매칭해서 먼저 매칭되는 메시지가 사용된다.

arguments에는 메시지 코드의 {0},  {1} 같은 변수를 지정해줄 수 있다.

 

 

V2의 업그레이드3

위의 코드를 좀 더 간략히 할 수 있다. 

컨트롤러에서 BindingResult는 이미 검증 대상 객체인 target 바로 뒤에 오기 때문에

본인이 target해야 할 객체를 이미 알고 있다. 

따라서 우리는 bindingResult가 제공하는 rejectValue(), reject() 등을 사용하여

직접 FieldError, ObjectError 등을 만들지 않도록 할 수 있다.

 

아래는 개선한 코드다.

더보기
//@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() > 1000000) {
            bindingResult.rejectValue("price", "range",new Object[]{1000, 1000000}, 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("error = {}", 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}";
    }

이때 rejectValue()의 생성자는 다음과 같다.

void rejectValue(@Nullable String field, String errorCode,
@Nullable Object[] errorArgs, @Nullable String defaultMessage);

 

그래서 얻는 장점은?

단순히 코드량을 줄일 수 있다는 장점 이외에도, errorCode를 사용할 때 편리해진다.

여기서 이 errorCode는 FieldError에 사용된 메시지 코드와는 약간 다르다.

위의 코드에서 보면

"required"라고만 코드를 넣어주었는데, 자동적으로 "required.item.itemName"이라는 메시지를 찾았다.

이게 가능한 이유는 MessageCodeResolver 때문이다.

 

MessageCodeResolver란?

- 검증 오류 코드로 메시지 코드들을 생성해준다.

- 기본 규칙은 다음과 같다.

1. 객체 오류의 경우

객체 오류의 경우 다음 순서로 2가지 생성
1.: code + "." + object name
2.: code
예) 오류 코드: required, object name: item
1.: required.item
2.: required

2. 필드 오류의 경우

필드 오류의 경우 다음 순서로 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()를 사용하면 내부에서 이 리졸버를 사용하여 메시지 코드들을 자동 생성해준다.

이때 위와 같은 규칙들로, codes 부분에 배열 형태로 여러 메시지 코드들을 넣어준다. 

그 예시로, rejectValue("itemName", "required")를 사용하면 아래와 같은 오류코드가 자동 생성된다.

- required.item.itemName

- required.itemName

- required.java.lang.String

- required

 

reject("totalPriceMin")의 경우,

- totalPriceMin.item

- totalPriceMin

 

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

 

 

V2의 업그레이드4

- validator 클래스를 따로 만들어줌으로써 controller 내부의 코드를 눈에 보기 좋게 만들 수 있다.

package hello.itemservice.web.validation;

import hello.itemservice.domain.item.Item;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;

@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() > 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);
            }
        }

    }
}

그럼 Controller는 아래와 같이 간단해진다.

private final ItemValidator itemValidator;

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

    itemValidator.validate(item, bindingResult);

    //검증에 실패하면 다시 입력 폼으로
    if (bindingResult.hasErrors()) {
        log.info("error = {}", 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}";
}

 

근데 왜 굳이 Validator라는 인터페이스를 구현하도록 ItemValidator를 만들었을까?

스프링의 추가적인 도움을 받을 수 있기 때문이다. 아래를 보자.

 

V2의 업그레이드5

- 컨트롤러에 다음을 추가하자. WebDataBinder에 검증기를 추가하면

해당 컨트롤러에서는 검증기를 자동으로 적용할 수 있게 된다.

@InitBinder
public void init(WebDataBinder dataBinder) {
    dataBinder.addValidators(itemValidator);
}

아래처럼 컨트롤러에선 더 이상 검증기를 따로 불러내지 않아도 된다.
대신 @Validated 어노테이션을 검증 객체에 달아준다.

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

    //검증에 실패하면 다시 입력 폼으로
    if (bindingResult.hasErrors()) {
        log.info("error = {}", 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}";
}

 

 

@Validated 는 검증기를 실행하라는 애노테이션이다. 이 애노테이션이 붙으면 앞서 WebDataBinder 에 등록한 검증기를 찾아서 실행한다. 그런데 여러 검증기를 등록한다면 그 중에 어떤 검증기가 실행되어야 할지 구분이 필요하다. 이때 supports() 가 사용된다.

 

 

----------------------------------------------

참고 : 인프런 김영한님 강의(스프링 MVC 2편 - 백엔드 웹 개발 활용 기술)

 

비전공자 네카라 신입 취업 노하우

시행착오 끝에 얻어낸 취업 노하우가 모두 담긴 전자책!

kmong.com

댓글