[Spring] Validation(밸리데이션) - 검증
검증 Validation
검증은 웹 애플리케이션의 핵심 요소 중 하나이다, 잘못된 데이터 입력은 시스템의 안정성을 저해하거나 예상치 못한 오류를 일으킬 수 있다.
검증 로직을 구현하지 않으면, 사용자가 잘못된 값을 입력했을 때 시스템이 오류 화면으로 넘어가버리는 문제가 발생할 수 있다. 이러한 경험은 사용자에게 부정적인 인식을 심어주므로, 웹 서비스에서는 오류 발생 시 오류의 원인을 알려주고 입력한 데이터를 유지해야 한다.
클라이언트 검증 vs 서버 검증
- 클라이어트 검증 : 사용자에게 실시간 피드백을 제공하지만, 보안 측면ㅇ에서는 취약하다, 클라이언트의 코드는 조작될 수 있기 때문이다.
- 서버 검증 : 보안적으로 더 안정적이지만, 사용자 경험을 향상시키기 위해서는 즉각적인 피드백이 부족하다. 그러므로, 클라이언트와 서버 검증을 적절히 혼합하는 것이 바람직 하다.
API 검증
- API를 사용할 경우, 응답 내에서 검증 오류 정보를 명확히 표현해야 한다.
검증 직접 처리
상품 저장 성공
- 사용자가 상품 등록 폼에서 모든 필드에 대한 정보를 올바르게 입력하면, 해당 데이터는 서버의 검증 로직을 통과한다.
- 검증에 성공하면, 서버는 입력 받은 데이터를 기반으로 상품을 저장한다.
- 상품 정보가 성공적으로 저장된 후에는, 사용자는 해당 상품의 상세 정보 화면으로 Redirect 된다.
상품 저장 검증 실패
- 사용자가 상품을 등록 폼에서 필요한 정보를 올바르게 입력하지 않거나, 입력한 정보가 검증조건을 만족시키지 못할 경우, 서버의 검증 로직을 실패한다.
- 예를 들어, 상품명이 입력되지 않았거나, 가격이나 수량이 정해진 범위를 벗어난 경우가 이에 해당한다.
- 검증에 실패할 경우, 사용자에게 다시 상품 등록 폼이 표시되며, 어떤 부분에서 오류가 발생했는지를 명확하게 알려주어야 한다. 이를 통해 사용자는 오류를 쉽게 파악하고 수정할 수 있다.
검증 직접 처리 - 개발
상품 등록 검증
@Slf4j
@Controller
@RequestMapping("/validation/v2/items")
@RequiredArgsConstructor
public class ValidationItemControllerV2 {
private final ItemRepository itemRepository;
@GetMapping
public String items(Model model) {
List<Item> items = itemRepository.findAll();
model.addAttribute("items", items);
return "validation/v2/items";
}
@GetMapping("/{itemId}")
public String item(@PathVariable long itemId, Model model) {
Item item = itemRepository.findById(itemId);
model.addAttribute("item", item);
return "validation/v2/item";
}
@GetMapping("/add")
public String addForm(Model model) {
model.addAttribute("item", new Item());
return "validation/v2/addForm";
}
@PostMapping("/add")
public String addItem(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
//검증 로직
if (!StringUtils.hasText(item.getItemName())) {
bindingResult.addError(new FieldError("item", "itemName", "상품 이름은 필수 입니다."));
}
if ((item.getPrice() == null) || (item.getPrice() < 1000) || item.getPrice() > 10000000) {
bindingResult.addError(new FieldError("item", "price", "가격은 1,000 1,000,000 까지 허용합니다."));
}
if (item.getQuantity() == null || item.getQuantity() >= 9999) {
bindingResult.addError(new FieldError("item", "quantity", "수량은 최대 9,999 까지 허용합니다."));
}
//특정 필드가 아닌 복합 룰 검증
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 (!errors.isEmpty()) {
model.addAttribute("errors", errors);
return "validation/v2/addForm";
}
//성공 로직
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v2/items/{itemId}";
}
@GetMapping("/{itemId}/edit")
public String editForm(@PathVariable Long itemId, Model model) {
Item item = itemRepository.findById(itemId);
model.addAttribute("item", item);
return "validation/v2/editForm";
}
@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId, @ModelAttribute Item item) {
itemRepository.update(itemId, item);
return "redirect:/validation/v2/items/{itemId}";
}
}
검증 오류 보관
- 검증 오류가 발생하면 'errors' HashMap에 해당 오류 내용을 담아 둔다.
- 오류가 발생한 필드명을 Key로 사용하여 어떤 오류인지 구분할 수 있다.
- 특정 필드를 넘어서는 오류를 처리하기 위해 'globalError' Key 를 사용한다.
검증 실패
- 검증에서 오류 메시지가 발생하면, 오류 메시지를 출력하기 위해 model 에 errors 를 담고 뷰 템플릿으로 데이터를 전달한다.
뷰 템플릿 (Thymeleaf)
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<link th:href="@{/css/bootstrap.min.css}"
href="../css/bootstrap.min.css" rel="stylesheet">
<style>
.container {
max-width: 560px;
}
.field-error {
border-color: #dc3545;
color: #dc3545;
}
</style>
</head>
<body>
<div class="container">
<div class="py-5 text-center">
<h2 th:text="#{page.addItem}">상품 등록</h2>
</div>
<form action="item.html" th:action th:object="${item}" method="post">
<!-- 검증 글로벌 오류 메시지-->
<div th:if="${errors?.containsKey('globalError')}">
<p class="field-error" th:text="${errors['globalError']}">전체 오류 메시지</p>
</div>
<div>
<label for="itemName" th:text="#{label.item.itemName}">상품명</label>
<input type="text" id="itemName" th:field="*{itemName}"
th:class="${errors?.containsKey('itemName')} ? 'form-control field-error' : 'form-control'"
class="form-control" placeholder="이름을 입력하세요">
<div class="field-error" th:if="${errors?.containsKey('itemName')}" th:text="${errors['itemName']}">
상품이름 오류
</div>
</div>
<div>
<label for="price" th:text="#{label.item.price}">가격</label>
<input type="text" id="price" th:field="*{price}"
th:class="${errors?.containsKey('price')} ? 'form-control field-error' : 'form-control'"
class="form-control" placeholder="가격을 입력하세요">
<div class="field-error" th:if="${errors?.containsKey('price')}" th:text="${errors['price']}">
상품 가격 오류
</div>
</div>
<div>
<label for="quantity" th:text="#{label.item.quantity}">수량</label>
<input type="text" id="quantity" th:field="*{quantity}"
th:class="${errors?.containsKey('quantity')} ? 'form-control field-error' : 'form-control'"
class="form-control" placeholder="수량을 입력하세요">
<div class="field-error" th:if="${errors?.containsKey('quantity')}" th:text="${errors['quantity']}">
상품 수량 오류
</div>
</div>
<hr class="my-4">
<div class="row">
<div class="col">
<button class="w-100 btn btn-primary btn-lg" type="submit" th:text="#{button.save}">상품 등록</button>
</div>
<div class="col">
<button class="w-100 btn btn-secondary btn-lg"
onclick="location.href='items.html'"
th:onclick="|location.href='@{/validation/v1/items}'|"
type="button" th:text="#{button.cancel}">취소</button>
</div>
</div>
</form>
</div> <!-- /container -->
</body>
</html>
오류 메시지 errors 에 내용이 있을 때만 출력한다.타임리프의 'th:if' 를 사용하여 조건에 만족할 때만 해당 HTML 태그를 출력한다.
문제점 및 개선 필요사항
- 뷰 템플릿 중복 : 현재 뷰 템플릿에서 중복 처리가 많이 발생한다.
- 타입 오류 처리 : Item 의 price, quantity 와 같은 숫자 필드는 문자열로 입력되면 타입 오류가 발생한다. 이러한 오류는 스프링MVC에서 처리되기 전에 발생하므로 컨트롤러까지 오류가 전달되지 않는다.
- 입력 값 유지 : 타입 오류 발생시 사용자가 입력한 값도 유지되어야 한다. 하지만 현재 구조에서는 문자열로 입력된 값을 숫자 타입 필드에 바인딩할수 없으므로, 사용자가 입력한 문자열 정보가 사라진다.
BindingResult
스프링에서는 BindingResult 를 사용하여 데이터 바인딩 또는 검증 과정에서 발생한 오류를 관리한다.
검증 로직
@PostMapping("/add")
public String addItemV1(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
// 여기에 item의 속성에 대한 검증 로직이 들어갑니다.
...
if (bindingResult.hasErrors()) {
return "validation/v2/addForm";
}
...
return "redirect:/validation/v2/items/{itemId}";
}
BindingResult 파라미터는 @ModelAttribute 객체 바로 다음에 위치해야 한다.
FieldError - 필드 오류
필드에 오류가 있으면 FieldError 객체 생성, bindingResult 에 담는다.
- ObjectName : @ModelAttribute 객체 이름
- field : 오류가 발생한 필드 이름
- defaultMessage : 오류 기본 메시지
ObjectError - 글로벌 오류
특정 필드를 넘어서는 오류가 있으면 Object 객체 생성, bindingResult 에 담는다.
- ObjectName : @ModelAttribute 객체 이름
- defaultMessage : 오류 기본 메시지
타임리프 스프링 검증 오류 통합 기능
타임리프는 스프링의 BindingResult 를 활용해서 편리하게 검증 오류를 표현하는 기능을 제공한다.
<!-- 글로벌 오류 처리 -->
<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" placeholder="이름을 입력하세요">
<div class="field-error" th:errors="*{itemName}"> 상품이름 오류 </div>
- #fields : #fields 로 BindingResult 가 제공하는 검증 오류에 접근할 수 있다.
- th:errors : 해당 필드에 오류가 있는 경우 태그를 출력한다.
- th:errorclass : th:field 에서 지정한 필드에 오류가 있으면 class 정보를 추가한다.
참고 자료 - 타임리프 공식 문서 - 검증 및 오류 메시지
Tutorial: Thymeleaf + Spring
Preface This tutorial explains how Thymeleaf can be integrated with the Spring Framework, especially (but not only) Spring MVC. Note that Thymeleaf has integrations for both versions 3.x and 4.x of the Spring Framework, provided by two separate libraries c
www.thymeleaf.org
BindingResult 정의
스프링에서 제공하는 객체로, 검증 오류를 보관한다.
오류가 발생하면 이 객체에 정보를 저장하여 활용할 수 있다.
BindingResult 중요성
BindingResult 가 있을 때 '@ModelAttribute' 에 데이터 바인딩 중 오류 발생
- BindingResult 없음 : 400 오류 발생, 컨트롤러 호출되지 않음, 오류 페이지로 이동
- BindingResult 있음 : 오류 정보('FieldError')를 BindingResult에 저장하고 컨트롤러가 정상적으로 호출됨
BindingResult 오류 정보 저장 방법
- 스프링이 자동으로 오류를 생성 : @ModelAttribute 의 객체에 바인딩 중 오류(예 : 타입 오류)가 발생하면 스프링이 'FieldError'를 생성하여 BindingResut에 저장.
- 개발자가 직접 오류 정보 추가.
- Vaildator 사용 : 고급 검증 방법
타입 오류 확인
- 예를 들어, 숫자가 필요한 필드에 문자가 입력되면 타입 오류가 발생. 이때 BindingResult 의 값을 확인하여 상황을 파악할 수 있다.
BindingResult 주요 사항
- BindingResult 는 검증 대상 객체 바로 다음에 위치해야 한다. 자동으로 Model에 포함된다.
BindingResult 와 Errors
- BindingResult 는 Errors 인터페이스를 상속한 인터페이스 이다.
- 실제 BindingResult 인터페이스의 구현체는 BeanPropertyBindingResult 이다.
- Errors 는 오류 저장 및 조회 단순 기능만 제공, 반면 BindingResult 는 추가 기능을 포함하여 제공 (예 : 'addError()' )
- 대부분의 경우, 관례적으로 BindingResult가 사용된다.
Field Error, Object Error
검증 로직 2
@PostMapping("/add")
public String addItemV2(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
//검증 로직
if (!StringUtils.hasText(item.getItemName())) {
bindingResult.addError(new FieldError("item", "itemName", item.getItemName(), false, null, null, "상품 이름은 필수 입니다."));
}
//...
//특정 필드가 아닌 복합 룰 검증
if (item.getPrice() != null && item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
bindingResult.addError(new ObjectError("item", null, null, "가격 * 수량의 합은 10,000원 이상 주문이 가능합니다. 현재 주문 가격 = " + resultPrice ));
}
}
//...
}
FieldError 생성자
FieldError는 두가지 생성자를 제공한다.
- 기본 오류 메시지만 전달하는 생성자
- 상세 정보를 전달하는 생성자
파라미터 목록
- objectName : 오류가 발생한 객체 이름
- field : 오류 필드
- rejectedValue : 사용자가 입력한 값 (거절된 값)
- bindingFailure : 타입 오류 같은 바인딩 실패인지, 검증 실패인지 구분 값
- codes : 메시지 코드
- arguments : 메시지에서 사용하는 인자
- defaultMessage : 기본 오류 메시지
ObjectError 생성자
ObjectError도 유사하게 두 가지 생성자를 제공한다.
사용자 입력 값 유지
- 오류 발생 시 사용자가 입력한 데이터를 웹페이지에 유지하는 것은 사용자 경험(UX)에 중요하다. '
- FieldError' 클래스는 'rejectedValue' 라는 필드를 제공하여 오류 발생 시의 입력 값을 저장할 수 있다.
- 또한 Integer 타입이므로 문자를 보관할 수 이는 방법이 없다.
- FieldError 는 오류 발생 시 사용자 입력 값을 저장하는 기능도 제공한다.
타임리프의 사용자 입력 값 유지
- 타임리프의 th:field 는 정상 작동에도 모델 객체의 값을 사용하지만, 오류가 발생 시 'FieldError' 에서 보관한 값을 사용하여 웹 페이지에 출력할 수 있다.
스프링의 바인딩 오류 처리
- 스프링에서 데이터 타입 오류 등 바인딩 오류 발생 시, 스프링은 자동으로 'FieldError' 객체를 생성하고, 오류 발생 시의 입력 값을 'rejectedValue' 에 저장한다. 이후, 이 오류 정보는 'BindingResult' 전달하게 된다.
- 따라서, 타입 오류 같은 바인딩 실패 시 사용자 입력 오류 메시지를 정황하게 웹 페이지에 출력할 수 있다.
오류 코드와 메시지 처리
FieldError, ObjectError의 생성자는 codes, arguments 를 제공한다. 이것은 오류 발생시 오류 코드로 메시지를 찾기 위해 사용된다.
메시지 소스 설정
오류 메시지 관리를 위해 별도의 properties 파일 ('errors.properties')을 생성할 수 있다. 스프링 부트 환경에서 이 파일을 인식하기 위해선 'application.properties' 에 설정을 추가 해야한다.
spring.messages.basename=messages,errors
errors.properties 파일
'errors.properties' 파일에 오류 메시지를 다음과 같이 정의
required.item.itemName=상품 이름은 필수입니다.
range.item.price=가격은 {0} ~ {1} 까지 허용합니다.
max.item.quantity=수량은 최대 {0} 까지 허용합니다.
totalPriceMin=가격 * 수량의 합은 {0}원 이상이어야 합니다. 현재 값 = {1}
errors_en.properties 와 같이 오류 메시지도 국제화 처리를 할 수 있다.
Controller
컨트롤러에서 FieldError를 생성할 때 해당 메시지 코드와 인자를 사용하여 오류를 추가한다.
if ((item.getPrice() == null) || (item.getPrice() < 1000) || item.getPrice() > 10000000) {
bindingResult.addError(new FieldError("item", "price", item.getPrice(), false, new String[]{"range.item.price"}, new Object[]{1000, 1000000}, null));
}
이때 'range.item.price' 는 메시지 코드(codes)이며, '{0} ~ {1}' 은 'Object[]{1000, 1000000}'(arguments)로 대체된다.
스프링은 내부에서 'MessageSource' 를 사용하여 제공된 메시지 코드를 바탕으로 'errors.properties' 파일에서 적절한 메시지를 찾아 사용자에게 표시한다.
codes 을 String[] 로 선언한 이유는, 첫번째 인자 값이 없을 경우, 두번째 인자 값으로 대체할 수 있다즉, 첫 오류 메시지가 없을경우, 경우의 수 두번째 메시지로 대체할 수 있다.
오류 코드와 메시지 처리 - 자동화, 간소화
BindingResult 의 특징
컨트롤러에서 'BindingResult' 는 검증해야 할 객체인 target 바로 다음에 위치한다.
BindingResult 는 이미 본인이 검증해야 할 객체인 target을 알고 있다.
log.info("objectName={}", bindingResult.getObjectName());
log.info("target={}", bindingResult.getTarget());
objectName=item
target=Item(id=null, itemName=스프링, price=10000, quantity=10)
rejectValue()와 reject()
이 메서드들을 사용하면 'FieldError' , 'ObjectError' 를 직접 생성하지 않고도 깔끔하게 검증 오류를 처리할 수 있다. 코드를 간소화하기 위해 'rejectValue() 와 reject() 를 사용한다.
if ((item.getPrice() == null) || (item.getPrice() < 1000) || item.getPrice() > 10000000) {
bindingResult.rejectValue("price", "range", new Object[]{1000, 10000}, null);
}
rejectValue()
- 이 메서드는 오류 필드에 대한 오류 코드, 메시지 인자, 기본 메시지를 설정할 수 있다.
- 이 오류 코드는 메시지에 등록된 코드가 아니라 'messageResolver' 를 위한 오류 코드이다.
- 오류 필드의 이름과 함께 단순한 오류 코드만 range 를 제공해도 해당 오류 메시지를 찾아 출력한다.
MessageCodesResolver
- rejectValue() 를 사용할 때 축약된 오류 코드로도 오류 메시지를 찾아 출력하는 것은 MessageCodesResolver 덕분이다.
- MessageCodesResolver 는 주어진 오류 코드를 기반으로 여러 개의 메시지 코드를 생성해 준다.
reject()
rejectVaule() 와 유사하지만, 특정 필드가 아니라 객체 전체에 대한 오류를 설정하는데 사용된다.
오류 코드와 메시지 처리 - 세분화, 범용성
오류 코드의 세분화 및 범용성
오류 코드와 메시지는 상황에 따라 세분화되거나 범용적으로 작성될 수 있다.
- 세분화된 메시지
#required.item.itemName=상품 이름은 필수입니다.
- 범용 메시지
required: 필수 값 입니다.
장단점
범용 메시지의 장점 : 다양한 상황에서 사용할 수 있으며, 코드의 재사용성이 높다.
범용 메시지의 단점 : 세부적인 내용을 포함하는 메시지를 제공하기 어렵다.
세분화된 메시지의 장점 : 상황에 맞는 구체적이고 세밀한 오류 메시지를 제공할 수 있다.세분화된 메시지의 단점 : 범용성이 떨어져 다양한 상황에서 사용하기 어렵다.
범용성과 세분화의 결합
상황에 따라 필요한 오류 메시지의 정밀도를 선택할 수 있게끔 메시지 코드에 단계를 설정하는 것이 이상적이다.
#Level1 : 객체와 필드명을 조합한 세분화된 메시지
required.item.itemName: 상품 이름은 필수 입니다.
#Level2 : 범용 메시지
required: 필수 값 입니다.
이렇게 객체명과 필드명을 조합한 메시지가 있는지 우선 확인하고, 없으면 범용적인 메시지를 선택하도록 범용성 있게 개발을 하면, 메시지의 추가만으로 매우 편리하게 오류 메시지를 관리할 수 있다.
스프링의 MessageCodesResolver
- 스프링에서는 'MessageCodesResolver' 를 통해 위와 같은 기능을 지원한다.
- 세부적인 메시지 코드가 있는 경우 그것을 우선적으로 사용하고, 없으면 범용적인 메시지 코드를 사용하는 로직을 자동으로 처리할 수 있다.
오류 코드와 메시지처리 - MessageResolver
MessageResolver 개요
- MessageResolver 는 검증 오류 코드로 메시지 코드를 생성하는 인터페이스이다.
- 스프링에서 기본적으로 제공하는 구현체는 'DefaultMessageCodeResolver 다.
- 이 구현체는 주로 'ObjectError' 와 FieldError' 와 함께 사용된다.
테스트 코드
public class MessageCodesResolverTest {
MessageCodesResolver codesResolver = new DefaultMessageCodesResolver();
@Test
void messageCodesResolverObject() {
String[] messagesCodes = codesResolver.resolveMessageCodes("required", "item");
assertThat(messagesCodes).containsExactly("required.item", "required");
}
@Test
void messageCodesResolverField() {
String[] messageCodes = codesResolver.resolveMessageCodes("required", "item", "itemName", String.class);
assertThat(messageCodes).containsExactly(
"required.item.itemName",
"required.itemName",
"required.java.lang.String",
"required"
);
}
}
DefaultMessageCodesResolver의 메시지 생성 규칙
객체 오류 : messageCodesResolverObject()
객체 오류의 경우 다음 순서로 2가지 생성
1. code + "." + object name
2. code
예) 오류 코드 : required, object name : item
1. required.item
2. requried
필드 오류 : messageCodesResolverField()
1. code + "." + object name + "." + field
2. code + "." field
3. code + "." + field type
4. code
예) 오류 코드 : typeMismatch, object name "user", field "age", field type : int
1. typeMismatch.user.age
2. typeMismatch.age
3. typeMismatch.int
4. typeMismatch
동작 방식
- rejectValue(), reject() 는 내부에서 MessageCodesResolver 를 사용한다. 여기에서 메시지 코드들을 생성한다.
- FieldError, ObjectError 의 생성자에서는 오류 코드를 여러 개 가질 수 있으며, MessageCodesResolver 를 통해 생성된 순서대로 오류 코드를 보관한다.
예 ) FieldError 의 rejectValue("itemName", "required") 는 다음 4가지 오류 코드를 생성
- required.item.itemName
- required.itemName
- required.java.lang.String
- required
예 ) ObjectError 의 reject("totalPriceMin") 는 다음 2가지 오류 코드를 생성
- totalPriceMin.item
- totalPriceMin
오류 메시지 출력
- 타임 리프 화면 렌더링 시 'th:errors' 가 실행된다.
- 오류가 있을 경우 생성된 오류 메시지 코드를 순서대로 돌며 메시지를 찾는다. 만약 메시지가 없으면 defaultMessage 출력된다.
오류 코드와 메시지 처리 - 오류 코드 관리 전략
구체적인 것에서 덜 구체적인 것으로 메시지 코드 생성
MessageCodesResolver 는 오류 메시지코드를 생성할 때, 구체적인 메시지 코드부터 덜 구체적인 메시지 코드 순서로 생성한다.
required.item.itemName -> required
이러한 전략은 개발자가 중요한 메시지만 구체적으로 정의하고, 나머지는 범용적인 메시지로 처리해 편리하게 관리할 수 있다.
왜 복잡한 메시지 코드 관리 전략을 사용하는가?
모든 오류 메시지를 개별적으로 정의하면 관리하기 복잡하다.일반적인 메시지는 범용적인 메시지(required)로 처리하고, 중요한 메시지는 구체적으로 정의하여 사용하면 효율적이다.
오류 코드 관리 전략 도입
errors.properties 파일에 오류 코드에 따른 메시지를 레벨 별로 정의한다.
#==FieldError==
#Level1
required.item.itemName=상품 이름은 필수입니다.
#Level2 - 생략
#Level3
required.java.lang.String = 필수 문자입니다.
required.java.lang.Integer = 필수 숫자입니다.
#Level4
required = 필수 값 입니다.
예를 들어, 'itemName' 필드에 'required' 오류 코드가 발생하면, 다음 순서로 메시지를 찾는다.
- required.item.itemName
- required.itemName
- required.java.lang.String
- required
만일 1번 메시지가 없으면 2번을 찾고, 2번도 없으면 3번을 찾는다. 이렇게 덜 구체적인 메시지로 넘어가면서 검색한다.
정리
1. rejectValue() 메서드를 호출하여 특정 필드에 오류를 등록한다.
2. MessageCodesResolver 를 사용하여 오류 코드에서 메시지 코드들을 생성한다.
3. 생성된 메시지 코드들은 FieldError 객체 생성 시 저장된다.
4. 타임리프의 'th:errors' 디렉티브는 이러한 메시지 코드들을 사용하여 순서대로 메시지를 찾아 화면에 노출한다.
오류 코드와 메시지 처리 - 스프링 검증 오류 메시지
검증 오류 코드 유형 : 검증 오류 코드는 크게 두 가지로 분류된다.
개발자 지정 오류 코드 : rejectValue() 메서드를 통해 개발자가 직접 호출하여 오류 코드를 설정하는 경우
스프링 자동 오류 코드 : 스프링이 자동으로 검증 오류를 추가하는 경우, 주로 타입 불일치와 같은 상황에서 발생한다.
타입 불일치 오류 처리
가격 필드(price)에 "A"와 같은 문자열을 입력했을 때 로그를 확인 해보면 BinddingResult 에 FielddError 가 담겨있고, 다음과 같은 메시지 코드들이 생성된다.
codes [typeMismatch.item.price,typeMismatch.price,typeMismatch.java.lang.Integer,typeMismatch]
스프링은 타입 불일치 오류 발생 시 'typeMismatch' 라는 오류 코드를 사용한다.
MessageCodesResolver 를 통해 typeMismach 오류 코드로부터 4가지 메시지 코드가 생성된다.
- typeMismatch.user.age
- typeMismatch.age
- typeMismatch.int
- typeMismatch
오류 메시지 설정
- 기본적으로, 오류 메시지 코드가 'errors.properties' 파일에 없으면, 스프링은 내장된 기본 메시지를 출력한다.
- 개발자는 'errors.porperties' 파일에 타입 불일치에 따른 원하는 오류 메시지 코드와 대응하는 메시지를 추가하여 사용자 정의 메시지를 설정할 수 있다.
errors.properties
typeMismatch.java.lang.Integer=숫자를 입력해주세요.
typeMismatch=타입 오류입니다
Validator 분리
목적
복잡한 검증 로직을 별도의 클래스로 분리하여 코드의 간결함과 재사용성을 높일 수 있다.
스프링의 'Validator' 인터페이스
스프링에서는 검증을 체계적으로 수행할 수 있게 'Validator' 인터페이스를 제공한다. 이 인터페이스에는 두 가지 주요 메서드가 존재한다.
supprots(Class<?> clazz) : 해당 검증기가 지원하는 클래스 타입을 확인한다.
validator(Object target, Errors errors) : 검증을 수행하며, 오류 발생시 'Errors' 객체에 정보를 저장한다.
ItemValidator 클래스 구현
@Component
public class ItemValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return Item.class.isAssignableFrom(clazz);
//item == clazz
//item == subItem
}
@Override
public void validate(Object target, Errors errors) {
Item item = (Item) target;
//검증 로직
if (!StringUtils.hasText(item.getItemName())) {
errors.rejectValue("itemName", "required");
}
if ((item.getPrice() == null) || (item.getPrice() < 1000) || item.getPrice() > 10000000) {
errors.rejectValue("price", "range", new Object[]{1000, 10000}, null);
}
if (item.getQuantity() == null || item.getQuantity() >= 9999) {
errors.rejectValue("quantity", "max", new Object[]{9999}, null);
}
//특정 필드가 아닌 복합 룰 검증
if (item.getPrice() != null && item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
errors.reject("total", new Object[]{10000, resultPrice}, null);
}
}
}
}
- Item 클래스에 대한 검증을 수행하는 별도의 검증기 클래스를 구현한다.
- supports 메서드에서는 검증기가 Item 클래스 또는 그 하위 클래스를 지원하는지 확인한다.
- validator 메서드에서는 다양한 검증 조건을 구현하며, 오류가 발생하면 'Errors' 객체에 오류 정보를 추가한다.
Validator 사용
private final ItemValidator itemValidator;
public String addItemV5(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
itemValidator.validate(item, bindingResult);
//검증에 실패하면 다시 입력 폼으로
if (bindingResult.hasErrors()) {
log.info("errors = {}", bindingResult);
return "validation/v2/addForm";
}
//성공 로직
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v2/items/{itemId}";
}
- 컨트롤 내에서 Validator 를 사용하여 객체를 검증한다.
- addItemV5 메서드에서 ItemValidator 인스턴스를 사용해 item 객체를 검증한다.
- 검증 후에는 BindingResult 를 통해 오류가 있는지 확인하고, 오류가 있으면 해당 오류 정보를 로깅하고 입력 폼으로 다시 리다이렉트 한다.
Validator 활용의 체계적인 접근
WebDataBinder를 통한 검증
@InitBinder
public void init(WebDataBinder dataBinder) {
dataBinder.addValidators(itemValidator);
}
- WebDataBinder 는 스프링에서 파라미터 바인딩을 수행하며 내부적으로 검증 기능도 포함하고 있다.
- @InitBinder 를 사용하여 컨트롤러에서 WebDataBinder 에 검증기를 추가할 수 있다.
- 이렇게 등록된 검증기는 해당 컨트롤러에서 자동으로 적용된다.
@Validated 도입
public String addItemV6(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
//검증에 실패하면 다시 입력 폼으로
if (bindingResult.hasErrors()) {
log.info("errors = {}", bindingResult);
return "validation/v2/addForm";
}
//성공 로직
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v2/items/{itemId}";
}
- @Validated 는 검증을 요청하는 어노테이션으로, 검증 대상 객체 앞에 붙여 사용한다.
- @Validated 가 붙은 객체는 WebDataBinder 에 등록된 검증기를 통해 검증된다.
- 여러 검증기를 등록할 경우, 'supports()' 메서드를 통해 적절한 검증기를 선택하여 실행한다.
글로벌 설정
- WebMvcConfigurer 인터페이스를 구현하여 애플리케이션 전체에 검증기를 적용할 수 있다. 이를 통해 특정 컨트롤러에서만 아닌 전체 컨트롤러에 검증기를 적용할 수 있다.
- 하지만 이 방법으로 글로벌 설정을 하면 BeanValidator 의 자동 등록이 되지 않으므로 주의가 필요하다.
@Vlidated 와 @Valid
- 검증시 @Validated 와 @Valid 둘 다 사용이 가능하다
- @Valid 는 자바 표준 검증 어노테이션으로 사용하기 위해서는 의존성 추가가 필요하다.
- @Validated 는 스프링 전용 검증 어노테이션으로, 스프링의 검증기와 함께 사용한다.
출처 : 인프런 - 🔗 스프링 MVC 2 - 백엔드 개발 활용 기술by 우아한형제 김영한님