오늘은 Bean Validation에 대해서 정리해보려고 한다!
Bean Validation 이란?
이전에 정리했던 BindingResult 내용을 살펴보면 검증 부분에 대해서 직접 코드를 작성하였는데, 매번 코드를 작성하는 것도 번거롭고 특정 필드에 대한 검증 로직은 대부분 비슷한 부분에 대해서 검증을 진행한다(ex. 빈 값, 특정 값..)
[Spring] Spring Validation-BindingResult(with.thymeleaf)
Bean Validation을 사용하기 전에 BindingResult로 검증 로직을 작성하는 것에 대한 내용을 기록하고자 한다. 데이터를 검증하고 문제가 있는 경우 사용자에게 오류 메시지를 보여줌과 동시에 사용자가
yelin1217.tistory.com
이러한 검증 로직을 모든 프로젝트에 적용할 수 있게 공통화하고, 표준화한 것이 바로 Bean Validation이다.
Bean Validation은 특정한 구현체가 아니라 Bean Validation 2.0(JSR-380)이라는 기술 표준이라고 한다.
즉, 검증 애노테이션과 여러 인터페이스의 모음인 것이다.
일반적으로 사용하는 구현체는 하이버네이트(JPA 아님) Validator이다.
하이버네이트 Validator 관련 링크는 아래를 참고하면 된다.
Bean Validation 설정 및 사용
build.gralde에 아래와 같이 의존성을 추가해주자
implementation 'org.springframework.boot:spring-boot-starter-validation'
상품 등록을 할 때 검증해야 하는 부분을 아래와 같이 기준을 세워 놓았다.
검증해야 하는 내용
- 상품 이름은 빈 값이 들어올 수 없다
- 가격은 최소 1000원, 최대 1000000원 범위 안에서 입력이 가능하다.
- 수량은 최대 9999개까지 입력이 가능하다.
Item 클래스에 아래와 같이 Bean Validation 애노테이션을 적용하였다.
@Data
public class Item {
private Long id;
@NotBlank
private String itemName;
@NotNull
@Range(min = 1000, max = 1000000)
private Integer price;
@NotNull
@Max(value = 9999)
private Integer quantity;
public Item() {
}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
그리고 컨트롤러의 상품 등록 API를 아래와 같이 작성하였다.
검증 객체 앞에 @Validated 애노테이션을 붙이고, 검증 객체 바로 뒤에 BindingResult를 넣어야 한다는 것에 주의하자!
@Slf4j
@Controller
@RequestMapping("/validation/v3/items")
@RequiredArgsConstructor
public class ValidationItemControllerV3 {
private final ItemRepository itemRepository;
@PostMapping("/add")
public String addItem(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
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/v3/addForm";
}
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v3/items/{itemId}";
}
}
그리고 실행을 해보면 정상 동작하는 것을 확인할 수 있다. (웹 부분 코드는 Validation 포스팅을 참조하자!)
Item 클래스에 애노테이션을 붙인 것 뿐인데 코드도 깔끔하고 편리하게 검증 로직이 돌아간다.
검증 코드도 없는데 어떻게 애노테이션만으로 검증이 되는걸까?
spring-boot-starter-validation 라이브러리를 넣으면 스프링 부트가 자동으로 Bean Validator를 인지하고 스프링에 통합하고,
자동으로 글로벌 Validator로 등록을 한다.(LocalValidatorFactoryBean을 글로벌 Validator로 등록)
이 Validator는 @NotBlank, @NotNull과 같은 애노테이션을 보고 검증을 수행한다.
글로벌 Validator가 적용되어 있기 때문에 @Valid나 @Validated만 적용하면 된다.
검증 오류가 발생하면 FieldError, ObjectError를 생성해서 BindingResult에 담아준다.
주의) 직접 글로벌 Validator를 등록하면 스프링 부트는 Bean Validator를 글로벌 Validator로 등록하지 않는다.
(WebMvcConfigurer를 상속받아 getValidator를 구현하는 부분을 제거하자)
검증 순서
@ModelAttribute에 담겨져 온 데이터 각각의 필드에 타입 변환을 시도하여 변환이 성공하면 Validator를 적용하고,
실패하면 typeMismatch로 FieldError가 추가되고, Validator가 적용되지 않는다.
Bean Validation에서 제공하는 오류 메시지를 자세하게 변경하기
Bean Validation을 적용하고 bindingResult에 등록된 검증 오류를 살펴보면 오류 코드가 애노테이션 이름으로 등록이 된다.
이전에 BindingResult 부분을 배울 때 봤던 typeMismatch와 유사하다.
@NotBlank로 생기는 오류 코드를 살펴보자
NotBlank라는 오류 코드를 기반으로 MessageCodeResolver를 통해 아래와 같이 메시지 코드가 순서대로 생서된다.
- NotBlank.item.itemName
- NotBlank.itemName
- NotBlank.java.lang.String
- NotBlank
실제로 bindingResult를 출력해보면 아래와 같이 나오는 것을 확인할 수 있다.
Field error in object 'item' on field 'quantity': rejected value [null]; codes [NotNull.item.quantity,NotNull.quantity,NotNull.java.lang.Integer,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [item.quantity,quantity]; arguments []; default message [quantity]]; default message [널이어서는 안됩니다]
Field error in object 'item' on field 'price': rejected value [null]; codes [NotNull.item.price,NotNull.price,NotNull.java.lang.Integer,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [item.price,price]; arguments []; default message [price]]; default message [널이어서는 안됩니다]
Field error in object 'item' on field 'itemName': rejected value []; codes [NotBlank.item.itemName,NotBlank.itemName,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [item.itemName,itemName]; arguments []; default message [itemName]]; default message [공백일 수 없습니다]
위와 같이 생성되는 것을 알았으니 메시지를 등록해보자!
errors.properties에 아래와 같이 메시지를 추가하였다.
#Bean Validation 관련 오류 메시지 추가
NotBlank.item.itemName=상품 이름을 적어주세요!
NotBlank={0} 공백을 허용하지 않습니다.
Range={0}, {2} ~ {1} 사이에 입력이 가능합니다.
Max={0}, 최대 {1} 까지 입력이 가능합니다.
여기서 {0}은 필드명, {1}, {2} 는 각 애노테이션 마다 설정했던 값들이 들어온다.
화면에서 실행을 해보면 아래와 같이 잘 나오는 것을 확인할 수 있다.

Bean Validation이 메시지를 찾는 순서는 아래와 같다.
- 생성된 메시지 코드 순서대로 messageSource에서 메시지
- 애노테이션의 message 속성으로 추가된 메시지 값 { ex. @NotBlank(message = "공백은 입력할 수 없습니다.") }
- 라이브러리가 제공하는 기본 메시지 값
ObjectError는 어떻게 다루는 것이 좋을까?
이전까지는 특정 필드(FieldError)에 대한 오류 메시지를 다뤄보았고 ObejctError에 대해서는 어떻게 다루는 것이 좋을까?
Item 객체에 @ScriptAssert(lang = "javascript", script = "_this.price * _this.quantity >= 10000")
이렇게 애노테이션을 추가하여 사용할 수 있다.
@Data
@ScriptAssert(lang = "javascript", script = "_this.price * _this.quantity >= 10000", message = "총 금액이 10,000원이 넘도록 입력해 주세요.")
public class Item {
:
}
위와 같이 설정한 뒤 실행을 해보면 총 금액에 대한 검증이 정상적으로 동작하는 것을 확인할 수 있다.
다만 해당 방법은 추천하지 않는다.
이렇게 억지로 사용하는 것보다는 자바 코드로 직접 작성해서 검증하는 것이 제약에 대한 문제에 대해서 마주하지 않을 것이다.
'TIL > Spring' 카테고리의 다른 글
[Spring] 서블릿 필터와 스프링 인터셉터(Servlet Filter, Spring Interceptor) (0) | 2025.03.28 |
---|---|
[Spring] Session으로 로그인 기능 구현하기(HttpSession, TrackingModes, Session Timeout) (0) | 2025.03.27 |
[Spring] Spring Validation-BindingResult(with.thymeleaf) (0) | 2025.03.16 |
[Spring] 빈 스코프 란? (0) | 2025.03.08 |
[Spring] 빈 생명주기 콜백 이란? - 스프링 초기화, 소멸 메서드 사용하기 (0) | 2025.03.04 |