[ 김영한 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 #4 ] 검증1 - Validation (4)

김수호·2023년 9월 15일
0
post-thumbnail

지난 포스팅에 이어, 이번 포스팅에서는 15) ~ 17) 까지의 내용을 정리한다.

👉 목차는 다음과 같다.

1) 검증 요구사항
2) 프로젝트 설정 V1
3) 검증 직접 처리 - 소개
4) 검증 직접 처리 - 개발
5) 프로젝트 준비 V2
6) BindingResult1
7) BindingResult2
8) FieldError, ObjectError
9) 오류 코드와 메시지 처리1
10) 오류 코드와 메시지 처리2
11) 오류 코드와 메시지 처리3
12) 오류 코드와 메시지 처리4
13) 오류 코드와 메시지 처리5
14) 오류 코드와 메시지 처리6
15) Validator 분리1
16) Validator 분리2
17) 정리


15) Validator 분리1

목표: 복잡한 검증 로직을 별도로 분리하자.

컨트롤러에서 검증 로직이 차지하는 부분은 매우 크다. 이런 경우 별도의 클래스로 역할을 분리하는 것이 좋다. 그리고 이렇게 분리한 검증 로직을 재사용 할 수도 있다.

 

👉 코드로 확인해보자.

  • ItemValidator 생성: src > main > java > hello > itemservice > web > validation > ItemValidator 클래스를 생성하자. ( 기존 검증 로직들을 처리할 Validator 클래스를 생성 )
    • 스프링은 검증을 체계적으로 제공하기 위해 Validator 인터페이스를 제공한다. (이후 설명)
    • validate 메서드 내부에 기존 ValidationItemControllerV2 클래스에 addItemV4 메서드에 있는 검증 로직을 복사해서 붙여넣자. (참고로 validate 메서드는 첫 번째 파라미터로 target을 받고, 두번째 파라미터에 Errors를 받는다. target은 Object로 받기 때문에 캐스팅 작업을 해주어야 한다. 그리고 Errors는 BindingResult의 부모 클래스이다. 따라서, 붙여넣은 검증 로직에서 bindingResult를 errors로 변경하자.)
    • 참고로 해당 Validator를 효율적으로 가져다 사용할 수 있도록 스프링 빈으로 등록하자. ( @Component )
  • ValidationItemControllerV2 - addItemV5 메서드 생성: 기존 addItemV4 메서드를 복사해서 addItemV5를 만들자. (참고로 기존 addItemV4 메서드의 @PostMapping은 주석처리 하자.)
  • ValidationItemControllerV2 - addItemV5: 기존 검증 로직을 삭제하자.
    • 노란 음영 처리된 부분을 삭제하자.
  • ValidationItemControllerV2 - addItemV5: ItemValidator 적용
    • ItemValidator 를 주입 받아서 직접 호출했다.
  • 실행해보자.
    • 실행해보면 기존과 완전히 동일하게 동작하는 것을 확인할 수 있다. 검증과 관련된 부분이 깔끔하게 분리되었다.

 

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

  • Validator
    • supports(Class<?> clazz) : 해당 검증기를 지원하는 여부 확인 (뒤에서 설명)
    • validate(Object target, Errors errors) : 검증 대상 객체와 BindingResult
      • 실제 검증하는 로직을 적용한다.

 

🤔 그런데 생각해보면, ItemValidator 에서 굳이 Validator 를 구현하고, 스프링 빈으로 등록해서 사용할 필요가 있을까 ? 생성한 ItemValidator 를 사용하는 곳이 많지 않다면, 그냥 다음과 같이 사용해도 될 것 같다.

  • ItemValidator 수정: @Component 삭제, Validator 인터페이스 구현 코드 삭제
  • ValidationItemControllerV2 > addItemV5 수정: ItemValidator 사용 코드 수정
    • 이렇게 해도 정상적으로 동작한다.

 

그러면 굳이 왜 스프링이 제공하는 Validator 인터페이스를 사용할까 ?
뭔가 이유가 있을 것 같다. 다음 내용을 통해 알아보자.


16) Validator 분리2

스프링이 Validator 인터페이스를 별도로 제공하는 이유는 체계적으로 검증 기능을 도입하기 위해서다. 그런데 앞에서는 검증기를 직접 불러서 사용했고, 이렇게 사용해도 된다. 그런데 Validator 인터페이스를 사용해서 검증기를 만들면 스프링의 추가적인 도움을 받을 수 있다.

 

WebDataBinder를 통해서 사용하기

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

 

👉 코드를 통해 확인해보자.

  • ValidationItemControllerV2: 다음 코드를 추가하자.
    • 위와 같이 적용하게되면, 이제부터는 해당 컨트롤러에 모든 요청이 들어올때마다 @InitBinder가 적용된 method가 호출된다. 위 코드에서는 WebDataBinder에 검증기( itemValidator )를 항상 넣어 두고 있다. 따라서 /validation/v2/items 가 호출될 때도, /validation/v2/items/{itemId} 가 호출될 때도 항상 검증기를 적용할 수 있다.
      (참고로 WebDataBinder 는 요청이 올 때 마다 항상 새로 만들어진다.)
    • 이렇게 WebDataBinder 에 검증기를 추가하면 해당 컨트롤러에서는 검증기를 자동으로 적용할 수 있다.
    • (참고) @InitBinder 는 해당 컨트롤러에만 영향을 준다. 글로벌 설정은 별도로 해야한다. (마지막에 설명)
  • ValidationItemControllerV2 - addItemV6: 기존 addItemV5 메서드를 복사해서 addItemV6를 만들고 아래와 같이 수정하자.
    (참고. 기존 addItemV5 메서드의 @PostMapping은 주석처리 하자.)
    • validator 를 직접 호출하는 부분이 사라지고, 대신에 검증 대상 앞에 @Validated 가 붙었다. (그러면 해당 애노테이션으로 인해, Item item에 대해서 자동으로 검증기가 수행이 된다. 그리고 검증된 결과가 BindingResult에 담기게 된다. (직접 검증기를 수행하는 것(addItem5 메서드)과 동일한 동작을 하며, 따라서 검증기를 직접 호출하는 코드가 필요가 없어진다.))
  • 실행해보자.
    • 기존과 동일하게 잘 동작하는 것을 확인할 수 있다.

 

동작 방식

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

 

글로벌 설정 - 모든 컨트롤러에 다 적용

  • 참고)
    • 이렇게 글로벌 설정을 추가할 수 있다. 기존 컨트롤러의 @InitBinder 를 제거해도 글로벌 설정으로 정상 동작하는 것을 확인할 수 있다. 이어지는 다음 강의를 위해서 글로벌 설정은 꼭 제거해두자.
  • 주의)
    • 글로벌 설정을 하면 다음에 설명할 BeanValidator 가 자동 등록되지 않는다. 글로벌 설정 부분은 주석처리 해두자. 참고로 글로벌 설정을 직접 사용하는 경우는 드물다.

 

✔️ 참고

  • 검증시 @Validated, @Valid 둘다 사용가능하다.
  • javax.validation.@Valid 를 사용하려면 build.gradle 의존관계 추가가 필요하다.
    • implementation 'org.springframework.boot:spring-boot-starter-validation'
  • @Validated 는 스프링 전용 검증 애노테이션이고, @Valid 는 자바 표준 검증 애노테이션이다. 둘 다 스프링이 지원하기 때문에 둘 중 아무거나 사용해도 된다. 자세한 내용은 다음 Bean Validation에서 설명하겠다.

17) 정리

  • 검증 요구사항
    • 검증에 대한 기획자의 요구사항이 추가되었다. (타입에 대한 검증, 필드에 대한 검증, 특정 필드의 범위를 넘어서는 검증)
    • 검증에는 클라이언트 검증과 서버 검증이 있다. 클라이언트 검증은 사용성이 좋다. 그렇지만 클라이언트 검증과 서버 검증은 둘 다 이뤄져야 한다. 왜냐하면 클라이언트 검증은 신뢰할 수 없기 때문이다. (파라미터를 조작해서 서버에 요청할 수 있다.)
  • 프로젝트 설정 V1
    • validation-start 의 폴더 이름을 validation 으로 변경했다.
  • 검증 직접 처리 - 소개
    • 상품 저장에 성공한 경우와, 상품 저장 검증에 실패한 경우에 대해서 알아보았다. (상품 저장 검증에 실패했다면, 사용자가 입력한 값을 유지한 상태로 다시 상품 등록 폼을 보여주고, 어떤 값을 잘못입력했는지 친절하게 알려주어야 한다.)
  • 검증 직접 처리 - 개발
    • 검증 처리를 직접 개발해보았다. 그러다보니 Map에 error 정보를 담고, 검증 처리를 진행하면서 코드의 복잡도가 증가했다. 그리고 그걸 화면에서 노출 처리하기 위한 코드도 반복된 부분이 많았고, 복잡했다.
  • 프로젝트 준비 V2
    • 스프링이 제공하는 검증 오류 처리 방법을 사용하기 위해 V2를 만들었다.
  • BindingResult1
    • 스프링이 제공하는 검증 오류 처리 방법인 BindingResult를 사용해보았다. (FieldError, ObjectError를 통해서 BindingResult에 오류를 담아서 동작하도록 적용해보았다.)
  • BindingResult2
    • BindingResult에 대해서 좀 더 자세히 알아보았다.
      • @ModelAttribute에 바인딩 시 타입 오류가 발생했을 때, BindingResult가 있을때와 없을때 어떻게 달라지는지 알아보았다.
      • BindingResult에 검증 오류를 적용하는 3가지 방법에 대해 알아보았다.
      • BindingResult와 Errors의 관계에 대해서 알아보았다. (BindingResult는 인터페이스이고, Errors 인터페이스를 상속받고 있다. 둘 중 아무거나 사용해도 상관없지만, 관례상 BindingResult를 많이 사용한다.)
  • FieldError, ObjectError
    • FieldError와 ObjectError에 대해서 자세히 알아보았다. 각각의 생성자 파라미터 목록에 대해서 알아보았다.
      • FieldErrror
        • objectName: 오류가 발생한 객체 이름
        • field: 오류 필드
        • rejectedValue: 사용자가 입력한 값(거절된 값)
        • bindingFailure: 타입 오류 같은 바인딩 실패인지, 검증 실패인지 구분 값
        • codes: 메시지 코드
        • arguments: 메시지에서 사용하는 인자
        • defaultMessage: 기본 오류 메시지
      • ObjectError
        • ObjectError도 유사하게 두 가지 생성자를 제공한다. 코드를 참고하자.
  • 오류 코드와 메시지 처리1
    • 오류메시지를 defaultMessage 파라미터를 통해서 개별적으로 적용하는게 아닌 codes, arguments 파라미터를 통해 일관성있게 관리할 수 있는 방법에 대해서 알아보았다.
  • 오류 코드와 메시지 처리2
    • BindingResult 가 제공하는 rejectValue(), reject()에 대해 알아보았고, 이를 사용하면서 코드가 많이 간소화되었다.
  • 오류 코드와 메시지 처리3
    • 오류 코드를 어떤식으로 설계하면 좋을지에 대해서 알아보았다.
  • 오류 코드와 메시지 처리4
    • MessageCodesResolver에 대해서 알아보았다.
    • DefaultMessageCodesResolver의 기본 메시지 생성 규칙에 대해서 알아보았다.
  • 오류 코드와 메시지 처리5
    • 오류 코드 관리 전략에 대해서 알아보았고 실제 도입해보았다. (Level1 ~ Level4)
      • 핵심은 구체적인 것에서! 덜 구체적인 것으로!
    • ValidationUtils에 대해서 알아보았다.
  • 오류 코드와 메시지 처리6
    • 스프링이 직접 만든 오류 메시지를 어떻게 처리하는지 알아보았다. (typeMismatch)
  • Validator 분리1
    • 컨트롤러에 작성된 복잡한 검증 로직을 별도로 분리해보았다. (처음에는 ItemValidator를 스프링 빈으로 등록해서 직접 호출해서 사용했다.)
  • Validator 분리2
    • 두 번째는 WebDataBinder에 검증기를 등록해두고, @Validated 애노테이션을 사용해서 검증기를 적용하는 방법에 대해 알아보았다.

이 과정들을 점진적으로 거치면서 검증 코드가 깔끔하게 작성되었다.
다음 섹션에서는 우리가 Validator에서 직접 작성했던 validate() 내부 검증 로직들을, 애노테이션 기반으로 작성할 수 있도록 하는 Bean Validation에 대해서 알아보자.


강의를 듣고 정리한 글입니다. 코드와 그림 등의 출처는 김영한 강사님께 있습니다.

profile
현실에서 한 발자국

0개의 댓글