@Valid์ @Validated
์๋น์ค ๊ทผ๋ก์์ ์ฅ๋ฐ๊ตฌ๋ ๋ฏธ์
API๋ฅผ ๋ง๋ค๋ฉฐ ์์ฒญ์ผ๋ก ๋ค์ด์จ DTO์ ๊ฐ์ ๊ฒ์ฆํ๋ ๋ฐฉ๋ฒ์ ๊ณ ๋ฏผํ๋ค๊ฐ, Spring Validation์ ์ฌ์ฉํด๋ณด๊ฒ ๋์๋ค.
์ด๋ฒ์๋ DTO์ ํ๋์ ์ ์ฝ์ ๊ฑธ์ด์ฃผ๊ณ ์ปจํธ๋กค๋ฌ์์ ๊ฒ์ฆ์ ํด์ฃผ์๋๋ฐ, ์๋กญ๊ฒ ๋ฐฐ์ด ๋ด์ฉ์ด๋ ์ด๋ฅผ ์ ๋ฆฌํด๋ณด๋ ค ํ๋ค.
์ฌ์ค ์ฌ๋ฐ๋ฅด๊ฒ ์ฌ์ฉํ ๊ฒ์ธ์ง๋ ํ์ ์ผ ์์ผ๋, ์ด๋ฐ ๊ฒ๋ ์๊ตฌ๋ ๋ค๋ค๋ณด๋ฉด์ ์ฌ๋ฌ ์ํ์ฐฉ์ค๋ฅผ ๊ฒช์๊ธฐ์ ์ข ๋ ๊ณต๋ถํ๋ฉด์ ์ ๋ฆฌํด์ผ์ง ๐
Dependency ์ถ๊ฐ - gradle
implementation 'org.springframework.boot:spring-boot-starter-validation'
@Valid
์ด๋ฒ์ ์ฐ๋ฆฌ๊ฐ ์ฌ์ฉํ ์์๋ฅผ ๋ณด๋ฉฐ ํ๋์ฉ ์ ๋ฆฌํ์.
ProductController
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductService productService;
public ProductController(final ProductService productService) {
this.productService = productService;
}
@PostMapping
public ResponseEntity<Void> add(@Validae @RequestBody final ProductDto productDto) {
final Long productId = productService.addProduct(productDto);
final URI uri = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/" + productId)
.build().toUri();
return ResponseEntity.created(uri).build();
}
// ...
}
์ปจํธ๋กค๋ฌ์์ @RequestBody๋ฅผ ํตํด DTO์ ๋งคํ์ ํ ๋ ๊ฒ์ฆ์ ์งํํ ๊ณณ์ @Valid
๋ฅผ ๋ถ์ฌ์ค๋ค.
ํด๋น ์์ฒญ์ด ๋ค์ด์ ๋ฉ์๋๊ฐ ์คํ๋ ์ ์ ํจ์ฑ ๊ฒ์ฌ๋ฅผ ์งํํ๋ค.
๋ง์ฝ ๊ฒ์ฆ์ ์คํจํ ๊ฒฝ์ฐ MethodArgumentNotValidException
๋ฅผ ๋์ง๋ค.
ProductDto
public class ProductDto {
@NotNull
private Long productId;
@NotBlank
private String name;
@Min(value = 0, message = "๊ธ์ก์ ์์์ผ ์ ์์ต๋๋ค.")
private Integer price;
@NotBlank
private String imageUrl;
public ProductDto() {
}
// ...
}
DTO์ ์ฌ์ฉ๋ ์ด๋ ธํ ์ด์ ๊ณผ ์ข ๋ ์ฐพ์๋ณธ ์ ์ฝ ์กฐ๊ฑด ์ด๋ ธํ ์ด์ ์ ์ ๋ฆฌํด๋ณด๋ฉดโฆ โ๏ธ
@NotNull
: ๋ชจ๋ ๋ฐ์ดํฐ ํ์ ์ ๋ํด null์ ํ์ฉํ์ง ์๋๋ค.@NotEmpty
: null๊ณผ โโ๋ฅผ ํ์ฉํ์ง ์๋๋ค. (ํ์ - String, Collection. Map, Array)@NotBlack
: null๊ณผ โโ, โ โ(๋น ๊ณต๋ฐฑ ๋ฌธ์์ด)์ ํ์ฉํ์ง ์๋๋ค.@Min(์ซ์)
/@Max(์ซ์)
: ์ต์, ์ต๋ ๊ฐ์ ๊ฒ์ฆํ๋ค.
๋ํ ์ ์ฝ ์กฐ๊ฑด ์ด๋ ธํ ์ด์ ์ ์์ฑ๋ค๋ก ์์ธ๋ก ๋์ ธ์ค message ๋ฑ์ ์ต์ ์ ์ค์ ํ ์ ์๋ค.
์ ์ฝ ์กฐ๊ฑด ์ด๋ ธํ ์ด์
Anotation | ์ ์ฝ ์กฐ๊ฑด |
---|---|
@NotNull | ๋ชจ๋ ๋ฐ์ดํฐ ํ์ ์ ๋ํด null์ ํ์ฉํ์ง ์๋๋ค. |
@NotEmpty | null๊ณผ โโ๋ฅผ ํ์ฉํ์ง ์๋๋ค. (ํ์ - String, Collection. Map, Array) |
@NotBlank | null๊ณผ โโ, โ โ(๋น ๊ณต๋ฐฑ ๋ฌธ์์ด)์ ํ์ฉํ์ง ์๋๋ค. |
@Null | Null๋ง ์ ๋ ฅ ๊ฐ๋ฅ |
@Size(min=,max=) | ๋ฌธ์์ด, ๋ฐฐ์ด๋ฑ์ ํฌ๊ธฐ ๊ฒ์ฆ |
@Pattern(regex=) | ์ ๊ท์ ๊ฒ์ฆ |
@Max(์ซ์) | ์ต๋๊ฐ ๊ฒ์ฆ |
@Min(์ซ์) | ์ต์๊ฐ ๊ฒ์ฆ |
@Future | ํ์ฌ ๋ณด๋ค ๋ฏธ๋์ธ์ง ๊ฒ์ฆ |
@Past | ํ์ฌ ๋ณด๋ค ๊ณผ๊ฑฐ์ธ์ง ๊ฒ์ฆ |
@Positive | ์์๋ง ๊ฐ๋ฅ |
@PositiveOrZero | ์์์ 0๋ง ๊ฐ๋ฅ |
@Negative | ์์๋ง ๊ฐ๋ฅ |
@NegativeOrZero | ์์์ 0๋ง ๊ฐ๋ฅ |
์ด๋ฉ์ผ ํ์๋ง ๊ฐ๋ฅ | |
@Digits(integer=, fraction = ) | ๋์ ์๊ฐ ์ง์ ๋ ์ ์์ ์์ ์๋ฆฌ ์ ๋ณด๋ค ์์์ง ๊ฒ์ฆ |
@DecimalMax(value=) | ์ง์ ๋ ์ค์ ์ดํ์ธ์ง ๊ฒ์ฆ |
@DecimalMin(value=) | ์ง์ ๋ ์ค์ ์ด์์ธ์ง ๊ฒ์ฆ |
@AssertFalse | false ์ธ์ง ๊ฒ์ฆ |
@AssertTrue | true ์ธ์ง ๊ฒ์ฆ |
๊ทธ๋ฐ๋ฐ ์ฐ๋ฆฌ๋ ์ด๋ค ์์ฒญ์์๋ id๊ฐ๋ง ์ ์ฝ์กฐ๊ฑด์ ๊ฑธ๊ณ , ์ด๋ค ์์ฒญ์์๋ ๋ชจ๋ ํ๋์ ๋ํ ์ ์ฝ์กฐ๊ฑด์ ๊ฑธ๊ณ ์ถ์๋ค.
๋น์ฐํ๊ฒ๋(?) ์ ์ฝ์กฐ๊ฑด์ ๋ํด ๊ทธ๋ฃนํ์ ํ ์ ์๋ ๋ฐฉ๋ฒ๋ ์์๋ค!
@Validated
์ ์ฝ์กฐ๊ฑด์ ๋ํ ๊ทธ๋ฃน์ ๋ง๋ค์ด ์ ์ฉ์ํฌ ์ ์๋ค.
ํน์ Validation ๊ทธ๋ฃน์ผ๋ก ๊ฒ์ฆํ๊ธฐ ์ํด์๋ Group ์ธํฐํ์ด์ค๋ฅผ ์์ฑํ๊ณ ์ด ์์ ๊ทธ๋ฃน์ ๋ํ ์ธํฐํ์ด์ค๋ฅผ ์ ์ํ๋ค.
Request
public interface Request {
interface id {
}
interface allProperties {
}
}
์ด์ ์์ฒญ๋ง๋ค id, or ๋ชจ๋ ํ๋์ ๋ํ ์ ์ฝ ์กฐ๊ฑด์ ๊ฒ์ฌํ๊ณ ์ถ์ ๋๋ฅผ ๋๋ ๊ทธ๋ฃน์ ์ ์ํ๋ค.
ProductDto
public class ProductDto {
@NotNull(groups = Request.id.class)
private Long productId;
@NotBlank(groups = Request.allProperties.class)
private String name;
@Min(value = 0, message = "๊ธ์ก์ ์์์ผ ์ ์์ต๋๋ค.", groups = Request.allProperties.class)
private Integer price;
@NotBlank(groups = Request.allProperties.class)
private String imageUrl;
public ProductDto() {
}
// ...
}
์์ฑ ์ ์ฝ์กฐ๊ฑด ์ด๋ ธํ ์ด์ ์ ์ต์ groups์ ๊ทธ๋ฃน์ ์ง์ ํด์ค๋ค.
ProductController
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductService productService;
public ProductController(final ProductService productService) {
this.productService = productService;
}
// ...
@PostMapping
public ResponseEntity<Void> add(@Validated(Request.allProperties.class) @RequestBody final ProductDto productDto) {
final Long productId = productService.addProduct(productDto);
final URI uri = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/" + productId)
.build().toUri();
return ResponseEntity.created(uri).build();
}
// ...
}
CartItemController
@RestController
@RequestMapping("/api/customers/{customerName}/carts")
public class CartItemController {
private final CartService cartService;
public CartItemController(final CartService cartService) {
this.cartService = cartService;
}
// ...
@PostMapping
public ResponseEntity<Void> addCartItem(@Validated(Request.id.class) @RequestBody final ProductDto productDto,
@PathVariable final String customerName) {
final Long newId = cartService.addCart(productDto.getProductId(), customerName);
final URI uri = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{cartId}")
.buildAndExpand(newId)
.toUri();
return ResponseEntity.created(uri).build();
}
// ...
}
@RequestBody
์์ @Validated
๋ฅผ ์ ์ธํ๊ณ ๊ดํธ๋ฅผ ์ด์ด ์ํ๋ ๊ทธ๋ฃน์ ๋ฃ์ด์ค๋ค.
์ปฌ๋ ์ @Valid ?
OrderDetailDto
public class OrderDetailDto {
@NotNull(groups = Request.allProperties.class)
private Long productId;
private Long cartId;
private int price;
private String name;
private String imageUrl;
@Min(value = 0, groups = Request.allProperties.class)
private int quantity;
public OrderDetailDto() {
}
// ...
}
์ฐ๋ฆฌ๊ฐ ๊ตฌํํ๋ค ๋ฌธ์ ๊ฐ ๋ ๋ถ๋ถ์ ๋ฐ๋ก ์์ ๊ฐ์ OrderDetailDto
์ ์ปฌ๋ ์
์ธ List<OrderDetailDto> orderDetailRequestDtos
๋ก ๋ค์ด์ค๋ ๊ฐ์ ๊ฒ์ฆํ๊ณ ์ถ์๋๋ฐ,
์ปฌ๋ ์
์ ์ํ ๊ฐ์ฒด๋ ์์ฑ ์ ์ฝ ์กฐ๊ฑด์ด ๊ฒ์ฆ๋์ง ์๊ณ ๊ทธ๋ฅ ํต๊ณผ๋์ด ๋ฒ๋ฆฌ๋ ๊ฒ์ด์๋ค.
@RestController
@RequestMapping("/api/customers/{customerName}/orders")
public class OrderController {
private final OrderService orderService;
public OrderController(final OrderService orderService) {
this.orderService = orderService;
}
@PostMapping
public ResponseEntity<Void> addOrder(@PathVariable final String customerName,
@RequestBody @Valid final List<OrderDetailDto> orderDetailRequestDtos) {
long orderId = orderService.addOrder(orderDetailRequestDtos, customerName);
return ResponseEntity.created(
URI.create("/api/" + customerName + "/orders/" + orderId)).build();
}
// ...
}
์โ
์ด์ ๋@Valid
๋ JSR-303์ ์ด๋
ธํ
์ด์
์ด๊ณ JSR-303์ JavaBeans์ ์ ์ฉ๋๋๋ฐ, List๋ JavaBeans๊ฐ ์๋๊ธฐ ๋๋ฌธ์ด๋ผ๊ณ ํ๋ค.
์ฐ๋ฆฌ๋ Collection DTO๋ฅผ ๊ฐ์ธ๋ ๋๋ค๋ฅธ DTO ๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด์ผํ๋โฆ ํ์ผ๋, ํด๋์ค ๋จ์ @Validated
์ ๋ถ์ฌ ํด๊ฒฐํ ์ ์์๋ค.
์์ธ
์ฌ๊ธฐ์ ์ฃผ์ํ ์ ์ด ์๋ค. ๋ฐ๋ก ์์ธ์ ๊ดํ ๋ถ๋ถ์ธ๋ฐ,
@Valid
๋ ๊ฒ์ฆ์ ์คํจํ๋ฉด MethodArgumentNotValidException
๋ฅผ ๋์ง๊ณ ,
ํด๋์ค ๋จ์ ๋ถ์ @Validated
๋ ๊ฒ์ฆ์ ์คํจํ๋ฉด ConstraintViolationException
๋ฅผ ๋์ง๋ค.
์ง๊ธ์ผ๋ก์จ๋ ๊ฐ ์์ธ๋ฅผ ์ก์์ ํธ๋ค๋ง ํด์ฃผ์๋ค.
๊ทผ๋กํ๋ฉด์ Spring Validation์ด๋ผ๋ ๊ฒ์ ์ฒ์์จ๋ด์ ์ฌ๋ฌ ์๋๋ค์ ํด๋ณด์๊ณ , ๋๋ถ์ ์ด๋ฐ๊ฒ๋ ์๊ตฌ๋๋ฅผ ๊นจ๋ฌ์ ์ ์์๋ค.
ํนํ ์ปฌ๋ ์
์ ๋ํ ๊ฒ์ฆ์์ ์์ด๋ฌ์ง๋ฅผ ๊ณ ๋ฏผํ๋ฉฐ ์ด์ ๋ฅผ ์ฐพ์๊ฐ์๊ณ ๋๋ถ์ ์ ๋ง ๋ง์ด ๋ฐฐ์ ๋ค.
์ฌ์ค ์ฅ๋ฐ๊ตฌ๋ API ๊ตฌํ์์ ์ด์ง Spring Validation์ ๋ค๋ฃฌ๊ฑฐ๋ผ ์ด์ ๋ํด ๊น๊ฒ ๊นจ์ฐ์น์ง ์์์ง๋ง, ๊ต์ฅํ ์ข์ ํ์ต์ด์๋ค.
์๋ง ์ด ๊ธ์ ๋ฏธ์
์ ์งํํ๋ฉด์ ์ ์ฐจ ์ด์ด ๋ถ์ ๊ฒ ๊ฐ๋ค.
์ผ๋จ ์ง๊ธ์ ์ด๋ฒ์ ๊ฒฝํํ ๋ด์ฉ์ ์ ๋ฆฌํ๋๋ฐ ์์๋ฅผ ๋๋ค!