๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
BE/๐Ÿƒ Spring

[Spring Boot] RestControllerAdvice๋กœ Validation Exception ํ•ธ๋“ค๋งํ•˜๊ธฐ

by ํ‹ด๋”” 2024. 11. 9.
๋ฐ˜์‘ํ˜•

request ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ์„ ์œ„ํ•ด Valid, Validation, Spring Boot์—์„œ ์ œ๊ณตํ•˜๋Š” ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•œ ๊ฒฝ์šฐ Exception์ด ๋ฐœ์ƒํ•œ๋‹ค. ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋ฐ์ดํ„ฐ๋ฅผ ์š”์ฒญํ–ˆ์œผ๋‚˜ ์•„๋ฌด๋Ÿฐ ์•ˆ๋‚ด ์—†์ด ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค๋ฉด ์—๋Ÿฌ์˜ ์›์ธ์„ ์•Œ ์ˆ˜ ์—†๋‹ค. Response์— ์—๋Ÿฌ์˜ ์›์ธ์„ ์ •ํ•ด์ง„ ๊ทœ๊ฒฉ์— ๋งž๊ฒŒ ์ œ๊ณตํ•˜์—ฌ ํด๋ผ์ด์–ธํŠธ ์ž‘์—…์„ ๋•๊ณ , ๋ฐ์ดํ„ฐ๋ฅผ ๊ฒ€์ฆํ•˜์—ฌ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

 

 RestControllerAdvice

  • Spring MVC์—์„œ ์ „์—ญ์ ์œผ๋กœ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋ฅผ ํ•˜๋Š” ์• ๋„ˆํ…Œ์ด์…˜
  • @ControllerAdvice์™€ ๋‹ค๋ฅธ ์ ์€ @ResponseBody๋ฅผ ํฌํ•จํ•˜๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— JSON ํ˜•์‹์˜ response๋ฅผ ๋ฐ˜ํ™˜ํ•จ
  • ๊ธ€๋กœ๋ฒŒ ์˜ˆ์™ธ์ฒ˜๋ฆฌ, ์‚ฌ์šฉ์ž ์ •์˜ ํ˜•์‹์˜ JSON ๊ตฌ์กฐ ๋ฐ˜ํ™˜ํ•˜๋Š”๋ฐ ์‚ฌ์šฉ
  • basePackages, basePackageClasses, annotations ์†์„ฑ์œผ๋กœ ํŠน์ • ์ปจํŠธ๋กค๋Ÿฌ, ํŒจํ‚ค์ง€์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์˜ˆ์™ธ๋งŒ ์ฒ˜๋ฆฌํ•˜๊ฒŒ ํ•  ์ˆ˜ ์žˆ๋‹ค
  • @Order ์• ๋„ˆํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•ด์„œ controller advice๊ฐ„์˜ ์ˆœ์„œ๋ฅผ ์ •ํ•  ์ˆ˜๋„ ์žˆ๋‹ค.
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

}
  • gloabl exception handler์— RestControllerAdvice์ถ”๊ฐ€
@Builder
@Getter
public class ErrorResponse {
    private int code;
    private String msg;

    @Builder
    public ErrorResponse(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }
}

 

  • Response๋กœ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•œ ๊ฐ์ฒด ์ถ”๊ฐ€

์˜ˆ์ œ ์ฝ”๋“œ

Valid, Validation exception ๊ด€๋ จ ์„ค๋ช…์€

 

[Validation] spring boot validation ์‚ฌ์šฉ๋ฒ•๊ณผ ์ข…๋ฅ˜

ValidationSpring Boot์—์„œ๋Š” Validation์„ ํ†ตํ•ด ํด๋ผ์ด์–ธํŠธ์—์„œ ์š”์ฒญ์‹œ ์ „์†กํ•œ ๋ฐ์ดํ„ฐ์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•˜๊ธฐ ์œ„ํ•œ ๊ธฐ๋Šฅ๋“ค์„ ์ œ๊ณตํ•œ๋‹ค. ์™œ Validation์„ ์‚ฌ์šฉํ• ๊นŒ์ฃผ๋กœ Controller์—์„œ ํด๋ผ์ด์–ธํŠธ๋กœ ๋ถ€ํ„ฐ ์ „์†ก

youbidan-project.tistory.com

์ด์ „ ๊ธ€์„ ์ฐธ๊ณ ํ•˜์„ธ์š”!

@Getter
@ToString
@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class)
public class RegistBookRequest {
    @Size(min = 1, max = 20, message = "์ฑ…์˜ ์ด๋ฆ„์€ 1~20 ์ž๋ฆฌ๋กœ ๋“ฑ๋กํ•ด ์ฃผ์„ธ์š”.")
    private String bookName;

    @NotBlank(message = "์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’์€ ํ•„์ˆ˜ ์ž…๋‹ˆ๋‹ค.")
    private String category;

    @Past(message = "๋ฐœํ–‰์ผ์€ ๊ณผ๊ฑฐ๋‚ ์งœ๋งŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ")
    private LocalDate issuedDay;

    @Max(value = 100, message = "์ตœ๋Œ€๋กœ ๋“ฑ๋กํ•  ์ˆ˜ ์žˆ๋Š” ์ˆ˜๋Ÿ‰์€ 100๊ฐœ ์ž…๋‹ˆ๋‹ค.")
    private int amount;
}
@RestController
@RequestMapping("/api/v1/book")
public class BookApiController {
    @PostMapping
    public String regist(
            @RequestBody @Valid RegistBookRequest request
    ) {
        return request.toString();
    }

    @GetMapping("/buy")
    public String buy(
            @RequestParam(name = "book_name") @Size(min = 3, max = 20, message = "์ฑ…์˜ ์ด๋ฆ„์€ 3~20 ์ž๋ฆฌ๋กœ ๋“ฑ๋กํ•ด ์ฃผ์„ธ์š”.")
            String bookName,
            @RequestParam(name = "amount") @Max(value = 10, message = "ํ•œ ๋ฒˆ์— ์ตœ๋Œ€๋กœ ๊ตฌ๋งคํ•  ์ˆ˜ ์žˆ๋Š” ์ˆ˜๋Ÿ‰์€ 10๊ฐœ ์ž…๋‹ˆ๋‹ค.")
            int amount
    ) {
        return bookName + " " + amount;
    }
}

 

 

MethodArgumentNotValidException

  • @RequestBody ๊ฐ์ฒด์˜ ๊ฒ€์ฆ์— ์‹คํŒจํ•œ ๊ฒฝ์šฐ ๋ฐœ์ƒํ•˜๋Š” ์—๋Ÿฌ
// http://localhost:8080/api/v1/book
{
    "book_name": "์šฐ๋ฆฌ๊ฐ€ ๋น›์˜ ์†๋„๋กœ ๊ฐˆ ์ˆ˜ ์—†๋‹ค๋ฉด",
    "issued_day": "2023-12-12",
    "amount": 10000
}

 

  • ํ•„์ˆ˜ ๊ฐ’์ธ category๋ฅผ ๋ˆ„๋ฝ์‹œํ‚ค๊ณ  ์ตœ๋Œ€ 100๊นŒ์ง€ ๊ฐ€๋Šฅํ•œ amount ๊ฐ’์„ ์ดˆ๊ณผ ์‹œ์ผœ request๋กœ ์ „๋‹ฌ
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleMethodArgumentNotValid(MethodArgumentNotValidException ex) {
        List<FieldError> fieldErrors = ex.getBindingResult().getFieldErrors();
        FieldError fieldError = fieldErrors.get(0);
        String field = fieldError.getField(); // amount
        String errorMessage = fieldError.getDefaultMessage(); // ์ตœ๋Œ€๋กœ ๋“ฑ๋กํ•  ์ˆ˜ ์žˆ๋Š” ์ˆ˜๋Ÿ‰์€ 100๊ฐœ ์ž…๋‹ˆ๋‹ค.
        Object value = fieldError.getRejectedValue(); // 10000
        String objectName = fieldError.getObjectName(); //registBookRequest

        ErrorResponse response = ErrorResponse.builder()
                .code(ResultCode.NOT_VALID_DATA.getCode())
                .msg(errorMessage + " " + field + "ํ•„๋“œ์˜ ๊ฐ’ " + value + "๋ฅผ ํ™•์ธํ•˜์„ธ์š”")
                .build();

        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .body(response);
    }

 

  • MethodArgumentNotValidException์€ fieldErrors๋ฅผ ํ†ตํ•ด validation์— ์‹คํŒจํ•œ ๋ชจ๋“  ํ•„๋“œ์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ ๋ฆฌ์ŠคํŠธ๋กœ ์ œ๊ณตํ•œ๋‹ค

 

  • ์œ ํšจํ•˜์ง€ ์•Š์€ ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ์— ๋Œ€ํ•œ ๊ฒฐ๊ณผ๋ฅผ fieldErrors์—์„œ ๋ชจ๋‘ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Œ
  • fieldError๋Š” ๊ฒ€์ฆ์— ์‹คํŒจํ•œ ํ•„๋“œ๋ช…(getField()), ์• ๋…ธํ…Œ์ด์…˜์—์„œ message๋กœ ์ง€์ •ํ•œ ๋ฌธ์ž์—ด(getDefaultMessage()), ๊ฒ€์ฆ์— ์‹คํŒจํ•œ ์š”์ฒญ ๊ฐ’(getRejectedValue()), RequstBody๋กœ ์ „๋‹ฌ ๋ฐ›์„ ๊ฐ์ฒด์˜ ์ด๋ฆ„(getObjectName()) ๋“ฑ์„ ์ œ๊ณตํ•œ๋‹ค
{
"code": 40001,
"msg": "์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’์€ ํ•„์ˆ˜ ์ž…๋‹ˆ๋‹ค. categoryํ•„๋“œ์˜ ๊ฐ’ null๋ฅผ ํ™•์ธํ•˜์„ธ์š”"
}

 

  • ํ”„๋ก ํŠธ์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋„๋ก ๋ฉ”์„ธ์ง€๋ฅผ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ๋‹ค.

ConstraintViolationException

// http://localhost:8080/api/v1/book/buy?book_name=te&amount=11
@RestController
@RequestMapping("/api/v1/book")
@Validated
public class BookApiController {
	@GetMapping("/buy")
    public String buy(
            @RequestParam(name = "book_name") @Size(min = 3, max = 20, message = "์ฑ…์˜ ์ด๋ฆ„์€ 3~20 ์ž๋ฆฌ๋กœ ๋“ฑ๋กํ•ด ์ฃผ์„ธ์š”.")
            String bookName,
            @RequestParam(name = "amount") @Max(value = 10, message = "ํ•œ ๋ฒˆ์— ์ตœ๋Œ€๋กœ ๊ตฌ๋งคํ•  ์ˆ˜ ์žˆ๋Š” ์ˆ˜๋Ÿ‰์€ 10๊ฐœ ์ž…๋‹ˆ๋‹ค.")
            int amount
    ) {
        return bookName + " " + amount;
    }
}

 

    @ExceptionHandler(ConstraintViolationException.class)
    public String handleConstraintViolationException(ConstraintViolationException ex) {
        StringBuilder sb = new StringBuilder();
        List<ConstraintViolation> constraintViolations = new ArrayList<>(ex.getConstraintViolations());
        for (ConstraintViolation violation : constraintViolations) {
            sb.append("ํ•„๋“œ ๋ช… : ").append(violation.getPropertyPath().toString()).append('\n')
                    .append("๋ฉ”์„ธ์ง€ : ").append(violation.getMessage()).append('\n')
                    .append("๊ฐ’ : ").append(violation.getInvalidValue()).append("\n\n");
        }
        return sb.toString();
    }

 

ํ•„๋“œ ๋ช… : buy.bookName
๋ฉ”์„ธ์ง€ : ์ฑ…์˜ ์ด๋ฆ„์€ 3~20 ์ž๋ฆฌ๋กœ ๋“ฑ๋กํ•ด ์ฃผ์„ธ์š”.
๊ฐ’ : te

ํ•„๋“œ ๋ช… : buy.amount
๋ฉ”์„ธ์ง€ : ํ•œ ๋ฒˆ์— ์ตœ๋Œ€๋กœ ๊ตฌ๋งคํ•  ์ˆ˜ ์žˆ๋Š” ์ˆ˜๋Ÿ‰์€ 10๊ฐœ ์ž…๋‹ˆ๋‹ค.
๊ฐ’ : 11

 

  • ๊ฒ€์ฆ์— ์‹คํŒจํ•œ ๋ชจ๋“  ํ•„๋“œ์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ  ์–ป์„ ์ˆ˜ ์žˆ์Œ
  • method๋ช….field๋ช…, defualt message, ๊ฒ€์ฆ ์‹คํŒจํ•œ ๊ฐ’์„ ์ œ๊ณต
    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseEntity<ErrorResponse> handleConstraintViolationException(ConstraintViolationException ex) {
        StringBuilder sb = new StringBuilder();
        ConstraintViolation<?> constraintViolations = ex.getConstraintViolations().iterator().next();

        String errorMessage = constraintViolations.getMessage();
        ErrorResponse response = ErrorResponse.builder()
                .code(ResultCode.NOT_VALID_DATA.getCode())
                .msg(errorMessage)
                .build();

        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .body(response);
    }

 

{
"code": 40001,
"msg": "์ฑ…์˜ ์ด๋ฆ„์€ 3~20 ์ž๋ฆฌ๋กœ ๋“ฑ๋กํ•ด ์ฃผ์„ธ์š”."
}

 

HandlerMethodValidationException

http://localhost:8080/api/v1/book/buy?book_name=te&amount=11
@RestController
@RequestMapping("/api/v1/book")
//@Validated
public class BookApiController {
    @GetMapping("/buy")
    public String buy(
            @RequestParam(name = "book_name") @Size(min = 3, max = 20, message = "์ฑ…์˜ ์ด๋ฆ„์€ 3~20 ์ž๋ฆฌ๋กœ ๋“ฑ๋กํ•ด ์ฃผ์„ธ์š”.")
            String bookName,
            @RequestParam(name = "amount") @Max(value = 10, message = "ํ•œ ๋ฒˆ์— ์ตœ๋Œ€๋กœ ๊ตฌ๋งคํ•  ์ˆ˜ ์žˆ๋Š” ์ˆ˜๋Ÿ‰์€ 10๊ฐœ ์ž…๋‹ˆ๋‹ค.")
            int amount
    ) {
        return bookName + " " + amount;
    }
}
    @ExceptionHandler(HandlerMethodValidationException.class)
    public String handleHandlerMethodValidation(HandlerMethodValidationException ex) {
        List<ParameterValidationResult> results = ex.getAllValidationResults();

        StringBuilder sb = new StringBuilder();
        for (ParameterValidationResult result : results) {
            String value = String.valueOf(result.getArgument());
            String parameterName = result.getMethodParameter().getParameterName();
            String message = result.getResolvableErrors().get(0).getDefaultMessage();
            sb.append(value).append("\n").append(parameterName).append("\n").append(message).append("\n\n");
        }

        return sb.toString();
    }

 

te
bookName
์ฑ…์˜ ์ด๋ฆ„์€ 3~20 ์ž๋ฆฌ๋กœ ๋“ฑ๋กํ•ด ์ฃผ์„ธ์š”.

11
amount
ํ•œ ๋ฒˆ์— ์ตœ๋Œ€๋กœ ๊ตฌ๋งคํ•  ์ˆ˜ ์žˆ๋Š” ์ˆ˜๋Ÿ‰์€ 10๊ฐœ ์ž…๋‹ˆ๋‹ค.

 

    @ExceptionHandler(HandlerMethodValidationException.class)
    public ResponseEntity<ErrorResponse> handleHandlerMethodValidation(HandlerMethodValidationException ex) {
        ParameterValidationResult results = ex.getAllValidationResults().get(0);

        ErrorResponse response = ErrorResponse.builder()
                .code(ResultCode.NOT_VALID_DATA.getCode())
                .msg(results.getResolvableErrors().get(0).getDefaultMessage())
                .build();

        return ResponseEntity.status(ex.getStatusCode())
                .body(response);
    }
{
"code": 40001,
"msg": "์ฑ…์˜ ์ด๋ฆ„์€ 3~20 ์ž๋ฆฌ๋กœ ๋“ฑ๋กํ•ด ์ฃผ์„ธ์š”."
}

 

HttpMessageNotReadableException

    @ExceptionHandler(HttpMessageNotReadableException.class)
    public ResponseEntity<ErrorResponse> handleReadableException(
            HttpMessageNotReadableException ex,
            HttpServletRequest request
    ) {
        String errorMessage = ex.getMessage(); // JSON parse error: Cannot deserialize value of type `int` from String "ํ•œ๊ฐœ": not a valid `int` value
        String uri = request.getRequestURI(); // /api/v1/book

        ErrorResponse response = ErrorResponse.builder()
                .code(ResultCode.NOT_VALID_DATA.getCode())
                .msg(errorMessage)
                .build();
        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .body(response);
    }
// http://localhost:8080/api/v1/book

{
    "book_name": "te",
    "category": "์ฒ ํ•™",
    "issued_day": "2023-12-12",
    "amount": "ํ•œ๊ฐœ"
}
  • amount๋Š” int ํƒ€์ž…์ด์–ด์•ผ ํ•˜๊ณ , ์ตœ๋Œ€๋กœ ๊ฐ€์งˆ ์ˆ˜ ์žˆ๋Š” 100์ด ์ดˆ๊ณผํ•˜์ง€ ์•Š์•˜๋Š”์ง€ ๊ฒ€์ฆํ•˜๋Š” ๋Œ€์ƒ
  • request์‹œ int๋กœ ๊ธฐ๋Œ€ํ•˜๊ณ  ์žˆ๋Š” ํƒ€์ž…์„ ํŒŒ์‹ฑํ•  ์ˆ˜ ์—†๋Š” String ์œผ๋กœ ๋ณด๋‚ด๋ฉด์„œ ๋ฐœ์ƒํ•œ ๋ฌธ์ œ
  • exception์˜ message์—์„œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์•ˆ๋‚ดํ•˜๊ณ  ์žˆ์œผ๋ฏ€๋กœ ์ด๋ฅผ ์ด์šฉ
{
"code": 40001,
"msg": "JSON parse error: Cannot deserialize value of type `int` from String \"ํ•œ๊ฐœ\": not a valid `int` value"
}

 

 

728x90
๋ฐ˜์‘ํ˜•

๋Œ“๊ธ€