DevLog ๐Ÿ˜ถ

[Spring] Bean Validation - ์–ด๋…ธํ…Œ์ด์…˜์„ ํ†ตํ•ด ๊ฒ€์ฆ ์ง„ํ–‰ํ•˜๊ธฐ ๋ณธ๋ฌธ

Back-end/Spring

[Spring] Bean Validation - ์–ด๋…ธํ…Œ์ด์…˜์„ ํ†ตํ•ด ๊ฒ€์ฆ ์ง„ํ–‰ํ•˜๊ธฐ

dolmeng2 2022. 8. 22. 22:58

๊น€์˜ํ•œ ๋‹˜์˜ '์Šคํ”„๋ง MVC 2ํŽธ - ๋ฐฑ์—”๋“œ ์›น ๊ฐœ๋ฐœ ํ™œ์šฉ ๊ธฐ์ˆ '์„ ๋ณด๊ณ  ์ •๋ฆฌํ•œ ๊ธ€์ž…๋‹ˆ๋‹ค ๐Ÿ˜Š

 

์Šคํ”„๋ง MVC 2ํŽธ - ๋ฐฑ์—”๋“œ ์›น ๊ฐœ๋ฐœ ํ™œ์šฉ ๊ธฐ์ˆ  - ์ธํ”„๋Ÿฐ | ๊ฐ•์˜

์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ฐœ๋ฐœ์— ํ•„์š”ํ•œ ๋ชจ๋“  ์›น ๊ธฐ์ˆ ์„ ๊ธฐ์ดˆ๋ถ€ํ„ฐ ์ดํ•ดํ•˜๊ณ , ์™„์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. MVC 2ํŽธ์—์„œ๋Š” MVC 1ํŽธ์˜ ํ•ต์‹ฌ ์›๋ฆฌ์™€ ๊ตฌ์กฐ ์œ„์— ์‹ค๋ฌด ์›น ๊ฐœ๋ฐœ์— ํ•„์š”ํ•œ ๋ชจ๋“  ํ™œ์šฉ ๊ธฐ์ˆ ๋“ค์„ ํ•™์Šตํ•  ์ˆ˜ ์žˆ

www.inflearn.com


- ์ง€๋‚œ ํฌ์ŠคํŒ…๊ณผ ์ด์–ด์ง‘๋‹ˆ๋‹ค :D

 

[Spring] BindingResult๋ฅผ ํ™œ์šฉํ•ด์„œ ๊ฒ€์ฆ ๋กœ์ง ์ถ”๊ฐ€ํ•˜๊ธฐ, MessageCodesResolver ์•Œ์•„๋ณด๊ธฐ

๊น€์˜ํ•œ ๋‹˜์˜ '์Šคํ”„๋ง MVC 2ํŽธ - ๋ฐฑ์—”๋“œ ์›น ๊ฐœ๋ฐœ ํ™œ์šฉ ๊ธฐ์ˆ '์„ ๋ณด๊ณ  ์ •๋ฆฌํ•œ ๊ธ€์ž…๋‹ˆ๋‹ค ๐Ÿ˜Š ์Šคํ”„๋ง MVC 2ํŽธ - ๋ฐฑ์—”๋“œ ์›น ๊ฐœ๋ฐœ ํ™œ์šฉ ๊ธฐ์ˆ  - ์ธํ”„๋Ÿฐ | ๊ฐ•์˜ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ฐœ๋ฐœ์— ํ•„์š”ํ•œ ๋ชจ๋“  ์›น ๊ธฐ์ˆ ์„

cl8d.tistory.com


 

| 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

 

- ๋‹ค์Œ ํฌ์ŠคํŒ…๋ถ€ํ„ฐ๋Š” ๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด์„œ ์•Œ์•„๋ณด์ž!

Comments