๐ŸŒฑ @Valid์™€ @Validated


@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๋งŒ ๊ฐ€๋Šฅ
@Email ์ด๋ฉ”์ผ ํ˜•์‹๋งŒ ๊ฐ€๋Šฅ
@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์„ ๋‹ค๋ฃฌ๊ฑฐ๋ผ ์ด์— ๋Œ€ํ•ด ๊นŠ๊ฒŒ ๊นจ์šฐ์น˜์ง„ ์•Š์•˜์ง€๋งŒ, ๊ต‰์žฅํžˆ ์ข‹์€ ํ•™์Šต์ด์—ˆ๋‹ค.
์•„๋งˆ ์ด ๊ธ€์€ ๋ฏธ์…˜์„ ์ง„ํ–‰ํ•˜๋ฉด์„œ ์ ์ฐจ ์‚ด์ด ๋ถ™์„ ๊ฒƒ ๊ฐ™๋‹ค.
์ผ๋‹จ ์ง€๊ธˆ์€ ์ด๋ฒˆ์— ๊ฒฝํ—˜ํ•œ ๋‚ด์šฉ์„ ์ •๋ฆฌํ•˜๋Š”๋ฐ ์˜์˜๋ฅผ ๋‘”๋‹ค!


์ฐธ๊ณ  ์ž๋ฃŒ