DevLog ๐ถ
[Spring] Bean Validation - ์ด๋ ธํ ์ด์ ์ ํตํด ๊ฒ์ฆ ์งํํ๊ธฐ ๋ณธ๋ฌธ
[Spring] Bean Validation - ์ด๋ ธํ ์ด์ ์ ํตํด ๊ฒ์ฆ ์งํํ๊ธฐ
dolmeng2 2022. 8. 22. 22:58๊น์ํ ๋์ '์คํ๋ง MVC 2ํธ - ๋ฐฑ์๋ ์น ๊ฐ๋ฐ ํ์ฉ ๊ธฐ์ '์ ๋ณด๊ณ ์ ๋ฆฌํ ๊ธ์ ๋๋ค ๐
- ์ง๋ ํฌ์คํ ๊ณผ ์ด์ด์ง๋๋ค :D
| Bean Validation
- ์ด๋ ธํ ์ด์ ์ ํตํด ๊ฒ์ฆ ๋ก์ง์ ์ค์ ํด๋ณด์.
- ์ฐ์ , build.gradle์ ๋ค์์ ์ถ๊ฐํด์ฃผ์.
[SItem.java] - ์์
@Getter
@NoArgsConstructor
@Setter
public class SItem {
private Long id;
@NotBlank
private String itemName;
@NotNull
@Range(min=1000, max=1000000)
private Integer price;
@NotNull
@Max(9999)
private Integer quantity;
...
- @Range์ ๊ฒฝ์ฐ ํ์ด๋ฒ๋ค์ดํธ validation์ธ๋ฐ ์ค๋ฌด์์ ๋ง์ด ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ ํฌ๊ฒ ์ ๊ฒฝ์ฐ์ง ์๊ณ ์ฌ์ฉํด๋ ๋๋ค.
- ๊ทธ์ธ ๋๋จธ์ง๋ javax.validation์ด๋ผ ํน์ ๊ตฌํ์ ๊ด๊ณ์์ด ์ ๊ณต๋๋ค.
[SItemController.java]
@Controller
@RequestMapping("/basic/items")
@RequiredArgsConstructor
public class SItemController {
private final SItemService service;
@GetMapping
public String items(Model model) {
List<SItem> items = service.findAllItem();
model.addAttribute("items", items);
model.addAttribute("userName", "Spring");
return "basic/items";
}
@GetMapping("/{itemId}")
public String item(@PathVariable Long itemId, Model model) {
SItem item = service.findByItemId(itemId);
model.addAttribute("item", item);
return "basic/item";
}
@GetMapping("/add")
public String addForm(Model model) {
model.addAttribute("item", new SItem());
return "basic/addForm";
}
@PostMapping("/add")
public String addItem (@Validated @ModelAttribute("item") SItem item, BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
if (bindingResult.hasErrors()) {
return "basic/addForm";
}
SItem savedItem = service.saveItem(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/basic/items/{itemId}";
}
@GetMapping("/{itemId}/edit")
public String editForm(@PathVariable Long itemId, Model model) {
SItem item = service.findByItemId(itemId);
model.addAttribute("item", item);
return "basic/editForm";
}
@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId, SItem item, Model model) {
service.updateItem(itemId, item);
model.addAttribute("item", item);
return "redirect:/basic/items/{itemId}";
}
@ModelAttribute("regions")
public Map<String, String> regions() {
Map<String, String> regions = new LinkedHashMap<>();
regions.put("SEOUL", "์์ธ");
regions.put("BUSAN", "๋ถ์ฐ");
regions.put("JEJU", "์ ์ฃผ");
return regions;
}
@ModelAttribute("itemTypes")
public SItemType[] itemTypes() {
return SItemType.values();
}
@ModelAttribute("deliveryCodes")
public List<DeliveryCode> deliveryCodes() {
return CreateDeliveryCode.getCodes();
}
@NoArgsConstructor
static class CreateDeliveryCode {
private static final List<DeliveryCode> deliveryCodes
= Arrays.asList(new DeliveryCode("FAST", "๋น ๋ฅธ ๋ฐฐ์ก"),
new DeliveryCode("NORMAL", "์ผ๋ฐ ๋ฐฐ์ก"),
new DeliveryCode("SLOW", "๋๋ฆฐ ๋ฐฐ์ก"));
public static List<DeliveryCode> getCodes() {
return deliveryCodes;
}
}
// ํ
์คํธ ๋ฐ์ดํฐ ์ถ๊ฐ (์์กด๊ด๊ณ ์ฃผ์
์ดํ ์คํ)
@PostConstruct
public void init() {
service.saveItem(new SItem("itemA", 10000, 10));
service.saveItem(new SItem("itemB", 20000, 20));
}
}
- ์ง๋ ํฌ์คํ ์์ ์์ฑํ์๋ WebBinder๋ฅผ ์ด์ฉํ validation ์ถ๊ฐ ์ฝ๋๋ฅผ ์ญ์ ํ์๋ค.
- ์ด๋ ๊ฒ ํด๋ ์ค์ ๋ก๋ ์ ๋์ํ๋ค. ์ด๋ป๊ฒ ์ด๋ฌ๋ ๊ฒ์ผ๊น? (๋จ, ์์ง์ ์ฐ๋ฆฌ๊ฐ ์ง์ ํ ๋ฉ์์ง๋๋ก ๋์ํ์ง๋ ์๋๋ค.)
- ๊ธฐ๋ณธ์ ์ผ๋ก, ์คํ๋ง ๋ถํธ๋ build.gradle์ ์๋ spring-boot-starter-validation ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํ์ธํ๋ฉด Bean Validator๋ฅผ ์๋์ผ๋ก ์ธ์งํ๋ค.
- ์คํ๋ง ๋ถํธ๋ ValidationAutoConfiguration ํด๋์ค๋ฅผ ํตํด์ LocalValidatorFactoryBean์ ๊ธ๋ก๋ฒ Validator๋ก ๋ฑ๋กํ๋ค.
- ๋ํ, ๋ฉ์๋ ํ๋ผ๋ฏธํฐ๋ ๋ฆฌํด ๊ฐ ๊ฒ์ฆ์ ์ํด MethodValidationPostProcessor ์ญ์ ์๋์ผ๋ก ์ค์ ํ๋ค.
@AutoConfiguration
@ConditionalOnClass(ExecutableValidator.class)
@ConditionalOnResource(resources = "classpath:META-INF/services/javax.validation.spi.ValidationProvider")
@Import(PrimaryDefaultValidatorPostProcessor.class)
public class ValidationAutoConfiguration {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
@ConditionalOnMissingBean(Validator.class)
public static LocalValidatorFactoryBean defaultValidator(ApplicationContext applicationContext) {
LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory(applicationContext);
factoryBean.setMessageInterpolator(interpolatorFactory.getObject());
return factoryBean;
}
@Bean
@ConditionalOnMissingBean(search = SearchStrategy.CURRENT)
public static MethodValidationPostProcessor methodValidationPostProcessor(Environment environment,
@Lazy Validator validator, ObjectProvider<MethodValidationExcludeFilter> excludeFilters) {
FilteredMethodValidationPostProcessor processor = new FilteredMethodValidationPostProcessor(
excludeFilters.orderedStream());
boolean proxyTargetClass = environment.getProperty("spring.aop.proxy-target-class", Boolean.class, true);
processor.setProxyTargetClass(proxyTargetClass);
processor.setValidator(validator);
return processor;
}
}
- ์ค์ ์ฝ๋๋ฅผ ๋ณด๋ฉด ๋ค์๊ณผ ๊ฐ์ด @Bean์ ํตํด 2๊ฐ์ง๋ฅผ ์ค์ ํ๋ ๊ฒ์ ๋ณผ ์ ์๋ค.
๐ฉ ์ปจํธ๋กค๋ฌ ํด๋์ค์ @Validated๋ฅผ ๋ถ์ด๊ณ ๊ฐ ๋ฉ์๋์ @RequestParam์ @Min ๊ฐ์ ๊ฒ์ฆ ์ด๋ ธํ ์ด์ ์ ์ค์ ํ๋ฉด,
MethodValidationPostProcessor์ ์ํด์ validation์ด ๊ฐ๋ฅํ๋๋ก ํ๋ก์ ๊ฐ์ฒด๊ฐ ์์ฑ๋๋ค!
โ ๊ฒ์ฆ ์์
- 1์ฐจ์ ์ผ๋ก @ModelAttribute์ ๊ฐ๊ฐ์ ํ๋์ ํ๋ผ๋ฏธํฐ๋ก ๋์ด์จ ๊ฐ์ ๋ฃ์ด ํ์ ๋ณํ์ ์๋ํ๋ค.
- ์คํจ ์ typeMismatch๋ก FieldError๋ฅผ ์ถ๊ฐํ๊ณ , ์ฑ๊ณตํ๋ฉด ๋ค์ ๋ก์ง์ผ๋ก ๋์ด๊ฐ๋ค.
- โญ ์ด๋, ๋ฐ์ธ๋ฉ์ ์ฑ๊ณตํ ํ๋๋ง Bean Validation์ ์ ์ฉํ๋ค.
- @Validated, @Valid๊ฐ ์๋ค๋ฉด ๊ธ๋ก๋ฒ ๊ฒ์ฆ๊ธฐ๋ฅผ ํ์ฉํด์ Validator๋ฅผ ์ฐพ๊ณ ๊ฒ์ฆ์ ์คํํ๋ค. (Bean Validation)
- ๊ทธ๋ ๋ค๋ฉด, ์ฐ๋ฆฌ๊ฐ ์ค์ ํ ๋ฉ์์ง๋ก ์ค๋ฅ ๋ฉ์์ง๊ฐ ๋์ค๋๋ก ํด๋ณด์.
- ๊ธฐ๋ณธ์ ์ผ๋ก, ๋ฉ์์ง ์ฝ๋์ ๊ฒฝ์ฐ ์ ๋ ธํ ์ด์ ์ด๋ฆ + ๊ท์น์ ํตํด ์ค์ ๋๋ค.
@Range
- Range.item.price
- Range.price
- Range.java.lang.Integer
- Range
- ๊ทธ๋ ๊ธฐ ๋๋ฌธ์, ์ฐ๋ฆฌ๊ฐ ๋ฐ๋ก ์ด์ ๊ด๋ จํด์ ํ๋กํผํฐ๋ฅผ ์์ฑํด์ฃผ๋ฉด ๋๋ค.
[errors.properties]
NotBlank=Please enter the {0} as required.
Range={0} range from KRW {1} to KRW {2}.
Max=Up to {1}, {0} are allowed.
[errors_ja.properties]
NotBlank=ๅฟ
่ฆใซๅฟใใฆ{0}ใๅ
ฅๅใใฆใใ ใใใ
Range={0}ใฎ็ฏๅฒใฏ{1}ใฆใฉใณใใ{2}ใฆใฉใณใงใใ
Max=ๆๅคง{1}ใ{0}ใ่จฑๅฏใใใพใใ
- ์ด๋ ๊ฒ ๋๋ฉด, ๋ง๋ค์ด์ง ๋ฉ์์ง ์ฝ๋์ ๋ํด ์์๋๋ก messageSource์์ ์ฐพ๋๋ค.
- ๋ง์ฝ ์์ผ๋ฉด ์ด๋ ธํ ์ด์ ์ message ์์ฑ ๊ฐ์ ์ด์ฉํ๊ฒ ๋๋ค. (@Range(message=""))
- ๊ทธ๋๋ ์์ผ๋ฉด ๊ทธ๋ฅ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ์ ๊ณตํ๋ ๊ธฐ๋ณธ ๊ฐ์ด ๋์จ๋ค.
- ๋ค์๊ณผ ๊ฐ์ด ์ ๋์ํ๋ ๊ฑธ ํ์ธํ ์ ์๋ค.
- ๊ทธ๋ผ, ํน์ ํ๋์ ๋ํ ์ค๋ฅ๊ฐ ์๋ ์ค๋ธ์ ํธ ์ค๋ฅ(ObjectError)์ ๊ฒฝ์ฐ ์ด๋ป๊ฒ ์ฒ๋ฆฌํ ์ ์์๊น?
@ScriptAssert๋ผ๋ ์ด๋ ธํ ์ด์ ์ ์ฌ์ฉํ ์ ์์ง๋ง, ์๋นํ ๋ณต์กํ๊ธฐ ๋๋ฌธ์ ์ฌ๋งํ๋ฉด ์ด ๋ถ๋ถ์ ์๋ฐ ์ฝ๋๋ก ์์ฑํ๋ ๊ฒ์ด ๋ซ๋ค.
[SItemController.java] - ์์
@PostMapping("/add")
public String addItem (@Validated @ModelAttribute("item") SItem 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);
}
}
...
| Bean Validation - ์ ๋ถ ์ ์ฉํ๊ธฐ
- ์ง๊ธ๊น์ง๋ ๋ฑ๋ก ํผ์๋ง ์ ์ฉํ์ง๋ง, ์ด๋ฒ์๋ ์์ ํผ์๋ ์ ์ฉํด์ฃผ์!
[SItemController.java]
@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId,
@Validated @ModelAttribute("item") SItem 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()) {
return "basic/editForm";
}
service.updateItem(itemId, item);
return "redirect:/basic/items/{itemId}";
}
[editForm.html]
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<link href="../css/bootstrap.min.css"
th:href="@{/css/bootstrap.min.css}"
rel="stylesheet">
<title>Item Edit Form</title>
<style>
.field-error{
border-color:#dc3545;
color:#dc3545;
}
</style>
</head>
<body>
<div class="container">
<div class="py-5 text-center">
<h2 class="font-monospace fw-bold"
th:text="#{page.updateItem}">Item Edit Form</h2>
</div>
<form action="item.html" th:action th:object="${item}" method="post">
<div th:if="${#fields.hasGlobalErrors()}">
<p class="field-error text-center fw-bold"
th:each="err: ${#fields.globalErrors()}"
th:text="${err}">
์ ์ฒด ์ค๋ฅ ๋ฉ์์ง</p>
</div>
<div class="row fw-bold font-monospace">
<div class="col text-center">
<label for="id"
th:text="#{label.item.id}">Id</label>
<input type="text"
th:field="*{id}"
class="form-control text-center"
readonly>
</div>
<div class="col text-center">
<label for="itemName"
th:text="#{label.item.itemName}">Name</label>
<input type="text"
th:field="*{itemName}"
th:errorclass="field-error"
class="form-control text-center"
placeholder="Please Enter an Item Name."
>
<div class="field-error"
th:errors="*{itemName}">
์ํ๋ช
์ค๋ฅ
</div>
</div>
</div>
<div class="row mt-2 fw-bold font-monospace">
<div class="col text-center">
<label for="price"
th:text="#{label.item.price}">Price</label>
<input type="text"
th:field="*{price}"
th:errorclass="field-error"
class="form-control text-center"
placeholder="Please Enter an Item Price."
>
<div class="field-error"
th:errors="*{price}">
๊ฐ๊ฒฉ ์ค๋ฅ
</div>
</div>
<div class="col text-center">
<label for="quantity"
th:text="#{label.item.quantity}">Quantity</label>
<input type="text"
th:field="*{quantity}"
th:errorclass ="field-error"
class="form-control text-center"
placeholder="Please Enter an Item Quantity."
>
<div class="field-error"
th:errors="*{quantity}">
์๋ ์ค๋ฅ
</div>
</div>
</div>
...
</form>
</div>
</body>
</html>
| Bean Validation - ํ๊ณ์
- ๋ฐ์ดํฐ๋ฅผ ๋ฑ๋กํ ๋์ ์์ ํ ๋์ ์๊ตฌ์ฌํญ์ด ๋ค๋ฅด๋ค๋ฉด ์ด๋ป๊ฒ ์ฒ๋ฆฌํด์ผ ํ ๊น?
- ์์ ํ ๋๋ ์๋์ ๋ฌด์ ํ์ผ๋ก ์ฒดํฌํ ์ ์๋๋ก ๋ณ๊ฒฝํด๋ณด์.
- ๋ํ, ์์ ์์๋ id ๊ฐ์ด ํ์๋ก ๋ค์ด๊ฐ๋ค.
์ด๋ฅผ ์ํด Bean Validation์ groups ๊ธฐ๋ฅ์ ์ฌ์ฉํ ์ ์๋ค.
๋ฑ๋ก์ฉ ์ธํฐํ์ด์ค์ ์์ ์ฉ ์ธํฐํ์ด์ค๋ฅผ ๋ฐ๋ก ์์ฑํ์ฌ ๊ฒ์ฆ ์ด๋ ธํ ์ด์ ์ ๊ทธ๋ฃน ์ต์ ์ ์ฌ์ฉํ๋ ๊ฒ์ด๋ค.
ex)
๋๋ฉ์ธ ํ๋ - @NotNull(groups=UpdateChecking.class)
์ปจํธ๋กค๋ฌ - @Validated(UpdateChecking.class)
cf) ์ด๋, @Valid๋ groups ๊ธฐ๋ฅ์ด ์๊ณ @Validated๋ง ์ฌ์ฉ ๊ฐ๋ฅํ๋ค!
๊ทธ๋ฌ๋, ์ด ๋ฐฉ๋ฒ๋ณด๋ค๋ ๋ฐ์ดํฐ ์ ๋ฌ์ ์ํ ๋ณ๋์ ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์ ๋ง์ด ์ฌ์ฉํ๋ค.
์๋๋ฉด, ๋ณดํต์ ๋๋ฉ์ธ ๊ฐ์ฒด๋ฅผ ๊ทธ๋๋ก ์ฌ์ฉํ๋ฉด ์์ฑ/์์ ์ ๊ฒ์ฆ๋ groups๋ฅผ ์ฌ์ฉํด์ผ ํ๊ณ , ๊ฐ๋จํ ๊ฒฝ์ฐ๋ง ๊ฐ๋ฅํ๊ฒ ๋๋ค.
๋ณดํต, DTO๋ฅผ ํตํด์ ์ด๋ฌํ ์ ๋ณด๋ฅผ ๋ง์ด ๊ด๋ฆฌํ๋ฉฐ, ์ด๋ฆ์ ๊ฐ ์ญํ ์ ๋ํ๋ผ ์ ์๋๋ก '์๋ฏธ์๊ฒ' ์ฌ์ฉํ๋ฉด ๋๋ค.
- ์ฐ๋ฆฌ์ ์ฝ๋๋ฅผ ์์ ํด๋ณด์. ์ฐ์ , ๊ธฐ์กด์ SItem์์ ์ ์ฉํ์๋ ๊ฒ์ฆ ์ด๋ ธํ ์ด์ ๋ค์ ๋ค ์ญ์ ํ๋ค.
[SItem.java]
@Getter
@NoArgsConstructor
@Setter
public class SItem {
private Long id;
private String itemName;
private Integer price;
private Integer quantity;
private Boolean open;
private List<String> regions;
private SItemType itemType;
private String deliveryCode;
public SItem(String itemName, Integer price, Integer quantity, Boolean open, List<String> regions, SItemType itemType, String deliveryCode) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
this.open = open;
this.regions = regions;
this.itemType = itemType;
this.deliveryCode = deliveryCode;
}
public void changeId(Long id) {
this.id = id;
}
public void changeItemInfo(String itemName, Integer price, Integer quantity,
Boolean open, List<String> regions, SItemType itemType,
String deliveryCode) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
this.open = open;
this.regions = regions;
this.itemType = itemType;
this.deliveryCode = deliveryCode;
}
}
- ๋ฑ๋ก, ์์ ์ฉ๋ DTO ๊ฐ์ฒด๋ฅผ ์๋กญ๊ฒ ๋ง๋ ๋ค.
- Setter๋ฅผ ์ญ์ ํ์์ผ๋ฉฐ, ์์ฑ์ ํ๋ผ๋ฏธํฐ๋ฅผ ์์ ํ์๋ค.
[SItemSaveRequest.java]
@Data
@AllArgsConstructor
public class SItemSaveRequest {
@NotBlank
private String itemName;
@NotNull
@Range(min = 1000, max = 1000000)
private Integer price;
@NotNull
@Max(value = 9999)
private Integer quantity;
@NotNull
private Boolean open;
@NotNull
private List<String> regions;
@NotNull
private SItemType itemType;
@NotNull
private String deliveryCode;
}
[SItemUpdateRequest.java]
@Data
public class SItemUpdateRequest {
@NotNull
private Long id;
@NotBlank
private String itemName;
@NotNull
@Range(min = 1000, max = 1000000)
private Integer price;
private Integer quantity;
@NotNull
private Boolean open;
@NotNull
private List<String> regions;
@NotNull
private SItemType itemType;
@NotNull
private String deliveryCode;
}
[SItemRepository.java] - ์ผ๋ถ ์์
public void update(Long itemId, String itemName, Integer price, Integer quantity,
Boolean open, String deliveryCode, List<String> regions, SItemType itemType) {
SItem findItem = store.get(itemId);
findItem.changeItemInfo(itemName, price, quantity, open, regions, itemType, deliveryCode);
}
- Dto๊ฐ ๋ ํ์งํ ๋ฆฌ ๋จ๊ณ๊น์ง ๋ด๋ ค๊ฐ๋ ๊ฑด ๋ณ๋ก ์ข์ง ์๊ธฐ ๋๋ฌธ์ ์ด๋ฐ ์์ผ๋ก ๋ฐ๊ฟ์ฃผ์๋ค.
[SItemService.java] - ์ผ๋ถ ์์
public SItem saveItem(SItemSaveRequest itemDto) {
SItem item = new SItem(itemDto.getItemName(), itemDto.getPrice(), itemDto.getPrice(), itemDto.getOpen(),
itemDto.getRegions(), itemDto.getItemType(), itemDto.getDeliveryCode());
return repository.save(item);
}
public void updateItem(Long itemId, SItemUpdateRequest item) {
repository.update(itemId, item.getItemName(), item.getPrice(), item.getQuantity(),
item.getOpen(), item.getDeliveryCode(), item.getRegions(), item.getItemType());
}
[SItemController.java]
@Controller
@RequestMapping("/basic/items")
@RequiredArgsConstructor
public class SItemController {
private final SItemService service;
...
@PostMapping("/add")
public String addItem (@Validated @ModelAttribute("item") SItemSaveRequest 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 "basic/addForm";
}
SItem savedItem = service.saveItem(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/basic/items/{itemId}";
}
...
@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId,
@Validated @ModelAttribute("item") SItemUpdateRequest 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()) {
return "basic/editForm";
}
service.updateItem(itemId, item);
return "redirect:/basic/items/{itemId}";
}
...
// ํ
์คํธ ๋ฐ์ดํฐ ์ถ๊ฐ (์์กด๊ด๊ณ ์ฃผ์
์ดํ ์คํ)
@PostConstruct
public void init() {
service.saveItem(new SItemSaveRequest("itemA", 10000, 10, true, null, SItemType.BOOK, null));
service.saveItem(new SItemSaveRequest("itemB", 20000, 20, false, null, SItemType.FOOD, null));
}
}
- ๋ฑ๋ก์ ๋ํด์๋ ItemSaveRequest๋ฅผ, ์์ ์ ๋ํด์๋ ItemUpdateRequest๋ฅผ ์ฌ์ฉํด์ฃผ์๋ค.
- ๋ชจ๋ ๋งค์ฐ ์ ๋์ํ๋ ๊ฒ์ ํ์ธํ ์ ์๋ค!
| Bean Validation - HTTP Message Converter
- @Valid, @Validated๋ @RequestBody์๋ ์ ์ฉ์ด ๊ฐ๋ฅํ๋ค.
cf) @RequestBody๋ HTTP Body์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ฒด๋ก ๋ณํํ ๋ ์ฌ์ฉ.
- ์ด ๊ฒฝ์ฐ, ํ์ ์ค๋ฅ ๋ฐ์ ์ ์ ์ด๋ถํฐ 'SItemSaveRequest ๊ฐ์ฒด๋ก ๋ณํ'ํ๋ ๊ฒ ์์ฒด๊ฐ ์คํจํ๊ธฐ ๋๋ฌธ์, ์ปจํธ๋กค๋ฌ ์์ฒด๊ฐ ํธ์ถ๋์ง ์๋๋ค.
- ๊ฒ์ฆ ์ค๋ฅ๋ผ๋ฉด (์๋์ด 9999๊ฐ ์ด์ ๊ฐ์ ์ค๋ฅ) ์ปจํธ๋กค๋ฌ๊ฐ ์คํ๋๋ฉฐ ๊ฒ์ฆ ์ค๋ฅ ์์ฒด๊ฐ ์ ์คํ๋๋ค.
โ @ModelAttribute vs @RequestBody
- HttpMessageConverter์ ๊ฒฝ์ฐ ์ ์ฒด ๊ฐ์ฒด ๋จ์๋ก ์ ์ฉ๋๊ธฐ ๋๋ฌธ์, ๊ฐ์ฒด ์์ฑ์ด ๋์ด์ผ @Valid, @Validated๊ฐ ์ ์ฉ๋๋ค.
- @ModelAttribute๋ ๊ฐ๊ฐ์ ํ๋ ๋จ์๋ก ๋ฐ์ธ๋ฉ์ด ์ ์ฉ๋๋ค. ๊ทธ๋์ ํน์ ํ๋๊ฐ ๋ฐ์ธ๋ฉ ์ ๋์ด๋ ๋ค๋ฅธ ํ๋๋ ์ ๋ฐ์ธ๋ฉ๋๋ค.
๐ฉ ์ฌ๋ฌ ๊ฐ์ง ์ฐจ์ด์
@ModelAttribute : Http ํ๋ผ๋ฏธํฐ, Http Body ๋ด์ฉ์ Setter๋ฅผ ํตํด 1:1๋ก ๊ฐ์ฒด์ ๋ฐ์ธ๋ฉ
- ๋ฐ์ธ๋ฉ ์ ๊ฒ์ฆ ์์ ์งํ
- Http Body๋ multipart/form-data ํํ์ด๋ค.
@RequestBody : Http Body์ application/json ํํ๋ก ๋ฐ์ดํฐ๋ฅผ ์ ์กํ๋ฉด ํด๋น ๋ด์ฉ์ ๊ฐ์ฒด๋ก ๋ณํํด์ฃผ๋ ์ญํ ์ํ
- ๊ฐ์ฒด ๋ณํ์ MappingJackson2HttpMessageConverter ์ด์ฉ
- Body ๋ด์ฉ์ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ ๊ฐ์ฒด์ @Setter ์์ด๋ ๋ฌด๊ด, POST์์๋ง ์ฌ์ฉ ๊ฐ๋ฅ (GET์ header ๊ฐ ์ฌ์ฉ)
@RequestParam : HTTP ์์ฒญ ํ๋ผ๋ฏธํฐ๋ฅผ @RequestParam์ด ์ฐ์ธ ๋ณ์๋ก mapping
- ๋ค์ ํฌ์คํ ๋ถํฐ๋ ๋ก๊ทธ์ธ ์ฒ๋ฆฌ ๋ฐฉ๋ฒ์ ๋ํด์ ์์๋ณด์!