상품 관리 시스템 등록/수정 폼에 다음과 같은 검증로직을 추가한다
- 타입 검증
- 필드 검증
- 특정 필드 범위 넘어서는지 검증
컨트롤러의 역할 중 하나 HTTP 요청이 정상인지 검증한다
클라이언트 검증 : 조작 가능 / 보안에 취약
서버 검증 : 즉각적인 고객 사용성, 즉 새로고침 REQUEST/RESONSE가 와야해서 사용성이 떨어짐
둘이 적절히 섞되, 서버 검증은 필수로 이루어져야한다.
API 방식 이용시 스펙을 잘 정의해서 검증 오류를 API 응답으로 잘 전달해야 함.
addItem() 컨트롤러 메서드를 통해 검증 로직 구현 / 발전시키기 V1
@PostMapping("/add")
public String addItem(@ModelAttribute Item item, RedirectAttributes redirectAttributes,
Model model) {
// Validation Result
Map<String, String> errors = new HashMap<>();
// Validation Logic
if(!StringUtils.hasText(item.getItemName())){
errors.put("itemName", "상품 이름은 필수입니다");
}
if(item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000){
errors.put("price", "가격은 1000원에서 100000원 사이로 허용합니다");
}
if(item.getQuantity() == null || item.getQuantity() >= 9999){
errors.put("quantity", "수량은 최대 9,999까지입니다");
}
// ModelAttribute는 값을 자동으로 넣어줌 -> th:object 그대로 남아있음.
// 메모리상에 남아있는 객체 자료 재활용함. 위에 new Item()으로 선언한 이유임
// Combinational Validation
if(item.getPrice() != null && item.getQuantity() != null){
int resultPrice = item.getPrice() * item.getQuantity();
if(resultPrice < 10000){
errors.put("globalError", " 가격 * 수량의 합은 10000원 이상이어야 함");
}
}
if(!errors.isEmpty()){
model.addAttribute("errors", errors);
return "validation/v1/addForm";
}
// 에러 없는 경우(아래 내용)
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v1/items/{itemId}";
}
Map<String, String> errors = new HashMap();
- 어떤 검증에서 오류가 발생하는지 정보 담아둠
if(!StringUtils.hasText(item.getItemName())){
errors.put("itemName", "상품 이름은 필수입니다");
}```
`if(!StringUtils.hasText(item.getItemName())))`
해당 필드에 텍스트가 없으면 상품 이름은 필수입니다라는 오류 메세지를 key, value 형태로 저장한다.
이때 부정의 부정은 코드의 가독성이 떨어지기 때문에 따로 `is_empty`식으로 method extraction을 통해 가독성을 향상시키는게 좋다
```java
// Combinational Validation
if(item.getPrice() != null && item.getQuantity() != null){
int resultPrice = item.getPrice() * item.getQuantity();
if(resultPrice < 10000){
errors.put("globalError", " 가격 * 수량의 합은 10000원 이상이어야 함");
}
}```
특정 필드를 넘어서는 오류를 처리해야할 경우 필드 이름을 넣을 수 없기 때문에 (여러 필드에 해당)
`globalError`라는 `key`를 사용한다.
```java
if(!errors.isEmpty()){
model.addAttribute("errors", errors);
return "validation/v1/addForm";
}
다음 코드는 입력 오류가 발생한 경우 즉 errors
에 내용이 있는 경우 오류 메세지 출력을 위해model
에 errors
를 담고 입력 폼이 있는 뷰 템플릿으로 다시 보내는 것이다.
이렇게 되면 item
객체를 저장하기 이전에 validation을 진행하고, validation에 오류가 있는 경우 미리 뷰 템플릿으로 다시 보냄으로써 잘못된 값의 입력을 방지할 수 있다.
템플릿 수정
그렇다면 이제 컨트롤러에서 담은 errors값을 템플릿에서 사용자에게 보이기 위해 html 코드를 수정하겠다.
<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:class="${errors?.containsKey('price')} ? 'form-control field-error' : 'form-control' "
th:field="*{price}" 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:class="${errors?.containsKey('quantity')} ? 'form-control field-error' : 'form-control' "
th:field="*{quantity}" class="form-control" placeholder="수량을 입력하세요">
<div class="field-error" th:if="${errors?.containsKey('quantity')}" th:text="${errors['quantity']}">
수량 확인
</div>
</div>
</form>
</div> <!-- /container -->
</body>
</html>
<input type="text" id="itemName" th:field="*{itemName}"
th:class="${errors?.containsKey('itemName')} ? 'form-control field-error' : 'form-control' "
class="form-control" placeholder="이름을 입력하세요">
th:class
속성에서 ?.
Operator는 Safe Navigation Operator로 errors
가 null
이라고 가정한다면, 즉 등록폼에 진입한 시점에서는 errors
에 아무런 값이 담겨있지 않기 때문에 null
에 해당하는데
errors.containsKey()
를 호출하면 NullPointerException
이 발생하게 된다.
?.
operator는 이와 같이 특정 객체가 null
에 해당할 경우 NullPointerException
을 반환하는 대신 null
을 반환하도록 하는 문법에 해당한다.
th:if
속성의 경우 null
의 경우 실패로 처리되기 때문에 오류 메세지 출력이 없다.
th:class
속성에서 조건식이 구성되어있는데 해당 조건식이 만족될 경우 form-control field-error
가 만족되지 않을 경우 form-control
이 태그의 속성으로 입력된다.
.field-error{
color:red;
}
.field-error
속성은 다음과 같이 기술되어, 오류메세지 출력에 사용된다.
addItem() 컨트롤러 메서드를 통해 검증 로직 구현 / 발전시키기 V2
위 V1 검증 로직에는 몇가지 불편한 점이 있다.
- 타입 오류 처리가 안된다
- 문자 보관이 어렵다(원래 입력했던 값)
- 바인딩이 안되면, 오류처리가 안된다(일단 바인딩 즉, 값이 들어와야 검증이 가능함)
- 객체 데이터 타입이 맞지 않으면 그냥 오류 발생(애초에 값 검증은 객체 필드에 값이 있음을 전제)
- 고객이 입력한 값이 어딘가에 별도로 관리가 되어야 다시 띄워줄 수 있음
//@PostMapping("/add")
public String addItemV1(@ModelAttribute Item item,
BindingResult bindingResult, // ModelAttribute 바로 뒤에 와야 함! (순서 중요)
RedirectAttributes redirectAttributes,
Model model) {
// Validation Logic
if(!StringUtils.hasText(item.getItemName())){
bindingResult.addError(new FieldError("item", "itemName", "상품 이름은 필수입니다"));
}
if(item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000){
bindingResult.addError(new FieldError("item", "price", "가격은 ~ 입니다"));
}
if(item.getQuantity() == null || item.getQuantity() >= 9999){
bindingResult.addError(new FieldError("item", "quantity", "수량 에러 최대 9,999"));
}
// ModelAttribute는 값을 자동으로 넣어줌 -> th:object 그대로 남아있음.
// 메모리상에 남아있는 객체 자료 재활용함. 위에 new Item()으로 선언한 이유임
// Combinational Validation
if(item.getPrice() != null && item.getQuantity() != null){
int resultPrice = item.getPrice() * item.getQuantity();
if(resultPrice < 10000){
bindingResult.addError(new ObjectError("item", "가격과 수량의 합은 10000 이상이여야 합니다"));
}
}
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}";
}
BindingResult
의 도입
BindingResult
를 사용하게 되면서 기존에 이용했던 HashMap()
의 errors
객체는 더 이상 불필요하게 됐다.
이 BindingResult
객체를 사용하려면 @ModelAttribute
를 사용하는 객체 다음에 위치해야한다.
(target 객체를 제대로 잡기 위함 / 내가 이 객체를 검증하겠다!)
그 대신 오류 메세지를 저장하기 위해 FieldError
와 ObjectError
가 등장하게 됐는데, 용례는 다음과 같다.
FieldError
필드에 오류가 있는 경우 FieldError
객체를 생성하여 다음과 같이 bindingResult
에 담아두면 된다.
public FieldError(String objectName, String field, String defaultMessage){}
// Validation Logic
if(!StringUtils.hasText(item.getItemName())){
bindingResult.addError(new FieldError("item", "itemName", "상품 이름은 필수입니다"));
}
ObjectError
특정 필드를 넘어서는 다중 필드 오류가 있는 경우 다음과 같이 ObjectError
를 생성하여 BindingResult
에 담아두면 된다.
public ObjectError(String objectName, String defaultMessage){}
// Combinational Validation
if(item.getPrice() != null && item.getQuantity() != null){
int resultPrice = item.getPrice() * item.getQuantity();
if(resultPrice < 10000){
bindingResult.addError(new ObjectError("item", "가격과 수량의 합은 10000 이상이여야 합니다"));
}
}
FieldError
, ObjectError
공통
1) objectName
: @ModelAttribute
의 이름
2) defaultMessage
: 오류 기본 메세지
HTML 코드 수정
컨트롤러 코드가 BindingResult
를 이용하여 리팩토링 되었기에 해당 내용을 템플릿 html코드에 적용하겠다.
<form action="item.html" th:action th:object="${item}" method="post">
<div th:if="${#fields.hasGlobalErrors}">
<p class="field-error" th:each="err : ${#fields.globalErrors()}"
th:text="${err}">전체 오류
메시지</p>
</div>
<div>
<label for="itemName" th:text="#{label.item.itemName}">상품명</label>
<input type="text" id="itemName" th:field="*{itemName}"
th:errorclass="field-error"
class="form-control" placeholder="이름을 입력하세요">
<div class="field-error" th:errors="*{itemName}">
상품명 오류
</div>
</div> <div> <label for="price" th:text="#{label.item.price}">가격</label>
<input type="text" id="price"
th:errorclass="field-error"
th:field="*{price}" class="form-control" placeholder="가격을 입력하세요">
<div class="field-error" th:errors="*{price}"> <!--에러 있음 렌더링 없음 X --> 가격 오류
</div>
</div> <div> <label for="quantity" th:text="#{label.item.quantity}">수량</label>
<input type="text" id="quantity" th:errorclass="field-error"
th:field="*{quantity}" class="form-control" placeholder="수량을 입력하세요">
<div class="field-error" th:errors="*{quantity}">
수량 확인
</div>
</div>
Thymeleaf는 스프링의 bindingResult
를 활용 / 검증 오류를 쉽게 렌더링 할 수 있도록 지원한다
#fields
: #fields
를 통해 BindingResult
가 제공하는 검증 오류에 접근할 수 있다.th:errors
: 해당 필드에 오류가 있는 경우 태그를 출력한다 th:if
보다 편리하다.th:errorclass
: th:field
에서 지정한 필드에 오류가 있으면 class
정보를 추가한다.th:attrappend
와 비슷하다고 보면 될듯!
'스프링 공부 (인프런 김영한 선생님) > 스프링 MVC 2편' 카테고리의 다른 글
[스프링 웹 MVC 2편] 007. Bean Validation (0) | 2023.08.15 |
---|---|
[스프링 웹 MVC 2편] 006. Validation (2) (0) | 2023.08.15 |
[스프링 웹 MVC 2편] 004. 국제화 메세지 (0) | 2023.08.11 |
[스프링 웹 MVC 2편] 003. 스프링 통합과 폼 (0) | 2023.08.11 |
[스프링 웹 MVC 2편] 002. 타임리프 기본기능(2) (0) | 2023.08.11 |
상품 관리 시스템 등록/수정 폼에 다음과 같은 검증로직을 추가한다
- 타입 검증
- 필드 검증
- 특정 필드 범위 넘어서는지 검증
컨트롤러의 역할 중 하나 HTTP 요청이 정상인지 검증한다
클라이언트 검증 : 조작 가능 / 보안에 취약
서버 검증 : 즉각적인 고객 사용성, 즉 새로고침 REQUEST/RESONSE가 와야해서 사용성이 떨어짐
둘이 적절히 섞되, 서버 검증은 필수로 이루어져야한다.
API 방식 이용시 스펙을 잘 정의해서 검증 오류를 API 응답으로 잘 전달해야 함.
addItem() 컨트롤러 메서드를 통해 검증 로직 구현 / 발전시키기 V1
@PostMapping("/add")
public String addItem(@ModelAttribute Item item, RedirectAttributes redirectAttributes,
Model model) {
// Validation Result
Map<String, String> errors = new HashMap<>();
// Validation Logic
if(!StringUtils.hasText(item.getItemName())){
errors.put("itemName", "상품 이름은 필수입니다");
}
if(item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000){
errors.put("price", "가격은 1000원에서 100000원 사이로 허용합니다");
}
if(item.getQuantity() == null || item.getQuantity() >= 9999){
errors.put("quantity", "수량은 최대 9,999까지입니다");
}
// ModelAttribute는 값을 자동으로 넣어줌 -> th:object 그대로 남아있음.
// 메모리상에 남아있는 객체 자료 재활용함. 위에 new Item()으로 선언한 이유임
// Combinational Validation
if(item.getPrice() != null && item.getQuantity() != null){
int resultPrice = item.getPrice() * item.getQuantity();
if(resultPrice < 10000){
errors.put("globalError", " 가격 * 수량의 합은 10000원 이상이어야 함");
}
}
if(!errors.isEmpty()){
model.addAttribute("errors", errors);
return "validation/v1/addForm";
}
// 에러 없는 경우(아래 내용)
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v1/items/{itemId}";
}
Map<String, String> errors = new HashMap();
- 어떤 검증에서 오류가 발생하는지 정보 담아둠
if(!StringUtils.hasText(item.getItemName())){
errors.put("itemName", "상품 이름은 필수입니다");
}```
`if(!StringUtils.hasText(item.getItemName())))`
해당 필드에 텍스트가 없으면 상품 이름은 필수입니다라는 오류 메세지를 key, value 형태로 저장한다.
이때 부정의 부정은 코드의 가독성이 떨어지기 때문에 따로 `is_empty`식으로 method extraction을 통해 가독성을 향상시키는게 좋다
```java
// Combinational Validation
if(item.getPrice() != null && item.getQuantity() != null){
int resultPrice = item.getPrice() * item.getQuantity();
if(resultPrice < 10000){
errors.put("globalError", " 가격 * 수량의 합은 10000원 이상이어야 함");
}
}```
특정 필드를 넘어서는 오류를 처리해야할 경우 필드 이름을 넣을 수 없기 때문에 (여러 필드에 해당)
`globalError`라는 `key`를 사용한다.
```java
if(!errors.isEmpty()){
model.addAttribute("errors", errors);
return "validation/v1/addForm";
}
다음 코드는 입력 오류가 발생한 경우 즉 errors
에 내용이 있는 경우 오류 메세지 출력을 위해model
에 errors
를 담고 입력 폼이 있는 뷰 템플릿으로 다시 보내는 것이다.
이렇게 되면 item
객체를 저장하기 이전에 validation을 진행하고, validation에 오류가 있는 경우 미리 뷰 템플릿으로 다시 보냄으로써 잘못된 값의 입력을 방지할 수 있다.
템플릿 수정
그렇다면 이제 컨트롤러에서 담은 errors값을 템플릿에서 사용자에게 보이기 위해 html 코드를 수정하겠다.
<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:class="${errors?.containsKey('price')} ? 'form-control field-error' : 'form-control' "
th:field="*{price}" 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:class="${errors?.containsKey('quantity')} ? 'form-control field-error' : 'form-control' "
th:field="*{quantity}" class="form-control" placeholder="수량을 입력하세요">
<div class="field-error" th:if="${errors?.containsKey('quantity')}" th:text="${errors['quantity']}">
수량 확인
</div>
</div>
</form>
</div> <!-- /container -->
</body>
</html>
<input type="text" id="itemName" th:field="*{itemName}"
th:class="${errors?.containsKey('itemName')} ? 'form-control field-error' : 'form-control' "
class="form-control" placeholder="이름을 입력하세요">
th:class
속성에서 ?.
Operator는 Safe Navigation Operator로 errors
가 null
이라고 가정한다면, 즉 등록폼에 진입한 시점에서는 errors
에 아무런 값이 담겨있지 않기 때문에 null
에 해당하는데
errors.containsKey()
를 호출하면 NullPointerException
이 발생하게 된다.
?.
operator는 이와 같이 특정 객체가 null
에 해당할 경우 NullPointerException
을 반환하는 대신 null
을 반환하도록 하는 문법에 해당한다.
th:if
속성의 경우 null
의 경우 실패로 처리되기 때문에 오류 메세지 출력이 없다.
th:class
속성에서 조건식이 구성되어있는데 해당 조건식이 만족될 경우 form-control field-error
가 만족되지 않을 경우 form-control
이 태그의 속성으로 입력된다.
.field-error{
color:red;
}
.field-error
속성은 다음과 같이 기술되어, 오류메세지 출력에 사용된다.
addItem() 컨트롤러 메서드를 통해 검증 로직 구현 / 발전시키기 V2
위 V1 검증 로직에는 몇가지 불편한 점이 있다.
- 타입 오류 처리가 안된다
- 문자 보관이 어렵다(원래 입력했던 값)
- 바인딩이 안되면, 오류처리가 안된다(일단 바인딩 즉, 값이 들어와야 검증이 가능함)
- 객체 데이터 타입이 맞지 않으면 그냥 오류 발생(애초에 값 검증은 객체 필드에 값이 있음을 전제)
- 고객이 입력한 값이 어딘가에 별도로 관리가 되어야 다시 띄워줄 수 있음
//@PostMapping("/add")
public String addItemV1(@ModelAttribute Item item,
BindingResult bindingResult, // ModelAttribute 바로 뒤에 와야 함! (순서 중요)
RedirectAttributes redirectAttributes,
Model model) {
// Validation Logic
if(!StringUtils.hasText(item.getItemName())){
bindingResult.addError(new FieldError("item", "itemName", "상품 이름은 필수입니다"));
}
if(item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000){
bindingResult.addError(new FieldError("item", "price", "가격은 ~ 입니다"));
}
if(item.getQuantity() == null || item.getQuantity() >= 9999){
bindingResult.addError(new FieldError("item", "quantity", "수량 에러 최대 9,999"));
}
// ModelAttribute는 값을 자동으로 넣어줌 -> th:object 그대로 남아있음.
// 메모리상에 남아있는 객체 자료 재활용함. 위에 new Item()으로 선언한 이유임
// Combinational Validation
if(item.getPrice() != null && item.getQuantity() != null){
int resultPrice = item.getPrice() * item.getQuantity();
if(resultPrice < 10000){
bindingResult.addError(new ObjectError("item", "가격과 수량의 합은 10000 이상이여야 합니다"));
}
}
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}";
}
BindingResult
의 도입
BindingResult
를 사용하게 되면서 기존에 이용했던 HashMap()
의 errors
객체는 더 이상 불필요하게 됐다.
이 BindingResult
객체를 사용하려면 @ModelAttribute
를 사용하는 객체 다음에 위치해야한다.
(target 객체를 제대로 잡기 위함 / 내가 이 객체를 검증하겠다!)
그 대신 오류 메세지를 저장하기 위해 FieldError
와 ObjectError
가 등장하게 됐는데, 용례는 다음과 같다.
FieldError
필드에 오류가 있는 경우 FieldError
객체를 생성하여 다음과 같이 bindingResult
에 담아두면 된다.
public FieldError(String objectName, String field, String defaultMessage){}
// Validation Logic
if(!StringUtils.hasText(item.getItemName())){
bindingResult.addError(new FieldError("item", "itemName", "상품 이름은 필수입니다"));
}
ObjectError
특정 필드를 넘어서는 다중 필드 오류가 있는 경우 다음과 같이 ObjectError
를 생성하여 BindingResult
에 담아두면 된다.
public ObjectError(String objectName, String defaultMessage){}
// Combinational Validation
if(item.getPrice() != null && item.getQuantity() != null){
int resultPrice = item.getPrice() * item.getQuantity();
if(resultPrice < 10000){
bindingResult.addError(new ObjectError("item", "가격과 수량의 합은 10000 이상이여야 합니다"));
}
}
FieldError
, ObjectError
공통
1) objectName
: @ModelAttribute
의 이름
2) defaultMessage
: 오류 기본 메세지
HTML 코드 수정
컨트롤러 코드가 BindingResult
를 이용하여 리팩토링 되었기에 해당 내용을 템플릿 html코드에 적용하겠다.
<form action="item.html" th:action th:object="${item}" method="post">
<div th:if="${#fields.hasGlobalErrors}">
<p class="field-error" th:each="err : ${#fields.globalErrors()}"
th:text="${err}">전체 오류
메시지</p>
</div>
<div>
<label for="itemName" th:text="#{label.item.itemName}">상품명</label>
<input type="text" id="itemName" th:field="*{itemName}"
th:errorclass="field-error"
class="form-control" placeholder="이름을 입력하세요">
<div class="field-error" th:errors="*{itemName}">
상품명 오류
</div>
</div> <div> <label for="price" th:text="#{label.item.price}">가격</label>
<input type="text" id="price"
th:errorclass="field-error"
th:field="*{price}" class="form-control" placeholder="가격을 입력하세요">
<div class="field-error" th:errors="*{price}"> <!--에러 있음 렌더링 없음 X --> 가격 오류
</div>
</div> <div> <label for="quantity" th:text="#{label.item.quantity}">수량</label>
<input type="text" id="quantity" th:errorclass="field-error"
th:field="*{quantity}" class="form-control" placeholder="수량을 입력하세요">
<div class="field-error" th:errors="*{quantity}">
수량 확인
</div>
</div>
Thymeleaf는 스프링의 bindingResult
를 활용 / 검증 오류를 쉽게 렌더링 할 수 있도록 지원한다
#fields
: #fields
를 통해 BindingResult
가 제공하는 검증 오류에 접근할 수 있다.th:errors
: 해당 필드에 오류가 있는 경우 태그를 출력한다 th:if
보다 편리하다.th:errorclass
: th:field
에서 지정한 필드에 오류가 있으면 class
정보를 추가한다.th:attrappend
와 비슷하다고 보면 될듯!
'스프링 공부 (인프런 김영한 선생님) > 스프링 MVC 2편' 카테고리의 다른 글
[스프링 웹 MVC 2편] 007. Bean Validation (0) | 2023.08.15 |
---|---|
[스프링 웹 MVC 2편] 006. Validation (2) (0) | 2023.08.15 |
[스프링 웹 MVC 2편] 004. 국제화 메세지 (0) | 2023.08.11 |
[스프링 웹 MVC 2편] 003. 스프링 통합과 폼 (0) | 2023.08.11 |
[스프링 웹 MVC 2편] 002. 타임리프 기본기능(2) (0) | 2023.08.11 |