공부 내용을 정리하고 앞으로의 학습에 이해를 돕기 위해 작성합니다.
Bean Validation - 시작
Bean Validation 기능을 스프링 없이 순수하게 사용하는 방법을 테스트 코드로 확인해 본다.
1. 의존관계 추가
build.gradle에 다음 의존성을 추가한다.
implementation 'org.springframework.boot:spring-boot-starter-validation'
이 의존성을 추가하면 다음과 같은 라이브러리가 포함된다.
- jakarta.validation-api : Bean Validation 인터페이스
- hibernate-validator : 실제 구현체
2. 도메인 객체에 검증 애노테이션 적용
@Data
public class Item {
private Long id;
@NotBlank
private String itemName;
@NotNull
@Range(min = 1000, max = 1000000)
private Integer price;
@NotNull
@Max(9999)
private Integer quantity;
public Item() {}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
3. 사용된 검증 애노테이션
- @NotBlank : 빈 문자열이나 공백만 있는 값 불가
- @NotNull : null 불가
- @Range(min, max) : 지정한 범위 내의 숫자만 허용 (Hibernate 전용)
- @Max(값) : 최대값 제한
참고
- javax.validation 으로 시작하는 애노테이션은 Bean Validation 표준 인터페이스
- org.hibernate.validator는 Hibernate 구현체에서 제공하는 애노테이션
실무에서는 Hibernate Validator를 많이 사용하므로 자유롭게 사용해도 된다.
4. Bean Validation 테스트 코드
public class BeanValidationTest {
@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>> violations = validator.validate(item);
for (ConstraintViolation<Item> violation : violations) {
System.out.println("violation=" + violation);
System.out.println("violation.message=" + violation.getMessage());
}
}
}
5. 검증 동작 방식
1. ValidatorFactory를 통해 검증기 생성
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
2. validate() 호출로 검증 실행
Set<ConstraintViolation<Item>> violations = validator.validate(item);
3. 결과는 ConstraintViolation 객체의 집합으로 반환되며, 각 항목에는 오류 메시지와 필드 정보 등이 포함됨
6. 실행 결과 예시
violation.message=공백일 수 없습니다
violation.message=9999 이하여야 합니다
violation.message=1000에서 1000000 사이여야 합니다
각 오류는 어떤 필드에서 어떤 문제인지 메시지로 나타난다.
Bean Validation - 스프링 적용
스프링 MVC에 Bean Validation을 통합하여, 검증 애노테이션 기반의 자동 검증 기능을 사용하는 방법을 다룬다.
컨트롤러 수정: ValidationItemControllerV3
@PostMapping("/add")
public String addItem(@Validated @ModelAttribute Item item, BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
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}";
}
- @Validated 애노테이션만 붙이면 스프링이 자동으로 Bean Validator를 실행한다.
- 기존의 addItemV1() ~ addItemV5() 제거
- 기존의 addItemV6() → addItem()으로 변경
불필요한 코드 제거
기존의 수동 Validator 적용 로직 제거
private final ItemValidator itemValidator;
@InitBinder
public void init(WebDataBinder dataBinder) {
dataBinder.addValidators(itemValidator);
}
이 코드를 그대로 둘 경우, ItemValidator와 BeanValidator가 중복 적용되므로 반드시 제거해야 한다.
스프링의 Bean Validation 적용 방식
- spring-boot-starter-validation 의존성을 등록하면, 스프링 부트는 자동으로 Bean Validator를 통합한다.
- LocalValidatorFactoryBean이 글로벌 Validator로 자동 등록됨
- 컨트롤러 메서드에서 @Validated, @Valid만 붙이면 자동으로 검증 수행
글로벌 Validator 수동 등록 주의
다음과 같이 직접 글로벌 Validator를 수동 등록하면, 스프링 부트의 자동 Bean Validator 등록이 비활성화된다.
@Override
public Validator getValidator() {
return new ItemValidator(); // 이 코드 제거할 것
}
이런 경우 @Validated, @Valid가 동작하지 않으므로 절대 등록하지 말 것
@Valid vs @Validated
- @Validated : 스프링 전용, groups 기능 지원
- @Valid : 자바 표준, 단순 검증
- 둘 다 사용 가능하며 기능상 큰 차이는 없음
검증 순서
- @ModelAttribute의 각 필드에 타입 변환 시도
- 타입 변환 성공 시 → Bean Validation 적용
- 타입 변환 실패 시 → FieldError 추가, Bean Validation 미적용
예시
- itemName에 "A" 입력 → 문자열 타입 변환 성공 → Bean Validation 적용
- price에 "A" 입력 → 숫자 타입 변환 실패 → typeMismatch 오류만 발생, Bean Validation 미적용
Bean Validation - 에러 코드
Bean Validation을 적용하면 검증 실패 시 오류 메시지가 자동으로 생성되고, 해당 메시지는 BindingResult에 등록된다. 이 메시지는 애노테이션 이름을 기반으로 오류 코드가 구성된다.
오류 코드 생성 방식 예시
MessageCodesResolver가 다음과 같이 다양한 형태의 메시지 코드를 자동 생성하여 순차적으로 메시지를 찾는다.
@NotBlank 적용 시
NotBlank.item.itemName
NotBlank.itemName
NotBlank.java.lang.String
NotBlank
@Range 적용 시
Range.item.price
Range.price
Range.java.lang.Integer
Range
메시지 커스터마이징
기본 메시지를 커스터마이징 하려면 errors.properties 또는 messages.properties 파일에 메시지를 등록한다.
예시: errors.properties
# Bean Validation 커스텀 메시지
NotBlank={0} 공백X
Range={0}, {2} ~ {1} 허용
Max={0}, 최대 {1}
- {0} : 필드명
- {1}, {2} 등은 애노테이션마다 의미가 다름 (예: @Range(min, max) → {1}=max, {2}=min)
메시지 적용 우선순위
1. messageSource에 등록된 메시지 코드 순서대로 탐색
2. 애노테이션에 명시한 message 속성 사용
@NotBlank(message = "공백은 입력할 수 없습니다.")
private String itemName;
3. 라이브러리 기본 메시지 사용
- 예: 공백일 수 없습니다.
정리
- 오류 코드는 애노테이션 이름 기반으로 자동 생성된다.
- errors.properties에 메시지를 등록하면 원하는 메시지로 덮어쓸 수 있다.
- 메시지 탐색은 코드 우선 → 애노테이션 직접 지정 → 기본 메시지 순으로 이루어진다.
- 필드명, 최대값, 최소값 등을 메시지 포맷에 활용할 수 있다.
Bean Validation - 오브젝트 오류
오브젝트 오류(ObjectError)란?
- 특정 필드에 국한되지 않고, 객체 전체에 대한 제약 조건 위반을 나타낼 때 사용
- 예: 가격 × 수량 >= 10,000처럼 필드 간의 조합을 검증해야 할 때
방법 1: @ScriptAssert 사용 (권장하지 않음)
@ScriptAssert(lang = "javascript", script = "_this.price * _this.quantity >= 10000")
public class Item {
// ...
}
메시지 코드
- ScriptAssert.item
- ScriptAssert
단점
- 복잡하고 제약이 많음
- 유지보수 어려움
- 필드 간의 유연한 조건 처리에 적합하지 않음
방법 2: 자바 코드에서 직접 ObjectError 추가 (권장)
@ScriptAssert 대신 컨트롤러에서 직접 글로벌 오류를 추가한다.
@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.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
}
}
if (bindingResult.hasErrors()) {
return "validation/v3/addForm";
}
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v3/items/{itemId}";
}
메시지 등록 예 (errors.properties)
totalPriceMin=가격 * 수량의 합은 {0}원 이상이어야 합니다. 현재 값 = {1}
정리
- 오브젝트 오류는 필드 단위로 처리할 수 없는 복합 조건에 사용
- @ScriptAssert는 사용성이 떨어지고 유지보수 어려움
- 실무에서는 BindingResult.reject()를 통해 글로벌 오류를 코드로 직접 추가하는 방식이 일반적
- 메시지 코드 등록을 통해 사용자 친화적인 오류 메시지 제공 가능
Bean Validation - 수정에 적용
1. 컨트롤러 코드 수정
ValidationItemControllerV3의 edit() 메서드에 @Validated를 추가하고, 글로벌 오류 처리도 적용한다.
@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId,
@Validated @ModelAttribute Item item,
BindingResult bindingResult) {
// 가격 * 수량 >= 10000 검증
if (item.getPrice() != null && item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
}
}
if (bindingResult.hasErrors()) {
return "validation/v3/editForm";
}
itemRepository.update(itemId, item);
return "redirect:/validation/v3/items/{itemId}";
}
- @Validated를 추가하여 필드 검증 활성화
- 글로벌 검증 로직도 등록과 동일하게 적용
2. 템플릿 수정: editForm.html
변경 사항 요약
- 글로벌 오류 메시지 영역 추가
- 각 필드에 th:errorclass="field-error" 속성 적용
- .field-error 스타일 정의
- 오류 메시지를 th:errors, th:each 등을 통해 출력
<div th:if="${#fields.hasGlobalErrors()}">
<p class="field-error" th:each="err : ${#fields.globalErrors()}" th:text="${err}">
글로벌 오류 메시지
</p>
</div>
각 필드 예시 (상품명)
<input type="text" id="itemName" th:field="*{itemName}" th:errorclass="field-error" class="form-control">
<div class="field-error" th:errors="*{itemName}">상품명 오류</div>
3. 메시지 등록 예 (errors.properties)
totalPriceMin=가격 * 수량의 합은 {0}원 이상이어야 합니다. 현재 값 = {1}
'Spring MVC' 카테고리의 다른 글
[MVC] 검증1 - Validation(2) (0) | 2025.07.08 |
---|---|
[MVC] 검증1 - Validation(1) (0) | 2025.07.03 |
[MVC] 타임리프 - 메시지, 국제화 (0) | 2025.06.29 |
[MVC] 타임리프 - 스프링 통합과 폼(2) (2) | 2025.06.24 |
[MVC] 타임리프 - 스프링 통합과 폼(1) (1) | 2025.06.21 |