4. 검증1-Validation

ys·2024년 1월 8일

Spring-mvc2

목록 보기
4/10

김영한 강사님의 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술을 듣고 정리한 내용입니다. 자세한 내용은 강의를 참고해주세요


View템플릿을 이용한 웹 어플리케이션 개발

  • 지금까지 한걸 정리해보면, MVC 모델에서 Controller에서, Model을 파라미터로 받고
  • Controller에서 addAttribute()로 Model에 넣어주거나, @ModelAttribute를 이용해 지정한 객체를, addAttribute()없이 Model에 넣어준다
  • 그다음 뷰에서 thymeleaf 템플릿을 이용해, Model에서 받은 데이터를 이용해 html문서를 http응답으로 넘겨서, 화면을 렌더링한다
  • 컨트롤러에서 여러 유연한 파라미터를 받을 수 있는 것Argument Roslever가 컨트롤러가 원하는 전달 데이터를 생성해주고 이는 http message converter을 이용해서 생성해준다
  • 이런 유연함 덕분에, 우리는 url을 리소스를 기준으로 만들 수 있고, @PathVariable로 url을 짤 수 있다.
  • PRG방식으로 짤 때, 파라미터로 RedirectAttribute를 이용해 리다이렉트 url도 인코딩하고, pathVariable과 쿼리 파라미터또한 모두 처리해준다
  • 오늘 배울 검증기능도 이런 Spring MVC의 어노테이션기반의 장점이 또 들어난다!

지금까지, 우리가 만든 웹 어플리케이션을 보면 꽤 잘 구현된듯 싶다. 그런데 아무것도 입력하지 않아도, 상품이 등록된다. 또 상품 가격에 문자열을 넣으면 오류가 난다... 이런 문제를 해결하기 위해서 검증 요구사항이 등장한다.

  • 지금까지 만든 웹 애플리케이션은 폼 입력시 숫자를 문자로 작성하거나해서 검증 오류가 발생하면 오류 화면으로 바로 이동한다
  • 사용자는 처음부터 해당 폼으로 다시 이동해서 입력을 해야 한다. 아마도 이런 서비스라면 사용자는 금방 떠나버릴 것이다.
  • 웹 서비스는 폼 입력시 오류가 발생하면,
    • 고객이 입력한 데이터를 유지한 상태
    • 어떤 오류가 발생했는지 친절하게 알려주어야 한다
  • 컨트롤러의 중요한 역할중 하나는 HTTP 요청이 정상인지 검증하는 것이다
  • 개발에서 정상로직보다 검증 로직을 잘 짜는게 중요하다!!!!!

cf) 참고: 클라이언트 검증, 서버 검증

  • 클라이언트 검증은 조작할 수 있으므로 보안에 취약하다.
  • 서버만으로 검증하면, 즉각적인 고객 사용성이 부족해진다.
  • 둘을 적절히 섞어서 사용하되, 최종적으로 서버 검증은 필수
  • API 방식을 사용하면 API 스펙을 잘 정의해서 검증 오류를 API 응답 결과에 잘 남겨주어야 함

  • 이렇게 컨트롤러에서 서버 검증 로직을 실패하면
  • Model에 틀린 data를 저장된상태로 view로 이동한다
  • 이때 다시 상품 등록 폼인 addForm.html로 이동하고 어떤 오류인지 친절하게 알려줘야 한다.

검증 직접 처리 - V1

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

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

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

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

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

        // 특정필드가 아닌 복합 룰 검증
        if (item.getPrice() != null && item.getQuantity() != null){
            int resultPrice = item.getPrice() * item.getQuantity();
            if (resultPrice < 10000) {
                erroers.put("globalError", "가격 * 수량의 합은 10,000원 이상이여야 합니다. 현재 값 = " + resultPrice);
            }
        }
        // 검증에 실패하면 다시 입력 폼으로 이동해야함
        if (!erroers.isEmpty()){
            log.info("errors = {} ",erroers);
            model.addAttribute("errors", erroers);
            return "validation/v1/addForm";
        }

        // 성공 로직 !
        Item savedItem = itemRepository.save(item);
        redirectAttributes.addAttribute("itemId", savedItem.getId());
        redirectAttributes.addAttribute("status", true);
        return "redirect:/validation/v1/items/{itemId}";
    }
  • 먼저 오류를 저장할 Map으로 오류 정보를 담아둔다
  • 그리고 오류 로직들의 이름과 출력 메시지를 Map에 담아둔다.
  • 이때 item의 필드값의 오류라면 ->fieldError 즉 itemName ,price,quantity의 문제이고
  • 복합적인 오류는 -> globalError가 된다.
  • 그리고 우리가 GetMapping의 addForm에서, item의 빈 객체를 생성하고 이를 thymeleaf에 넘겨서 html Form데이터를 받았었는데,
    만약 검증 오류가 나면, 그 빈 item 객체에, 오류가 난 입력 html Form데이터가 저장이 되어있어서
  • 고객에게 그 오류난 데이터까지 보여줄 수 있는 장점이 있다
  • 이제 오류 메시지를 보여주면 된다.
  • 오류 메시지는 errors의 HashMap의 value값으로 저장해주었다
  • key값만 잘 호출해주면 오류코드를 view에서 나타낼 수 있을 것이다


위 사진 처럼, global Error는 맨 위에, field Error는 각 상품 입력 밑에 넣어줄 것이다

  • 이제 html form을 수정해야 한다
  • thymeleaf를 수정해야 한다

<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>
  • th:if 문법을 이용해 우리가 error의 저장한 key를 containsKey로 조회하고,
  • th:text 문법을 이용해 value값을 꺼내 출력한다
  • 근데 생각보다 중복이 많기 때문에... 개선해보자
  • 또한 아직 타입 오류에 대한 검증로직은 없다... -> 400 error페이지로 바로 이동함...

검증 직접 처리 - V2

BindingResult

  • 스프링 컨테이너가 제공해주는 기능
  • 말 그대로 BindingResult를 컨트롤러에서 view로 이동해주는 기능이다
  • BindingResult또한 Model에 addAttribute해줘야 되나??
    • No!!! -> BindingResult객체도 컨트롤러에서 view로 바로 접근 가능
  • 대신 바인딩할 객체 바로 다음에 BindingResult를 적어줘야 된다
  • 뭔말이냐면...
  • public String addItemV1(@ModelAttribute Item item, BindingResult bindingResult,RedirectAttributes redirectAttributes) ` 이렇게 순서가 중요함!!!
  • 우리가 파라미터로 BindingResult객체를 불렀으니까, 객체에 넘길 바인딩 오류들을 추가해줘야 한다!
    • addError을 이용해 추가해준다
    • 이 안에는 오류객체를 넣어줘야하는데...
    • FieldError : 객체의 필드단위에서 오류가 가는 에러
    • FieldError(): (바인딩객체명 , 필드명 , 오류메시지)
    • ObjectError : 객체의(글로벌) 오류, 파라미터에 필드명은 이제 필요 없다
    • ObjectError : (바인딩객체명 , 오류메시지)
@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", "가격은 1000 ~ 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}";
    }
  1. 먼저 GlobalError부분이다
<div th:if="${#fields.hasGlobalErrors()}">
            <p class="field-error" th:each="err : ${#fields.globalErrors}" th:text="${err}">글로벌 오류 메시지</p>
        </div>
  • 검증 결과 오류(BindingResult)에 접근하려면 "@{#fields}" 문법을 사용해야 한다
  • Global Error에 오류가 여러개 있을 수 있으므로 th:each문법을 사용한다
  1. FieldError부분
<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>
  • #fields : 스프링이 제공하는 BindingResult객체에 접근하는 방법
  • th:errors : 해당 필드에 오류가 있는 경우 태그를 출력한다
    • th:errors를 이용해 BindingResult 객체에 접근한다
    • 우리가 errors에 파라미터로 객체명, 필드명, 메시지를 넣었었는데
    • thymeleaf에서 편리하게 객체명.필드명으로 메시지를 검색 후 출력까지 해준다.
    • 만약 필드오류가 나면
    • <div class="field-error" th:errors="상품 이름은 필수입니다">이렇게 thymeleaf가 동적으로 작동하고 출력까지 해준다.
  • th: errorclass: th:field에서 지정한 필드에 오류가 있으면, class정보를 추가한다
  • 위 예시에 field-error의 빨간색으로 바꾸는 클래스를, input box에도 적용을 해준다

정리!!!

  • BindingResult는 스프링이 제공하는 검증 오류를 보관하는 객체
  • BindingResult가 있으면, @ModelAttribute에 데이터 바인딩시 오류가 발생해도 컨트롤러가 호출된다!
  • BindingResult 는 검증할 대상 바로 다음에 와야한다. 순서가 중요하다. 예를 들어서 @ModelAttribute Item item , 바로 다음에 BindingResult 가 와야 한다.
  • BindingResult 는 Model에 자동으로 포함된다.

예) @ModelAttribute에 바인딩 시 타입 오류가 발생하면?

  • BindingResult 가 없으면 : 400 오류가 발생하면서 컨트롤러가 호출되지 않고, 오류 페이지로 이동한다.
  • BindingResult 가 있으면 : 오류 정보( FieldError )를 BindingResult 에 담아서 컨트롤러를 정상 호출한다.

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

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

지금까지 BindingResult , FieldError , ObjectError 를 사용해서 오류 메시지를 처리하는 방법을 알아보았다.
-> 그런데 오류가 발생하는 경우 고객이 입력한 내용이 모두 사라진다. 이 문제를 해결해보자.


그건 바로 FieldError의 생성을 오버로딩 된 다른 방법을 쓰는 것이다
지금까지FieldError를 생성할 때는 ObjectName,field,defaultMessage만 적어줬다면

  • objectName : 오류가 발생한 객체 이름
  • field : 오류 필드
  • rejectedValue : 사용자가 입력한 값(거절된 값)
  • bindingFailure : 타입 오류 같은 바인딩 실패인지, 검증 실패인지 구분 값
  • codes : 메시지 코드
  • arguments : 메시지에서 사용하는 인자
  • defaultMessage : 기본 오류 메시지
    이렇게 넣어준다.

바인딩 시점에 오류가 발생하면, 타입이 다른 이유 등등으로 우리가 생각한 Model객체에 입력한 값을 유지하기 힘들다.
즉, Model객체에 담는게 아니라 별도의 객체가 필요하다
그것이 바로 rejectedValue이다.
만약, fieldError가 발생하면, 스프링은 사용자가 입력한 값을 rejectedValue에 담아서 컨트롤러를 호출한다
이렇게 되면, 바인딩 실패시에도 오류 메시지를 컨트롤러에서 뷰로 이동해, 화면에 렌더링이 정상적으로 된다
thymeleafth:field또한 정상 상황에서는 Model객체의 값을 사용하지만, 검증 오류가 발생하는 상황에는 FieldError에서 보관한 값(rejectedValue)을 사용해서 값을 출력한다

즉 에러페이지를 나타내는게 아니라, 오류를 낸 이유와 오류를 낸 데이터 모두 화면에 렌더링할 수 있다는 소리다!!!


오류코드와 메시지 처리1

  • 아까 말했던 것처럼, FieldError를 만드는 방법이 2가지가 있다.
  • 전 코드에서는, codesarguments를 null로 두고 defaultMessage를 이용해 오류를 출력했었다면
  • 이번 코드에서는 codesarguments를 사용해 오류들을 메시지화 해서
  • 오류 메시지의 유지,보수를 더욱 쉽게 코드를 짜보도록 하겠다
  • 물론 SOLID의 OCP또한 만족하게 짜보겠다!
  • 메시지,국제화와 비슷한 논리다

message.properties를 이용하는 것이다!!! 여기에 메시지를 넣고 properties 파일만 수정해서 확장성을 높이는 방식이다!

그럴려면, application.properties파일의 설정을
spring.messages.basename=message,errors를 넣어주고 src/java/resources경로에 message.properties를 넣어준다


이렇게 메시지를 적어주고, 컨트롤러를 수정해 보자!!!

  • null을 안쓰고 FieldError의 파라미터를 봐보니...
  • String 배열, Object 배열을 받는 것을 알 수 있다.
  • 이는 메시지 코드는 하나가 아니라 배열로 여러 값을 전달할 수 있는데, 배열 순서대로 매칭해서 처음 매칭되는 메시지를 사용한다
  • 만약 매칭되는게 없으면 defaultMessage를 사용하고, null이면 오류가 난다
  • 배열이 나중에 코드에 엄청난 확장성을 제공한다!!!
  • 이렇게 우리가 errors.properties에 설정한 key값을 배열안에 넣어서 파라미터 codes안에 넣어주고
  • errors.properties에 설정한 key값의 value값을 code에 파라미터로 넣어준다
  • 이렇게 message를 이용해서, 코드를 설정파일에서 수정할 수 있게 바꾸었다
  • 하지만,,, 역시 개발자는 FieldError, ObjectError이런 거를 만들기 싫어한다 -> 코드를 더 자동화 한다

오류 코드와 메시지 처리 2

  • BindingResult는 검증할 객체의 target다음에 온다
  • 이렇게 순서를 적어주면 BindingResult는 검증해야 할 객체인 target을 파라미터에 넣어주지 않아도 알고 있다

rejectValue(),reject()

  • rejectValue(),reject()를 사용하면 FieldError,ObjectError를 직접 생성하지 않고, 메서드에서 자동으로 만들어준다.

  • rejectValue() : FieldError를 만들어준다

  • reject() : ObjectError를 만들어준다

 public String addItemV4(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {

        if (bindingResult.hasErrors()){
            log.info("errors={}",bindingResult);
            return "validation/v2/addForm";
        }

        log.info("objectName={}", bindingResult.getObjectName());
        log.info("target={}",bindingResult.getTarget());

        // 검증로직
//        ValidationUtils.rejectIfEmptyOrWhitespace(bindingResult,"itemName","required");

        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[]{100000,resultPrice},null);
            }

        }
        // 검증에 실패하면 다시 입력 폼으로 이동해야함
        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}";
    }
  • 우리가 FieldError, ObjectError을 생성하지 않고 rejectValue, reject 메서드를 이용해서, 대신 생성을 한다
  • 이때 아까 말했듯, 객체는 이미 알고 있고, 오류 필드명, 오류코드의 첫 단어, 파라미터 리스트를 넣어준다...
  • 아니 그래 오류 필드명과 파라미터 리스트는 그렇다 쳐도...
  • 어떻게 오류코드의 첫 단어만 넣어서 errors.properties에서 인식을 제대로 한다는 걸까???

  • 축약된 오류 코드
    FieldError() 를 직접 다룰 때는 오류 코드를 range.item.price 와 같이 모두 입력했다. 그런데 rejectValue() 를 사용하고 부터는 오류 코드를 range 로 간단하게 입력했다. 그래도 오류 메시지를 잘 찾아서 출
    력한다. 무언가 규칙이 있는 것 처럼 보인다. 이 부분을 이해하려면 MessageCodesResolver 를 이해해야 한다

오류 코드와 메시지 처리 3,4

오류 코드는 단순히 만들 수 있고, 자세히 만들 수 있다.

  • required.item.itemName : 상품 이름은 필수 입니다.
  • range.item.price : 상품의 가격 범위 오류 입니다.
  • required : 필수 값 입니다.
  • range : 범위 오류 입니다.
  • 오류 메시지에 required.item.itemName 와 같이 객체명과 필드명을 조합한 세밀한 메시지 코드가 있으면 이 메시지를 높은 우선순위로 사용하는 것이다.
    -> 스프링에서 이 기능을 MessageCodesResolver가 해준다

MessageCodesResolver

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

DefaultMessageCodesRosolver의 기본 메시지 생성 규칙

객체 오류
객체 오류의 경우 다음 순서로 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 를 통해서 생성된 순서대로 오류 코드를 보관한다.
  • FieldError rejectValue("itemName", "required")
    • 다음 4가지 오류 코드를 자동으로 생성
    1. required.item.itemName
    2. required.itemName
    3. required.java.lang.String
    4. required
  • ObjectError reject("totalPriceMin")
    • 다음 2가지 오류 코드를 자동으로 생성
    1. totalPriceMin.item
    2. totalPriceMin

오류 코드와 메시지 처리 5

  • 이제 그러면 우리는 구체적인 것에서 -> 덜 구체적인 것으로 가야한다
    • MessageCodesResolver 는 required.item.itemName 처럼 구체적인 것을 먼저 만들어주고, required 처럼 덜 구체적인 것을 가장 나중에 만든다.
    • 이렇게 하면 앞서 말한 것 처럼 메시지와 관련된 공통 전략을 편리하게 도입할 수 있다
    • 이렇게 하는 이유도 모든 오류 코드 메시지를 다 구현하면... 개발자 입장에서는 너무 힘들다
    • 유지,보수적인 측면에 의해 필요한 것들을 세세하게 구현하고, 범용성있는 큰 단어로 메시지화를 하자!!!
#required.item.itemName=상품 이름은 필수입니다.
#range.item.price=가격은 {0} ~ {1} 까지 허용합니다.
#max.item.quantity=수량은 최대 {0} 까지 허용합니다.
#totalPriceMin=가격 * 수량의 합은 {0}원 이상이어야 합니다. 현재 값 = {1}

#==ObjectError==

#Level1

totalPriceMin.item=상품의 가격 * 수량의 합은 {0}원 이상이어야 합니다. 현재 값 = {1}

#Level2 - 생략

totalPriceMin=전체 가격은 {0}원 이상이어야 합니다. 현재 값 = {1}
#==FieldError==
#Level1
required.item.itemName=상품 이름은 필수입니다.
range.item.price=가격은 {0} ~ {1} 까지 허용합니다.
max.item.quantity=수량은 최대 {0} 까지 허용합니다.
#Level2 - 생략

#Level3

required.java.lang.String = 필수 문자입니다.
required.java.lang.Integer = 필수 숫자입니다.
min.java.lang.String = {0} 이상의 문자를 입력해주세요.
min.java.lang.Integer = {0} 이상의 숫자를 입력해주세요.
range.java.lang.String = {0} ~ {1} 까지의 문자를 입력해주세요.
range.java.lang.Integer = {0} ~ {1} 까지의 숫자를 입력해주세요.
max.java.lang.String = {0} 까지의 문자를 허용합니다.
max.java.lang.Integer = {0} 까지의 숫자를 허용합니다.

#Level4

required = 필수 값 입니다.
min= {0} 이상이어야 합니다.
range= {0} ~ {1} 범위를 허용합니다.
max= {0} 까지 허용합니다.

## 추가
typeMismatch.java.lang.Integer=숫자를 입력해주세요.
typeMismatch=타입 오류입니다.

다음과 같이, 레벨에 맞게 오류 메시지를 범용성있고, 구체적이게 모두 구현한다

  • 이제 우리가 rejectValue, reject를 사용하면
  • 그 메서드 안에서 자동으로 필드값과, 오류코드를 조합해 MessageCodesResolver오류 메시지들을 리스트로 만들어준다!
  • 거기서 구체적인 순서에 따라 배열에 담겨지는데, 그 순서대로 우선순위를 부여한다
  • 리스트에서 순서대로 돌면서= 우선순위인 것 부터 properties에서 찾아서 -> bindingResult에 담아준다
  • bindingResult를 컨트롤러에서 뷰로 이동시키면서
  • 화면까지 렌더링 해준다

오류 코드와 메시지 처리 6

검증 오류 코드

  1. 개발자가 직접 설정한 오류 코드 -> rejectValue()로 직접 호출
  2. 스프링에서 기본설정된 오류
  • 예를 들어서, 우리가 수량을 입력받기 위해서 필드에 Integer을 주었다고 하자. 그런데 클라이언트가 잘못해서 문자열을 넣었다.
  • 이럴 때, 들어간 오류를 확인해보면
  • 로 우리가 error.properties에 메시지 코드를 설정해놓지 않았기 때문에, 스프링이 생성한 기본 메시지가 출력된다.

    그런데 우리가 배운걸 응용해보자!!!
    오류 코드를 잘 읽어보면, TypeMismatch를 이용해 오류를 MessageCodesResolver가 규칙을 가지고 4가지 메시지를 생성했네????
    범용성있게, errors.properties에 메시지를 넣어주면????

  • 스프링이 지정한 저렇게 긴 오류코드가 아니라, 우리가 원하는 검정 메시지를 생성한 것을 알 수 있다
  • 이렇게 메시지를 이용하니, OCP에 좋은 코드 즉 소스코드를 하나도 건들지 않고, 원하는 메시지를 단계별로 설정(확장에 매우 자유)할 수 있었다
  • 그런데 이런 메시지 코드 생성 전략은 나중에 배울 Bean Validation에서 더욱 진가를 발휘한다고 한다!!!

Validator 분리

  • 컨트롤러를 보면-> 검증 로직을 처리하는 부분, 정상 로직 즉 컨트롤러에서 2가지 기능을 하고 있다
  • SolidSRP를 적용시켜 보자
  • 검증 부분을 따른 클래스로 두고, 컨트롤러에서는 정상 로직 부분만 남겨두는 것이다.
  • 스프링이 제공하는 Validator인터페이스를 상속 받아서 사용할건데,
    1. supports() 해당 검증기를 지원하는지
    2. Validate 검증 대상 객체와 BindingResult
  • 이렇게 두부분으로 코드를 나눠준다

그런데 굳이 왜 Validator을 쓸까? 이는 스프링에서 추가적인 도움을 받을 수 있기 때문이다

@InitBinder
public void init(WebDataBinder dataBinder) {
 log.info("init binder {}", dataBinder);
 dataBinder.addValidators(itemValidator);
}
  • InitBinder는 컨트롤러가 호출될때 마다 항상 불려져서
  • WebDataBinder가 내부적으로 항상 새로 만들어져서
  • 이 때, itemValidator을 넣어준다
  • 즉 컨트롤러가 호출될때마다 새로운 검정기를 하나 넣어둔다
  • @Validated 이거를 넣으면 넣은 변수에 대해서 자동으로 검정을 한다
@Component
public class ItemValidator implements Validator {
    @Override
    public boolean supports(Class<?> clazz) {
        return Item.class.isAssignableFrom(clazz); // 파라미터로 들어오는 클래스가 Item에 지원이 되냐?
        // Item == clazz
        // Item == subItem
    }

    @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[]{100000,resultPrice},null);
            }
        }
    }
}
@PostMapping("/add")
    public String addItemV6(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {


        // 검증에 실패하면 다시 입력 폼으로 이동해야함
        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}";
    }
  • @Validated는 검증기를 실행하라는 애노테이션이다.
  • 이 애노테이션이 붙으면 앞서 WebDataBinder 에 등록한 검증기를 찾아서 실행한다.
    그런데 여러 검증기를 등록한다면 그 중에 어떤 검증기가 실행되어야 할지 구분이 필요하다. 이때 supports() 가 사용된다. 여기서는
    supports(Item.class) 호출되고, 결과가 true 이므로 ItemValidatorvalidate()가 호출된다.
profile
개발 공부,정리

0개의 댓글