개요
기존에 공유한 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편 (김영한 강사님)
'Spring' 카테고리의 다른 글
[SpringBoot] 쿠키, 세션을 이용한 로그인 처리 (2) | 2021.07.31 |
---|---|
[SpringBoot] 쿠키, 세션 개념 정리 (0) | 2021.07.29 |
[SpringBoot] Validation 간단 정리 - 1 (BindingResult, Validator) (6) | 2021.07.16 |
[Spring Boot] 메시지, 국제화 간단 정리 (0) | 2021.07.10 |
HttpMessageConverter 간단 정리 (0) | 2021.06.11 |