OrderItem 객체의 유효성 검사
1) price 필드는 1000이상의 값을 갖는다.
2) quantity 필드는 1~1000 사이의 값을 갖는다.
3) 주문 총액(price * quantity)는 2만원 이상이어야 한다.
@Getter
@Setter
@ToString
public class OrderItem {
private String name;
@Min(1000)
private int price;
@Min(1) @Max(1000)
private int quantity;
}
스프링에서 유효성 검사를 자동으로 해주고 실패할 경우 MethodArgumentNotValidException을 발생시키기 때문에 해당 예외를 @ExceptionHandler로 처리하면 될 것 같다고 생각했습니다.
객체 유효성 검사시 발생하는 예외는 하나의 Exception 클래스로 일관되게 처리하려고 합니다.
@Slf4j
@Validated
@RestController
public class OrderController {
@ExceptionHandler
public String handleMethodArgumentNotValidException(
MethodArgumentNotValidException e) {
//bindingResult 객체에 담겨있는 오류 메시지를 담아서 return 하는 것이 바람직하나
//예제를 간단하게 하기 위해 string 반환
return "MethodArgumentNotValidException 발생";
}
@PostMapping("/api/v1/orders")
public String orderV1(@RequestBody @Validated OrderItem orderItem) {
if (orderItem.getPrice() * orderItem.getQuantity() < 20000) {
//MethodArgumentNotValidException 하더라도 bindingResult 객체가 없어
//오류 메시지를 일관된게 처리할 수 없다.
}
return "order-v1-ok";
}
}
@PostMapping("/api/v2/orders")
public String orderV2(@RequestBody @Validated OrderItem orderItem,
BindingResult bindingResult) throws
NoSuchMethodException, MethodArgumentNotValidException {
// 요구사항 3)을 check하는 로직
if (orderItem.getPrice() * orderItem.getQuantity() < 20000) {
bindingResult.reject(null, "수량 * 금액은 2만원을 초과해야합니다.");
}
if (bindingResult.hasErrors()) {
//MethodArgumentNotValidException에 bindingResult를 담아서 throw
throw new MethodArgumentNotValidException(
new MethodParameter(
this.getClass()
.getDeclaredMethod("orderV2",
new Class[] {OrderItem.class, BindingResult.class}), 0),
bindingResult);
}
return "order-v2-ok";
}
public class CustomInvalidException extends RuntimeException {
BindingResult bindingResult;
public CustomInvalidException(BindingResult bindingResult) {
this.bindingResult = bindingResult;
}
public BindingResult getBindingResult() {
return bindingResult;
}
```생략```
}
@ExceptionHandler
public String handleCustomInvalidException(CustomInvalidException e) {
return "handleCustomInvalidException 발생";
}
@PostMapping("/api/v3/orders")
public String orderV3(@RequestBody @Validated OrderItem orderItem,
BindingResult bindingResult) {
if (orderItem.getPrice() * orderItem.getQuantity() < 20000) {
bindingResult.reject(null, "수량 * 금액은 2만원을 초과해야합니다.");
}
if (bindingResult.hasErrors()) {
// 사용자 정의 Exception throw
throw new CustomInvalidException(bindingResult);
}
return "order-v3-ok";
}
OrderItem 객체의 유효성 검사 외에도 API의 @PathVariable 값의 유효성 검사 추가
@Validated //추가
@RestController
public class OrderController {
```생략```
}
@ExceptionHandler
public String handleConstraintViolationException(ConstraintViolationException e){
return "ConstraintViolationException 발생";
}
@PostMapping("/api/v4/orders/{orderId}")
public String orderV4(
@PathVariable @Validated @Length(min = 2, max = 50) String orderId,
@RequestBody @Validated OrderItem orderItem, BindingResult bindingResult) {
if (orderItem.getPrice() * orderItem.getQuantity() < 20000) {
bindingResult.reject(null, "수량 * 금액은 2만원을 초과해야합니다.");
}
if (bindingResult.hasErrors()) {
throw new CustomInvalidException(bindingResult);
}
return "order-v4-ok";
}
사용자 정의 Exception을 정의해서 처리하는 방법과 MethodArgumentNotValidException 을 이용해서 처리하는 방법중 어떤 방법이 더 나은 선택일지 궁금합니다.
요구사항 3)과 같이 비즈니스 로직성 유효성 검사는 어디서 하는게 더 좋을까요?
위 예제와 같이 "수량 x 금액은 2만원을 초과해야한다" 라는 요구사항은 업무 로직이라고 생각합니다.
컨트롤러 계층이 아니라 서비스 계층에서 유효성 검사를 실행하고 실패하면 Exception을 컨트롤러 계층으로 throw하는게 더 좋은 선택일지 궁금합니다.
@PathVariable 유효성 검사시 클래스에 @Validated 어노테이션을 추가하는 이유는 무엇일까요?
Bean
유효성 검사와는 달리 @PathVariable 유효성 검사를 할 땐 클래스 레벨에 @Validated 어노테이션을 추가해야만 했습니다.