on
Bean Validation
스프링 MVC 2편 - 백엔드 웹 개발 활용 기술을 공부하고 정리하는 포스트입니다.
Bean Validation
Bean Validation 이란?
- 특정한 구현체가 아닌 Bean Validation 2.0(JSR-380)이라는 기술 표준
- 검증 애노테이션과 여러 인터페이스의 모음
- 마치 표준 기술인 JPA가 있고 그 구현체로 Hibernate가 있는 것과 같은 논리
Bean Validation
을 구현한 기술 중 일반적으로 사용하는 구현체 → Hibernate Validation- 이름이 Hibernate일 뿐이지 ORM과는 무관
- 검증 기능을 매번 코드로 작성하는 것은 번거로운 일, 특히 특정 필드에 대한 검증 로직은 대부분 같은 내용이 반복됨
→ 모든 프로젝트에 적용할 수 있게 공통화하고, 표준화 한 것이Bean Validation
📌 검증 애노테이션
@NotBlank
: 빈값 + 공백만 있는 경우를 허용하지 않는다.@NotNull
: null을 허용하지 않는다.@Range(min, max)
: 범위 안의 값이어야 한다.@Max(9999)
: 최대 9999까지만 허용
Bean Validation - 시작
- build.gradle에 의존관계 추가
implementation 'org.springframework.boot:spring-boot-starter-validation'
Bean Validation을 직접 사용하는 방법
@Test
void beanValidation() {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Item item = new Item();
item.setItemName(" ");
item.setPrice(0);
item.setQuantity(10000);
Set<ConstraintViolation<Item>> validate = validator.validate(item);
for (ConstraintViolation<Item> violation : validate) {
System.out.println("violation = " + violation);
System.out.println("violation.getMessage() = " + violation.getMessage());
}
}
- 임의로 검증기를 생성하고, 검증 대상을 넣어 결과를 받는 테스트코드 → Bean Validation을 직접 사용하는 방법
→ 스프링에 이미 통합되어 있음. 따라서, 이렇게 할 필요 없음
스프링 MVC는 어떻게 Bean Validator를 사용하는가?
spring-boot-starter-validation
라이브러리를 넣으면 자동으로 Bean Validator를 인지하고 스프링에 통합
스프링 부트는 자동으로 글로벌 Validator로 등록한다.
LocalValidatorFactoryBean
을 글로벌 Validator로 등록 →@NotNull
같은 애노테이션을 보고 검증을 수행- 글로벌 Validator가 적용되어 있기 때문에
@Valid
,@Validated
만 적용하면 됨 - 검증 오류 발생 시
FieldError
,ObjectError
를 생성해서BindingResult
에 담아줌
- 글로벌 Validator가 적용되어 있기 때문에
- 다음과 같이 직접 글로벌 Validator를 등록하면 스프링 부트는 Bean Validator를 글로벌 Validator로 등록하지 않음 → 애노테이션 기반의 빈 검증기가 동작하지 않으므로 다음 부분은 제거
@SpringBootApplication
public class ItemServiceApplication implements WebMvcConfigurer {
// 글로벌 검증기 추가
@Override
public Validator getValidator() {
return new ItemValidator();
}
//...
}
📌 참고
@Validated
는 스프링 전용 검증 애노테이션이고, @Valid
는 자바 표준 검증 애노테이션이다.
둘 중 아무거나 사용해도 동일하게 작동하지만, @Validated
는 내부에 groups라는 기능을 포함하고 있다.
필드 검증
- Controller 계층에 다음과 같은 코드를 추가
@PostMapping("/add")
public String addItem(@Validated @ModelAttribute Item item,
BindingResult bindingResult,
RedirectAttributes redirectAttributes,
Model model) {
//...
}
- 검증 순서
@ModelAttribute
각각의 필드에 타입 변환 시도- 성공 시 다음으로
- 실패 시
typeMissmatch
로FieldError
추가
Validator
적용
- 바인딩에 성공한 필드만 Bean Validation을 적용
- @ModelAttribute → 각각의 필드에 타입 변환 시도 → 변환에 성공한 필드만 Bean Validation 적용
Bean Validation - 에러 코드
-
Bean Validation을 사용하면
message.properties
를 설정하거나 작성하지 않아도 기본 메시지가 출력됨
→ 해당 라이브러리에서 지정한 기본 메시지
→ 임의로 변경하려면? -
Bean Validation 적용 후
bindingResult
에 등록된 검증 오류를 보면 오류 코드가 애노테이션 이름으로 등록됨
→typeMissmatch
와 유사 NotBlank
라는 오류 코드를 기반으로MessageCodesResolver
를 통해 다양한 메시지 코드가 순서대로 생성@NotBlank
- NotBlank.item.itemName
- NotBlank.itemName
- NotBlank.java.lang.String
- NotBlank
@Range
- Range.item.price
- Range.price
- Range.java.lang.Integer
- Range
- 메시지 코드에 직접 등록하면 적용됨
NotBlank={0} 공백은 유효하지 않습니다.
Range={0}, {2}~{1}만 허용됩니다.
Max={0}, 최대{1}까지만 허용됩니다.
-
{0}은 필드명, {1}, {2}, … 은 각 애노테이션마다 다름 → 일반적으로 arguments
-
다음과 같이 직접 애노테이션에 message 속성으로 지정하는 것도 가능
@NotBlank(message = "공백은 입력할 수 없습니다.")
private String itemName;
Bean Validation 메시지 찾는 순서
- 생성된 메시지 코드 순서대로
messageSource
에서 메시지 찾기 - 애노테이션의
message
속성 사용 →@NotBlank(message = "공백! {0}")
- 라이브러리가 제공하는 기본 값 사용 → “공백일 수 없습니다”.
Bean Validation - 오브젝트 오류
특정 필드가 아닌 해당 오브젝트 관련 오류(ObjectError)
@Data
@ScriptAssert(lang = "javascript", script = "_this.price * _this.quantity >= 10000")
public class Item {
//...
}
@ScriptAssert()
를 사용ScriptAssert.item
→ScriptAssert
순으로 메시지 코드 생성- 실제로 사용해보면 제약이 많고 복잡함. 검증 기능이 객체의 범위를 넘어서는 경우에 대응이 어려움.
- 따라서, 오브젝트 오류(글로벌 오류)의 경우 관련 부분만 직접 자바 코드로 작성하는 것이 더 좋은 방법
- 코드 중복이 걱정되면 메소드를 분리하도록 하자
- 요약
- 간단한 필드 검증에는
Bean Validation
을 이용하여 검증 애노테이션을 활용하자 - 복잡한 객체 검증에는 제약이 많은 애노테이션보다 직접 코드로 구현하자
→ 코드의 재사용성이 높다면 모듈화를 진행하자
- 간단한 필드 검증에는
Bean Validation의 한계
- 예를 들어, 데이터를 등록(POST)할 때와 수정(Fetch, PUT)할 때는 요구사항이 다름 → 변경된 제약조건은 기존에 작성된 Entity에 적용이 불가능
- 등록 시에는 최대 갯수 제한이 있지만, 수정 시에는 수량을 무제한으로 변경 가능
- 등록 시에는
id
에 값이 없어도 되지만, 수정 시에는id
값이 필수
- 수정에 맞춰 id 필드에
@NotNull
검증 애노테이션을 붙이고 수량 필드에@Max
애노테이션을 지우면
→ 수정은 동작하지만, 등록 시에 오류가 발생
따라서, 등록과 수정에서 검증 조건의 충돌이 발생하고, 등록과 수정은 같은 BeanValidation을 적용할 수 없다. 이 문제를 어떻게 해결해야하는가?
Bean Validation - groups를 사용해 검증 분리
- 이러한 문제를 해결하기위해 Bean Validation은
groups
라는 기능을 제공- 수행할 기능을 그룹으로 나누어 적용
- 하지만 이 방법은 복잡도가 상승하기 때문에 실제로 잘 사용하지 않음
Form 전송 객체 분리를 이용한 검증 분리
groups
방식은 등록 시 폼에서 전달하는 데이터가 도메인 객체와 정확하게 일치하지 않음- 실무에서는 해당 정보와 관계없는 수 많은 부가 데이터도 함께 넘어옴
-
보통 해당 객체를 직접 전달받는 것이 아닌, 복잡한 폼의 데이터를 컨트롤러까지 전달할 별도의 객체를 만들어서 전달함
→ 예를 들어,XxxSaveForm
이라는 폼을 전달받는 전용 객체를 만들어@ModelAttribute
로 사용
→ 컨트롤러에서 폼 데이터를 전달 받고, 이후 컨트롤러에서 필요한 데이터를 사용해Xxx
를 생성 - 폼 데이터 전달에
Xxx
도메인 객체 사용:HTML Form → Xxx → Controller → Xxx → Repository
- 장점: Xxx 도메인 객체를 컨트롤러, 리포지토리까지 직접 전달해서 중간에 Xxx을 만드는 과정이 없어서 간단.
- 단점: 간단한 경우에만 적용 가능. 수정 시 검증이 중복될 수 있고,
groups
를 사용해야 함.
- 폼 데이터 전달을 위한 별도의 객체 사용:
HTML Form → XxxSaveForm → Controller → Xxx 생성 → Repository
- 장점: 전송하는 폼 데이터가 복잡해도 거기에 맞춘 별도의 폼 객체를 사용해서 데이터를 전달받을 수 있음.
보통 등록과 수정용으로 별도의 폼 객체를 만들기 때문에 검증이 중복되지 않음. - 단점: 폼 데이터를 기반으로 컨트롤러에서 Xxx 객체를 생성하는 변환 과정이 추가됨.
- 장점: 전송하는 폼 데이터가 복잡해도 거기에 맞춘 별도의 폼 객체를 사용해서 데이터를 전달받을 수 있음.
Bean Validation - HTTP 메시지 컨버터
-
Form 방식을 이용한 검증에서,
@Valid
,@Validated
는HttpMessageConverter(@RequestBody)
에도 적용 가능 - API의 경우 3가지 경우를 나누어 생각해야함
- 성공 요청 : 성공
- 실패 요청 : JSON 객체를 생성하는 것 자체가 실패함
- 검증 오류 요청 : JSON 객체로 생성하는 것은 성공했고, 검증에서 실패함
- 잘못된 값을 전달하여 실패하게 만들어보자.
→ 검증 실패 시HttpMessageConverter
에서 요청 JSON을 객체로 생성하는데 실패
→ 객체를 만들지 못하기 때문에 컨트롤러 자체가 호출되지 않고 그 전에 예외가 발생. 또한Validator
도 실행되지 않음.
@ModelAttribute vs @RequestBody
- HTTP 요청 파라미터를 처리하는
@ModelAttribute
는 각각의 필드 단위로 세밀하게 적용됨 -
특정 필드에 타입이 맞지 않는 오류가 발생해도 나머지 필드는 정상 처리할 수 있음
-
HttpMessageConverter
는@ModelAttribute
와 다르게 각각의 필드 단위로 적용되는 것이 아니라, 전체 객체 단위로 적용
→ 따라서 메시지 컨버터의 작동이 성공해서 Item 객체를 만들어야@Valid
,@Validated
가 적용됨. @ModelAttribute
- 필드 단위로 정교하게 바인딩이 적용.
- 특정 필드가 바인딩 되지 않아도 나머지 필드는 정상 바인딩 되고, Validator를 사용한 검증도 적용할 수 있음.
@RequestBody
HttpMessageConverter
단계에서 JSON 데이터를 객체로 변경하지 못하면 이후 단계 자체가 진행되지 않고 예외가 발생함.- 컨트롤러도 호출되지 않고, Validator도 적용할 수 없음.
📌 참고
HttpMessageConverter 단계에서 실패하면 예외가 발생
Comments
SPRING 의 다른 글
-
스프링 타입 컨터버 24 Jun 2022
-
API 예외 처리 17 Jun 2022
-
예외 처리와 오류 페이지 12 Jun 2022
-
로그인 처리 - 인터셉터 08 Jun 2022
-
로그인 처리 - 필터 06 Jun 2022
-
로그인 처리 - 쿠키, 세션 31 May 2022
-
Bean Validation 22 May 2022
-
검증 22 May 2022
-
메시지, 국제화 21 May 2022
-
타임리프 - 스프링 통합과 폼 19 May 2022
-
타임리프 - 기본 기능 10 May 2022
-
스프링 MVC 기본 기능 - 웹 페이지 만들기 02 May 2022
-
스프링 MVC 기본 기능 - HTTP 응답 30 Apr 2022
-
스프링 MVC 기본 기능 - HTTP 요청 24 Apr 2022
-
스프링 MVC 기본 기능 - 요청 매핑 19 Apr 2022
-
스프링 MVC 기본 기능 19 Apr 2022
-
스프링 MVC 구조 이해 14 Apr 2022
-
MVC 프레임워크 만들기 - V4, V5 12 Apr 2022
-
MVC 프레임워크 만들기 - V1, V2, V3 09 Apr 2022
-
서블릿, JSP, MVC 패턴 05 Apr 2022
-
서블릿 29 Mar 2022
-
웹 애플리케이션 이해 24 Mar 2022
-
스프링 웹 계층이란? 05 Nov 2021
-
스프링 시큐리티 공식문서 번역 27 Sep 2021
-
스프링 AOP 총정리 : 개념, 프록시 기반 AOP, @AOP 27 Apr 2021
-
SpEL (스프링 Expression Language) 25 Apr 2021
-
데이터 바인딩 추상화 : Converter와 Formatter 21 Apr 2021
-
데이터 바인딩 추상화 : PropertyEditor 12 Apr 2021
-
Validation 추상화 10 Apr 2021
-
Resource 추상화 08 Apr 2021
-
IoC 컨테이너 9부 07 Apr 2021
-
IoC 컨테이너 8부 06 Apr 2021
-
IoC 컨테이너 7부 02 Apr 2021
-
IoC 컨테이너 6부 29 Mar 2021
-
IoC 컨테이너 5부 27 Mar 2021
-
IoC 컨테이너 4부 23 Mar 2021
-
IoC 컨테이너 3부 20 Mar 2021
-
IoC 컨테이너 2부 18 Mar 2021
-
IoC 컨테이너 1부 12 Mar 2021
-
스프링 PSA 07 Jan 2021
-
스프링 @AOP 실습 07 Jan 2021
-
프록시 패턴 06 Jan 2021
-
스프링 AOP 04 Jan 2021
-
의존성 주입(Dependency Injection) 04 Jan 2021
-
스프링 빈(Bean) 02 Jan 2021
-
스프링 IoC 컨테이너 01 Jan 2021
-
스프링 IoC 01 Jan 2021