Spring

[SpringBoot] Validation 간단 정리 - 2 (Bean Validation)

꾸준함. 2021. 7. 25. 17:11

개요

기존에 공유한 Validtor의 경우 간단한 검증 기능조차도 코드로 작성했기 때문에 복잡하다고 느껴질 수 있습니다.

(https://jaimemin.tistory.com/1874)

이에 따라 스프링에서는 애노테이션 하나로 검증 로직을 매우 편리하게 적용할 수 있는 Bean Validation을  제공하며 오늘은 이에 대해 정리해보고자 합니다.

 

1. Bean Validation

  • 특정한 구현체가 아닌 Bean Validation 2.0(JSR-380)이라는 기술 표준
  • JPA가 표준 기술이고 그 구현체로 hibernate가 있는 것처럼 Bean Validation 또한 검증 애노테이션과 여러 인터페이스의 모음
  • 대표적인 구현체로 hibernate validator가 있으며 보다 자세한 내용은 http://hibernate.org/validator 참고
  • 적용하기 위해서는 gradle에 아래의 라이브러리를 추가해야 함
    • implementation 'org.springframework.boot:spring-boot-starter-validation'

 

2. Spring과 통합하지 않은 순수한 Bean Validation 예시

기존 Validation 간단 정리 - 1의 Member를 예시로 진행하겠습니다.

 

Member.java

 

 

위에서 사용한 검증 애노테이션은 아래와 같습니다.

  • @NotNull: null을 허용하지 않음
  • @NotBlank: 빈칸 혹은 공백만 있는 경우를 허용 안 함
  • @Pattern: 정규표현식 (Regex) 적용
  • @Range: 범위 안의 값이어야 함 (따라서, 1900 ~ 2021의 범위 내만 허용)

 

* 보다 많은 검증 애노테이션을 알고 싶으시면 https://docs.jboss.org/hibernate/validator/6.2/reference/en-US/html_single/#validator-defineconstraints-spec 참고

 

Member.java에 대해 Bean Validation 테스트하기 위한 테스트 코드

 

 

스프링과 통합하여 사용할 경우 실제 위와 같은 검증 코드를 작성하지 않지만 위 코드를 통해 Bean Validation은 ConstraintViolation 객체를 통해 검증 오류를 파악하고 검증 오류가 발생할 경우 객체, 필드, 메시지 정보와 같이 다양한 정보를 확인할 수 있다는 것을 파악할 수 있습니다.

 

3. 스프링 MVC에서 Bean Validator 적용

  • 앞서 추가한 spring-boot-starter-validation 라이브러리를 gradle에 추가하면 스프링은 자동으로 Bean Validator 인지
  • 스프링 부트는 자동으로 LocalValidatorFactoryBean을 글로벌 Validator로 등록하며 해당 Validator는 검증 애노테이션을 보고 검증을 수행
  • 글로벌 Validator가 적용되었기 때문에 @Validated 애노테이션을 추가해야 검증이 적용되며 검증 오류 발생 시 FieldError, ObjectError를 생성해 BindingResult에 추가
  • 기존 게시글(https://jaimemin.tistory.com/1874)에서 Validator를 글로벌하게 설정하는 방법을 공유했었는데 이렇게 설정할 경우 LocalValidatorFactoryBean과 충돌이 나기 때문에 Bean Validator를 사용하기 위해서는 글로벌로 설정한 커스텀 Validator를 제거해야 함
  • 검증 순서는 아래와 같음
    • @ModelAttribute 각각의 필드에 타입 변환 시도 (성공 시 다음으로 진행, 실패 시 typeMismatch로 FieldError가 추가되면서 검증이 진행되지 않음)
    • 검증 어노테이션을 기반으로 Validator 적용
  • BeanValidator는 타입 변환에 성공해서 바인딩에 성공한 필드여야 Bean Validation을 적용
  • 정리를 하자면, @ModelAttribute이 적용된 빈에 대해 각각의 필드 타입으로 변환을 시도한 후 변환에 성공한 필드에 대해서만 BeanValidation 적용

 

4. Validation 버전 4

  • 기존 게시글(https://jaimemin.tistory.com/1874)의 Validation 버전 3에서는 Validator를 분리해서 컨트롤러 코드를 단순화하는 데 성공했습니다.
  • 이제 버전 4에서는 Validator 마저 사용하지 않고 Bean Validation을 사용하는 방법으로 검증을 진행하겠습니다.

 

 

* DB에 해당 ID가 존재하여 중복되는지 여부는 Bean Validation으로 불가능하기 때문에 컨트롤러에서 작성했습니다.

* 물론, 서비스 내에서 예외 처리하는 방식으로 조금 더 깔끔하게 코드를 작성할 수 있습니다.

 

5. Bean Validation 에러 메시지

  • Bean Validation 또한 MessageCodeResolver를 통해 다양한 메시지 코드가 순서대로 생성됨
  • 예를 들자면, id 필드가 @NotBlank였으므로 아래의 순서대로 errors.properties에서 오류 메시지 코드를 탐색
    • NotBlank.member.id
    • NotBlank.id
    • NotBlank.java.lang.String
    • NotBlank

 

* 여기서 errors.properties는 application.yml에 등록한 메시지 기능 파일

* application.yml에 다음과 같이 추가해야 사용 가능 (spring.messages.basename=messages,errors)

 

6. @ScriptAssert 애노테이션

  • ObjectError의 경우 단순히 필드 값만으로 예외처리를 못하는 경우가 있음
  • 예를 들자면, Item이란 객체가 있고 Item 객체 내 가격을 나타내는 price 필드와 수량을 나타내는 quantity 필드가 있다고 가정했을 때, 가격 * 수량이 반드시 10,000원을 넘어야 하는 케이스가 있을 수 있음
  • 이런 경우 필드 값만으로 예외처리를 못하므로 @ScriptAssert 애노테이션을 사용하거나 컨트롤러 내에서 예외처리 코드를 작성해야 함
  • @ScriptAssert 애노테이션 같은 경우 아래와 같은 단점들이 있으므로 컨트롤러 내에서 예외처리를 하는 것을 추천하지만 일단 방법은 알아보자
    • Java 11에 종속적(https://jaimemin.tistory.com/1883)
    • 오타 발생 시 찾기 힘듦
    • 검증 기능이 해당 객체의 범위를 넘어서는 경우들이 종종 등장 (앞서 Member 객체의 id 같은 경우 MemberService를 통해 DB 내 동일한 ID가 있는지 여부를 파악해야 함)

 

Item.java

 

 

 * _this는 현재 객체 즉, Item 객체를 나타냅니다. 따라서, 충족해야 하는 조건을 _this.price * _this.quantity >= 10000과 같이 작성 가능

* 여기서 오타가 발생해도 컴파일 단계에서 파악 불가능 (오타에 취약하므로 추천하는 방법은 아님)

 

7. Bean Validation의 한계

  • 간단한 프로젝트의 경우 회원 가입과 회원 정보 수정 모두 같은 Member 객체를 사용할 수도 있지만, 실무 프로젝트의 경우 회원등록과 회원 정보 수정 시 검증 요구사항이 다를 수 있음
  • 예를 들자면, 기존 Member.java 객체에 db 시퀀스 ID 필드를 추가하고 해당 필드가 primary key라고 가정
  • 회원 등록할 때는 AUTO_INCREMENT 특성 때문에 db 시퀀스 ID 필드를 전달받을 필요가 없지만 회원 정보 수정 시 db 시퀀스 필드 ID를 전달받아야 어떤 회원 정보를 수정할 수 있는지 파악 가능
  • 또한, 회원정보 수정 시에는 id를 수정할 일은 없음 (id는 한 번 등록 시 고정)
  • 정리를 하자면, 등록 시에는 db 시퀀스 ID 필드가 NULL이지만 회원 정보 수정 시에는 db 시퀀스 ID 필드가 @NotNull이여야 하고 id 필드는 등록 시에만 필요
  • 이럴 경우에는 db 시퀀스 ID 필드에 @NotNull 애노테이션 추가 시 회원 등록에서 오류가 발생하고 애노테이션 제거 시 회원 정보 수정에서 오류가 발생할 가능성이 있음
  • 위 문제를 해결하기 위해 두 가지 해결방법이 있음
    • BeanValidation의 groups 기능을 사용
    • Member 객체를 그대로 사용하지 않고, MemberRegisterForm과 MemberEditForm과 같이 폼 전송을 위한 별도의 모델 객체를 사용

 

8. BeanValidation groups 기능

  • groups 기능을 사용하기 위해서는 등록, 수정 그룹을 나타내기 위한 인터페이스 선언 필요
  • 아래 코드를 보면 이해하기 쉬움 

 

MemberRegisterValidationCheck interface

 

public interface MemberRegisterValidationCheck {

}

 

MemberEditValidationCheck interface

 

public interface MemberEditValidationCheck {

}

 

Member.java

 

 

MemberController 버전 5

 

 

* 각각의 인터페이스가 그룹을 나타내고 @Validatied 애노테이션에 어떤 그룹을 적용할지 명시해주면 해당 그룹에 해당하는 검증만 진행

* 기존의 문제를 해결했지만 코드가 상당히 많이 추가되었고 상대적으로 복잡한 것을 확인할 수 있음

* 또한, 지금은 간단한 프로젝트라 Member 객체 내 필드가 별로 없지만 실무에서는 필드가 훨씬 많고 등록 시 필요한 필드와 회원 정보 수정 시 필요한 필드가 상당히 많이 다를 수 있음

* 따라서, 이어서 설명할 두 번째 방법인 폼 전송을 위한 별도 객체를 사용하는 것을 추천

 

9. Member 객체를 폼 전송 객체로 분리

  • 앞서 설명한 것처럼 실무에서는 회원 등록과 회원 정보 수정 시 필요한 필드들이 많이 다를 수 있고 회원 등록 시 넘겨받는 정보가 회원과 관련된 데이터뿐만 아니라 수많은 부가 데이터를 전달받을 수 있음 (groups 기능을 추천하지 않는 이유)
  • 정리를 하자면, 회원 등록 form과 회원 정보 수정 form 형태가 많이 다를 수 있으므로 Member 객체를 회원 등록용 폼 객체와 회원 정보 수정용 폼 객체로 분리하는 것을 추천
  • 폼 객체들로 분리할 경우 등록, 수정이 완전히 분리되기 때문에 검증이 중복되지 않고 groups를 적용할 일이 없다는 것이 장점
  • 폼 객체들을 분리한 이유 전달받은 폼 객체를 기반으로 컨트롤러 내에서 Member 객체를 생성하는 것이 일반적인 방법

 

Member.java (검증 코드 제거)

 

 

MemberRegisterForm.java

 

 

MemberEditForm.java

 

 

MemberController 버전 6

 

 

10. Bean Validation - HttpMessageConverter

  • Bean Validation은 앞선 예시처럼 @ModelAttribute 애노테이션이 붙은 객체에 대한 검증에도 사용되지만 @RequestBody 즉, HttpMesssageConverter에도 적용 가능
  • @ModelAttribute는 HTTP 요청 파라미터 (쿼리 스트링, Post Form)을 다룰 때 사용
  • @RequestBody는 HTTP Body의 데이터를 JSON 객체로 변환할 때 사용
  • 아래의 코드 참고

 

ValidationExampleApiController.java

 

 

 

 

위 API의 경우 아래와 같이  3 가지 결과가 나올 수 있음

  • 성공 요청 -> 성공
  • 실패 요청(TypeMismatch) -> JSON을 객체로 생성하는 것 자체가 실패
  • 검증 오류 요청: JSON을 객체로 변환하는 것은 성공했지만 검증에 실패

 

@ModelAttribute vs @RequestBody

  • 가장 큰 차이점은 위에 빨간색으로 표시한 부분
  • HTTP 요청 파라미터를 처리하는 @ModelAttribute 애노테이션 같은 경우 각각의 필드 단위로 세밀하게 적용되기 땜누에 특정 필드에 타입이 맞지 않는 오류가 발생하더라도 나머지 필드는 정상 처리 가능
    • Integer 필드에 String이 넘어오더라도 나머지 필드에 대해서는 검증 수행
  • 반면, HttpMessageConverter 같은 경우 @ModelAttribute와 달리 각각의 필드에 적용하는 것이 아니라 전체 객체 단위로 적용
  • 따라서, MessageConverter 작동이 성공해서 JSON이 Message 객체로 변환이 돼야 @Validated가 적용
  • 정리를 하자면, @ModelAttribute는 필드 단위로 정교하게 바인딩이 적용되므로 특정 필드가 바인딩이 되지 않더라도 나머지 필드는 정상 바인딩이 되고 Validator를 적용한 검증도 가능
  • @RequestBody는 HttpMessageConverter 단계에서 JSON 데이터를 객체로 변환하지 못할 경우 이후 단계 자체가 진행되지 않고 예외가 발생
    • Integer 필드에 String이 넘어오면 JSON에서 객체로 변환이 안되어 Bean Validation이 적용 안될 뿐만 아니라 Exception 발생 (컨트롤러 호출도 안되고 Validator도 적용 불가)

 

출처

인프런 스프링 MVC 2편 (김영한 강사님)

반응형