์ง๋ ์๊ฐ์ ๋ฐ์ํ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด,
๋์ผํ ๋ชจ๋ธ ๊ฐ์ฒด๋ฅผ ๋ฑ๋กํ ๋์ ์์ ํ ๋ ๊ฐ๊ฐ ๋ค๋ฅด๊ฒ ๊ฒ์ฆํ๋ ๋ฐฉ๋ฒ์ ์์๋ณด์๐
1๏ธโฃ BeanValidation์ groups ๊ธฐ๋ฅ์ ์ฌ์ฉํ๋ค.
2๏ธโฃ Item์ ์ง์ ์ฌ์ฉํ์ง ์๊ณ , ItemSaveForm, ItemUpdateForm ๊ฐ์ ํผ ์ ์ก์ ์ํ ๋ณ๋์ ๋ชจ๋ธ ๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด์ ์ฌ์ฉํ๋ค.
โก๏ธ ์ด๋ฐ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด Bean Validation์ groups๋ผ๋ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ค.
์๋ฅผ ๋ค์ด์ ๋ฑ๋ก์์ ๊ฒ์ฆํ ๊ธฐ๋ฅ๊ณผ ์์ ์์ ๊ฒ์ฆํ ๊ธฐ๋ฅ์ ๊ฐ๊ฐ ๊ทธ๋ฃน์ผ๋ก ๋๋์ด ์ ์ฉํ ์ ์๋ค.
package hello.itemservice.domain.item;
public interface SaveCheck {
}
package hello.itemservice.domain.item;
public interface UpdateCheck {
}
package hello.itemservice.domain.item;
import lombok.Data;
import org.hibernate.validator.constraints.Range;
import org.hibernate.validator.constraints.ScriptAssert;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
@Data
public class Item {
@NotNull(groups = UpdateCheck.class)
private Long id;
@NotBlank(groups = {SaveCheck.class, UpdateCheck.class})
private String itemName;
@NotNull(groups = {SaveCheck.class, UpdateCheck.class})
@Range(min = 1000, max = 1000000, groups = {SaveCheck.class, UpdateCheck.class})
private Integer price;
@NotNull(groups = {SaveCheck.class, UpdateCheck.class})
// ๋ฑ๋ก์์๋ง ์ฒดํฌํ๊ณ ์์ ์์๋ ์ฒดํฌ X
@Max(value = 9999, groups = {SaveCheck.class})
private Integer quantity;
public Item() {
}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
@PostMapping("/add")
public String addItem2(@Validated(SaveCheck.class) @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()) {
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}";
}
@PostMapping("/{itemId}/edit")
public String editV2(@PathVariable Long itemId, @Validated(UpdateCheck.class) @ModelAttribute Item item, BindingResult bindingResult) {
//ํน์ ํ๋๊ฐ ์๋ ๋ณตํฉ ๋ฃฐ ๊ฒ์ฆ
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()) {
log.info("errors={}", bindingResult);
return "validation/v3/editForm";
}
itemRepository.update(itemId, item);
return "redirect:/validation/v3/items/{itemId}";
}
โฌ๏ธ ๋ฑ๋ก ์์๋ 99999์ ๊ฑธ๋ฆฌ์ง๋ง, ์์ ํ ๋๋ ๊ฑธ๋ฆฌ์ง ์๋๋ค๐๐ป
๐ ์ฐธ๊ณ
@Valid
์๋ groups๋ฅผ ์ ์ฉํ ์ ์๋ ๊ธฐ๋ฅ์ด ์๋ค.
๋ฐ๋ผ์ groups๋ฅผ ์ฌ์ฉํ๋ ค๋ฉด@Validated
๋ฅผ ์ฌ์ฉํด์ผ ํ๋ค!
์์ ๋ง๋ ๊ธฐ๋ฅ์ ์ ์งํ๊ธฐ ์ํด, ์ปจํธ๋กค๋ฌ์ ํ ํ๋ฆฟ ํ์ผ์ ๋ณต์ฌํ์๐
1๏ธโฃ ValidationItemControllerV4 ์ปจํธ๋กค๋ฌ ์์ฑ
hello.itemservice.web.validation.ValidationItemControllerV3
๋ณต์ฌhello.itemservice.web.validation.ValidationItemControllerV4
๋ถ์ฌ๋ฃ๊ธฐ2๏ธโฃ ํ ํ๋ฆฟ ํ์ผ ๋ณต์ฌ
validation/v3
๋๋ ํ ๋ฆฌ์ ๋ชจ๋ ํ
ํ๋ฆฟ ํ์ผ์ validation/v4
๋๋ ํ ๋ฆฌ๋ก ๋ณต์ฌ/resources/templates/validation/v4/
ํ์ 4๊ฐ ํ์ผ ๋ชจ๋ URL ๊ฒฝ๋ก ๋ณ๊ฒฝ3๏ธโฃ ์คํ
http://localhost:8080/validation/v4/items
์คํ ํ ์น ๋ธ๋ผ์ฐ์ ์ URL์ด validation/v4
์ผ๋ก ์ ์ ์ง๋๋์ง ํ์ธ!์ค๋ฌด์์๋ groups ๋ฅผ ์ ์ฌ์ฉํ์ง ์๋๋ฐ, ๋ฑ๋ก์ ํผ์์ ์ ๋ฌํ๋ ๋ฐ์ดํฐ๊ฐ Item ๋๋ฉ์ธ ๊ฐ์ฒด์ ๋ฑ ๋ง์ง ์๊ธฐ ๋๋ฌธ์ด๋คโน๏ธ
๊ทธ๋์ ๋ณดํต Item ์ ์ง์ ์ ๋ฌ๋ฐ๋ ๊ฒ์ด ์๋๋ผ, ๋ณต์กํ ํผ์ ๋ฐ์ดํฐ๋ฅผ ์ปจํธ๋กค๋ฌ๊น์ง ์ ๋ฌํ ๋ณ๋์ ๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด์ ์ ๋ฌํ๋คโ๏ธ
ItemSaveForm
์ด๋ผ๋ ํผ์ ์ ๋ฌ๋ฐ๋ ์ ์ฉ ๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด์ @ModelAttribute
๋ก ์ฌ์ฉํ๋ค. Item
์ ์์ฑํ๋ค.Item
๋๋ฉ์ธ ๊ฐ์ฒด๋ฅผ ํผ ์ ๋ฌ ๋ฐ์ดํฐ๋ก ์ฌ์ฉํ๊ณ , ๊ทธ๋๋ก ์ญ ๋๊ธฐ๋ฉด ํธ๋ฆฌํ๊ฒ ์ง๋ง,
์์์ ์ค๋ช
ํ ๊ฒ๊ณผ ๊ฐ์ด ์ค๋ฌด์์๋ Item
์ ๋ฐ์ดํฐ๋ง ๋์ด์ค๋ ๊ฒ์ด ์๋๋ผ ๋ฌด์ํ ์ถ๊ฐ ๋ฐ์ดํฐ๊ฐ ๋์ด์จ๋ค.
๊ทธ๋ฆฌ๊ณ ๋ ๋์๊ฐ์ Item
์ ์์ฑํ๋๋ฐ ํ์ํ ์ถ๊ฐ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ ๋ค๋ฅธ ๊ณณ์์ ์ฐพ์์์ผ ํ ์๋ ์๋ค๐ค
โก๏ธ ๋ฐ๋ผ์ ์ด๋ ๊ฒ ํผ ๋ฐ์ดํฐ ์ ๋ฌ์ ์ํ ๋ณ๋์ ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํ๊ณ , ๋ฑ๋ก, ์์ ์ฉ ํผ ๊ฐ์ฒด๋ฅผ ๋๋๋ฉด ๋ฑ๋ก, ์์ ์ด ์์ ํ ๋ถ๋ฆฌ๋๊ธฐ ๋๋ฌธ์ groups ๋ฅผ ์ ์ฉํ ์ผ์ ๋๋ฌผ๋คโ๏ธ
@Data
public class Item {
private Long id;
private String itemName;
private Integer price;
private Integer quantity;
public Item() {
}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
package hello.itemservice.web.validation.form;
import lombok.Data;
import org.hibernate.validator.constraints.Range;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
@Data
public class ItemSaveForm {
@NotBlank
private String itemName;
@NotNull
@Range(min = 1000, max = 1000000)
private Integer price;
@NotNull
@Max(value = 9999)
private Integer quantity;
}
package hello.itemservice.web.validation.form;
import lombok.Data;
import org.hibernate.validator.constraints.Range;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
@Data
public class ItemUpdateForm {
@NotNull
private Long id;
@NotBlank
private String itemName;
@NotNull
@Range(min = 1000, max = 1000000)
private Integer price;
//์์ ์์๋ ์๋์ ์์ ๋กญ๊ฒ ๋ณ๊ฒฝํ ์ ์์
private Integer quantity;
}
package hello.itemservice.web.validation;
import hello.itemservice.domain.item.Item;
import hello.itemservice.domain.item.ItemRepository;
import hello.itemservice.domain.item.SaveCheck;
import hello.itemservice.domain.item.UpdateCheck;
import hello.itemservice.web.validation.form.ItemSaveForm;
import hello.itemservice.web.validation.form.ItemUpdateForm;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import java.util.List;
@Slf4j
@Controller
@RequestMapping("/validation/v4/items")
@RequiredArgsConstructor
public class ValidationItemControllerV4 {
private final ItemRepository itemRepository;
@GetMapping
public String items(Model model) {
List<Item> items = itemRepository.findAll();
model.addAttribute("items", items);
return "validation/v4/items";
}
@GetMapping("/{itemId}")
public String item(@PathVariable long itemId, Model model) {
Item item = itemRepository.findById(itemId);
model.addAttribute("item", item);
return "validation/v4/item";
}
@GetMapping("/add")
public String addForm(Model model) {
model.addAttribute("item", new Item());
return "validation/v4/addForm";
}
@PostMapping("/add")
public String addItem(@Validated @ModelAttribute("item") ItemSaveForm form, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
// ํน์ ํ๋๊ฐ ์๋ ๋ณตํฉ ๋ฃฐ ๊ฒ์ฆ
if (form.getPrice() != null && form.getQuantity() != null) {
int resultPrice = form.getPrice() * form.getQuantity();
if (resultPrice < 10000) {
bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
}
}
// ๊ฒ์ฆ์ ์คํจํ๋ฉด ๋ค์ ์
๋ ฅ ํผ์ผ๋ก
if (bindingResult.hasErrors()) {
log.info("errors={} ", bindingResult);
return "validation/v4/addForm";
}
// ์ฑ๊ณต ๋ก์ง
Item item = new Item();
item.setItemName(form.getItemName());
item.setPrice(form.getPrice());
item.setQuantity(form.getQuantity());
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v4/items/{itemId}";
}
@GetMapping("/{itemId}/edit")
public String editForm(@PathVariable Long itemId, Model model) {
Item item = itemRepository.findById(itemId);
model.addAttribute("item", item);
return "validation/v4/editForm";
}
@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId, @Validated @ModelAttribute("item") ItemUpdateForm form, BindingResult bindingResult) {
// ํน์ ํ๋๊ฐ ์๋ ๋ณตํฉ ๋ฃฐ ๊ฒ์ฆ
if (form.getPrice() != null && form.getQuantity() != null) {
int resultPrice = form.getPrice() * form.getQuantity();
if (resultPrice < 10000) {
bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
}
}
if (bindingResult.hasErrors()) {
log.info("errors={}", bindingResult);
return "validation/v4/editForm";
}
Item itemParam = new Item();
itemParam.setItemName(form.getItemName());
itemParam.setPrice(form.getPrice());
itemParam.setQuantity(form.getQuantity());
itemRepository.update(itemId, itemParam);
return "redirect:/validation/v4/items/{itemId}";
}
}
addItem()
, addItemV2()
edit()
, editV2()
addItem()
, edit()
๐ ํผ ๊ฐ์ฒด ๋ฐ์ธ๋ฉ
@PostMapping("/add") public String addItem(@Validated @ModelAttribute("item") ItemSaveForm form, BindingResult bindingResult, RedirectAttributes redirectAttributes) { . . }
Item
๋์ ์ItemSaveform
์ ์ ๋ฌ ๋ฐ๊ณ@Validated
๋ก ๊ฒ์ฆ๋ ์ํํ๊ณ ,BindingResult
๋ก ๊ฒ์ฆ ๊ฒฐ๊ณผ๋ ๋ฐ๋๋ค.
๐ ํผ ๊ฐ์ฒด๋ฅผ Item์ผ๋ก ๋ณํ
//์ฑ๊ณต ๋ก์ง Item item = new Item(); item.setItemName(form.getItemName()); item.setPrice(form.getPrice()); item.setQuantity(form.getQuantity()); Item savedItem = itemRepository.save(item);
- ํผ ๊ฐ์ฒด์ ๋ฐ์ดํฐ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก Item ๊ฐ์ฒด๋ฅผ ์์ฑํ๋ค.
- ์ด๋ ๊ฒ ํผ ๊ฐ์ฒด ์ฒ๋ผ ์ค๊ฐ์ ๋ค๋ฅธ ๊ฐ์ฒด๊ฐ ์ถ๊ฐ๋๋ฉด ๋ณํํ๋ ๊ณผ์ ์ด ์ถ๊ฐ๋๋ค.
โฌ๏ธ ์์ ์ ์๋์ด 0์ด์ด๋ ๋จ
โฌ๏ธ ์์ ์ ์๋์ด max์ฌ๋ ๋จ
@Valid
, @Validated
๋ HttpMessageConverter
(@RequestBody
)์๋ ์ ์ฉํ ์ ์๋คโ๏ธ
package hello.itemservice.web.validation;
import hello.itemservice.web.validation.form.ItemSaveForm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController
@RequestMapping("/validation/api/items")
public class ValidationItemApiController {
@PostMapping("/add")
public Object addItem(@RequestBody @Validated ItemSaveForm form, BindingResult bindingResult) {
log.info("API ์ปจํธ๋กค๋ฌ ํธ์ถ");
if (bindingResult.hasErrors()) {
log.info("๊ฒ์ฆ ์ค๋ฅ ๋ฐ์ errors={}", bindingResult);
return bindingResult.getAllErrors();
}
log.info("์ฑ๊ณต ๋ก์ง ์คํ");
return form;
}
}
Postman์์ Body โก๏ธ raw โก๏ธ JSON์ ์ ํ!
API์ ๊ฒฝ์ฐ 3๊ฐ์ง ๊ฒฝ์ฐ๋ฅผ ๋๋์ด ์๊ฐํด์ผ ํ๋คโ๏ธ
1๏ธโฃ-1) ์ฑ๊ณต ์์ฒญ
POST http://localhost:8080/validation/api/items/add
{"itemName":"hello", "price":1000, "quantity": 10}
1๏ธโฃ-2) ์ฑ๊ณต ์์ฒญ ๋ก๊ทธ
API ์ปจํธ๋กค๋ฌ ํธ์ถ
์ฑ๊ณต ๋ก์ง ์คํ
2๏ธโฃ-1) ์คํจ ์์ฒญ
POST http://localhost:8080/validation/api/items/add
{"itemName":"hello", "price":"A", "quantity": 10}
โฌ๏ธ price
์ ๊ฐ์ ์ซ์๊ฐ ์๋ ๋ฌธ์๋ฅผ ์ ๋ฌํด์ ์คํจํ๊ฒ ๋ง๋ค์ด๋ณด์.
2๏ธโฃ-2) ์คํจ ์์ฒญ ๊ฒฐ๊ณผ
{
"timestamp": "2021-04-20T00:00:00.000+00:00",
"status": 400,
"error": "Bad Request",
"message": "",
"path": "/validation/api/items/add"
}
2๏ธโฃ-3) ์คํจ ์์ฒญ ๋ก๊ทธ
.w.s.m.s.DefaultHandlerExceptionResolver : Resolved
[org.springframework.http.converter.HttpMessageNotReadableException: JSON parse
error: Cannot deserialize value of type `java.lang.Integer` from String "A":
not a valid Integer value; nested exception is
com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize
value of type `java.lang.Integer` from String "A": not a valid Integer value
at [Source: (PushbackInputStream); line: 1, column: 30] (through reference
chain: hello.itemservice.domain.item.Item["price"])]
โฌ๏ธ ์ด ๊ฒฝ์ฐ๋ Item
๊ฐ์ฒด๋ฅผ ๋ง๋ค์ง ๋ชปํ๊ธฐ ๋๋ฌธ์ ์ปจํธ๋กค๋ฌ ์์ฒด๊ฐ ํธ์ถ๋์ง ์๊ณ ๊ทธ ์ ์ ์์ธ๊ฐ ๋ฐ์ํ๋ค.
(+ ๋ฌผ๋ก Validator๋ ์คํ๋์ง ์๋๋ค!)
3๏ธโฃ-1) ๊ฒ์ฆ ์ค๋ฅ ์์ฒญ
POST http://localhost:8080/validation/api/items/add
{"itemName":"hello", "price":1000, "quantity": 10000}
โฌ๏ธ ์๋(quantity
)์ด 10000์ด๋ฉด BeanValidation @Max(9999)
์์ ๊ฑธ๋ฆผ
3๏ธโฃ-2) ๊ฒ์ฆ ์ค๋ฅ ๊ฒฐ๊ณผ
{
"codes": [
"Max.itemSaveForm.quantity",
"Max.quantity",
"Max.java.lang.Integer",
"Max"
],
"arguments": [
{
"codes": [
"itemSaveForm.quantity",
"quantity"
],
"arguments": null,
"defaultMessage": "quantity",
"code": "quantity"
},
9999
],
"defaultMessage": "9999 ์ดํ์ฌ์ผ ํฉ๋๋ค",
"objectName": "itemSaveForm",
"field": "quantity",
"rejectedValue": 10000,
"bindingFailure": false,
"code": "Max"
}
3๏ธโฃ-3) ๊ฒ์ฆ ์์ฒญ ๋ก๊ทธ
API ์ปจํธ๋กค๋ฌ ํธ์ถ
๊ฒ์ฆ ์ค๋ฅ ๋ฐ์, errors=org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'itemSaveForm' on field 'quantity': rejected value [99999]; codes [Max.itemSaveForm.quantity,Max.quantity,Max.java.lang.Integer,Max]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [itemSaveForm.quantity,quantity]; arguments []; default message [quantity],9999]; default message [9999 ์ดํ์ฌ์ผ ํฉ๋๋ค]
โฌ๏ธ ๋ก๊ทธ๋ฅผ ๋ณด๋ฉด ๊ฒ์ฆ ์ค๋ฅ๊ฐ ์ ์ ์ํ๋ ๊ฒ์ ํ์ธํ ์ ์์
1๏ธโฃ @ModelAttribute
@ModelAttribute
๋ ํ๋ ๋จ์๋ก ์ ๊ตํ๊ฒ ๋ฐ์ธ๋ฉ์ด ์ ์ฉ๋๋ค. 2๏ธโฃ @RequestBody
@RequestBody
๋ HttpMessageConverter ๋จ๊ณ์์ JSON ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ฒด๋ก ๋ณ๊ฒฝํ์ง ๋ชปํ๋ฉด ์ดํ ๋จ๊ณ ์์ฒด๊ฐ ์งํ๋์ง ์๊ณ ์์ธ๊ฐ ๋ฐ์ํ๋ค. AMOLAYO......