ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [SpringBoot] @Valid์— ์˜ํ•œ Validation Errors ๋ฐœ์ƒ ์‹œ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๋ฐ ๋ฐ˜ํ™˜
    Back-end/TIL 2022. 4. 4. 17:51

    ๐Ÿ“Œ SpringBoot RestController์—์„œ Validation Errors ๋ฐœ์ƒ ์‹œ ์ฒ˜๋ฆฌ

    @Valid์— ์˜ํ•ด ๋ฐœ์ƒํ•œ Validation Error๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ Errors ์ธํ„ฐํŽ˜์ด์Šค ํƒ€์ž…์˜ ๊ฐ์ฒด์— ๋‹ด๊ธด๋‹ค.

    ๋”ฐ๋ผ์„œ ๋ฉ”์„œ๋“œ์˜ ์ธ์ž๋กœ Errors ํƒ€์ž…์˜ ๊ฐ์ฒด๋ฅผ ๋ฐ›๋Š”์ง€ or ์•ˆ๋ฐ›๋Š”์ง€์— ๋”ฐ๋ผ ์ฒ˜๋ฆฌ๊ฐ€ ๋‹ฌ๋ผ์ง„๋‹ค.

     

    ํ•ด๋‹น ๋‚ด์šฉ์€ ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

    //์ƒํ™ฉ1) Errors ํƒ€์ž… ๊ฐ์ฒด๋ฅผ ํฌํ•จํ•˜์ง€ ์•Š์•˜์„ ๋•Œ (400 Bad Request ์ž๋™ ๋ฐ˜ํ™˜)
    @PostMapping("/user/sign-up")
    public ResponseEntity<SingleResult<UserDto.UserSignUpResDto>> userSignUp(
                @RequestBody @Valid UserDto.UserSignUpReqDto userSignUpReqDto) {
    				...
    }
    
    //์ƒํ™ฉ2) Errors ํƒ€์ž… ๊ฐ์ฒด๋ฅผ ํฌํ•จํ–ˆ์„ ๋•Œ (if๋ฌธ์œผ๋กœ ์ถ”๊ฐ€ ์ฒ˜๋ฆฌํ•ด์ค˜์•ผ ํ•จ)
    @PostMapping("/user/sign-up")
    public ResponseEntity<SingleResult<UserDto.UserSignUpResDto>> userSignUp(
                @RequestBody @Valid UserDto.UserSignUpReqDto userSignUpReqDto, Errors errors) {
    				...
    	if (errors.hasErrors()) { ... }
        			...
    }

     

    ์œ„์˜ ์ƒํ™ฉ1)์ฒ˜๋Ÿผ Errors errors ๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์ง€ ์•Š์œผ๋ฉด, ์•„๋ž˜์™€ ๊ฐ™์ด Spring Boot๊ฐ€ ์ž๋™์œผ๋กœ 400 Bad Request๋ฅผ ๋ฐ˜ํ™˜ํ•ด์ค€๋‹ค. ์ด๋ ‡๊ฒŒ ์ฒ˜๋ฆฌ๋˜๋Š” ๊ฒฝ์šฐ ๊ตฌ์ฒด์ ์ธ ์—๋Ÿฌ ๋‚ด์šฉ์„ ํ™•์ธํ•  ์ˆ˜ ์—†๋‹ค.

    {
        "timestamp": "2022-04-04T08:38:40.322+00:00",
        "status": 400,
        "error": "Bad Request",
        "path": "/v3/user/sign-up"
    }

    ๊ตฌ์ฒด์ ์ธ ์—๋Ÿฌ ๋‚ด์šฉ์„ ํ™•์ธํ•˜๋ ค๋ฉด, @RestControllerAdvice์™€ @ExceptionHandler๋ฅผ ์‚ฌ์šฉํ•ด @Valid ์œ ํšจ์„ฑ์„ ํ†ต๊ณผํ•˜์ง€ ๋ชปํ–ˆ์„ ๋•Œ ๊ธฐ๋ณธ์œผ๋กœ ๋ฐœ์ƒํ•˜๋Š” MethodArgumentValidException ์˜ˆ์™ธ์— ๋Œ€ํ•œ ๋ฐ˜ํ™˜ ๊ฐ’ ์ฒ˜๋ฆฌ๋ฅผ ํ•ด์ค˜์•ผ ํ•œ๋‹ค.

     

    ๋ฐ˜๋ฉด, ์œ„์˜ ์ƒํ™ฉ2)์ฒ˜๋Ÿผ Errors errors๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ๋‹ค๋ฉด, ๋ฐ˜๋“œ์‹œ if๋ฌธ์œผ๋กœ ์ฒ˜๋ฆฌ๋ฅผ ํ•ด์ค˜์•ผ ํ•œ๋‹ค.

    ์ฒ˜๋ฆฌ๋ฅผ ๋”ฐ๋กœ ํ•ด์ฃผ์ง€ ์•Š์œผ๋ฉด ์ž๋™์œผ๋กœ ์˜ˆ์™ธ ๋ฐ˜ํ™˜์ด ๋˜์ง€ ์•Š๊ณ , ๊ทธ๋ƒฅ 200 OK๋กœ ๋‚˜์˜จ๋‹ค.

     

    ๐Ÿ“Œ @RestControllerAdvice & @ExceptionHandler๋กœ ์˜ˆ์™ธ ์ฒ˜๋ฆฌํ•˜๊ธฐ

    ์œ„์—์„œ ์–ธ๊ธ‰ํ–ˆ๋“ฏ์ด @Valid์˜ ์œ ํšจ์„ฑ์„ ํ†ต๊ณผํ•˜์ง€ ๋ชปํ•˜๋ฉด, ๊ธฐ๋ณธ์œผ๋กœ MethodArgumentValidException์ด ๋ฐœ์ƒํ•œ๋‹ค.

    ๋งŒ์•ฝ ํ•ด๋‹น ์˜ˆ์™ธ๋ง๊ณ  Custom Exception์„ ๋ฐœ์ƒ์‹œํ‚ค๋ ค๋ฉด, ์•„๋ž˜์™€ ๊ฐ™์ด Errors๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธํ•œ ๋’ค throw๋ฅผ ํ•ด์ค˜์•ผ ํ•œ๋‹ค.

    public ResponseEntity<SingleResult<UserDto.UserSignUpResDto>> userSignUp(
                @RequestBody @Valid UserDto.UserSignUpReqDto userSignUpReqDto, Errors errors) {
    				...
    
            if(errors.hasErrors()) {
                throw new ApiParamNotValidException(errors);
            }
    
            ...
    }

     

    Custom Exception์— ๋Œ€ํ•œ ์ฒ˜๋ฆฌ๋Š” ์•„๋ž˜์™€ ๊ฐ™์ด @RestControllerAdvice์™€ @ExceptionHandler๋ฅผ ์‚ฌ์šฉํ•ด ์ง์ ‘ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๊ตฌ์„ฑ์„ ํ•ด์ค˜์•ผ ํ•œ๋‹ค. ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด, 500 Internal Server ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.

    @RestControllerAdvice
    public class ExceptionAdvice {
    
        /**
         * @valid ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ํ†ต๊ณผํ•˜์ง€ ๋ชปํ•˜๋ฉด ๋ฐœ์ƒํ•˜๋Š” ์ปค์Šคํ…€ ์˜ˆ์™ธ์— ๋Œ€ํ•œ ์ฒ˜๋ฆฌ
         */
        @ExceptionHandler(ApiParamNotValidException.class)
        @ResponseStatus(HttpStatus.BAD_REQUEST)
        protected SingleResult<Errors> apiParamNotValid(HttpServletRequest httpServletRequest, ApiParamNotValidException e) {
    
            return new SingleResult<>(e.getErrors());
        }
    }

     

    ๐Ÿ“Œ API ์‘๋‹ต์œผ๋กœ ๊ตฌ์ฒด์ ์ธ ์—๋Ÿฌ ๋‚ด์šฉ ๋ฐ˜ํ™˜ํ•˜๊ธฐ

    โ–ถ๏ธ ์ƒํ™ฉ

    @RestControllerAdvice
    public class ExceptionAdvice {
    
    	@ExceptionHandler(ApiParamNotValidException.class)
      	@ResponseStatus(HttpStatus.BAD_REQUEST)
      	protected SingleResult<Errors> apiParamNotValid(HttpServletRequest httpServletRequest, ApiParamNotValidException e) { 
                return new SingleResult<>(e.getErrors());
      	}
    }

    ์œ„์™€ ๊ฐ™์ด ์ „๋‹ฌ๋ฐ›์€ Errors ๊ฐ์ฒด๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ”๋กœ ๋ฐ˜ํ™˜ํ•˜๋ ค ํ•˜์˜€๋‹ค.

    Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class org.springframework.validation.DefaultMessageCodesResolver and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)

    ์œ„์™€ ๊ฐ™์€ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๊ฒŒ ๋œ๋‹ค.

     

    โ–ถ๏ธ ์›์ธ

    • ๊ธฐ๋ณธ์ ์ธ ๋„๋ฉ”์ธ๋“ค์€ Java Bean ์ŠคํŽ™์„ ๋”ฐ๋ฅด๊ธฐ ๋•Œ๋ฌธ์— BeanSerializer์— ์˜ํ•ด JSON Serialization์ด ๊ฐ€๋Šฅํ•˜๋‹ค. Spring์ด ์‚ฌ์šฉํ•˜๋Š” ObjectMapper์—๋Š” ์—ฌ๋Ÿฌ Serializer๊ฐ€ ๋“ฑ๋ก๋˜์–ด ์žˆ๋‹ค.
    • Errors ์ธํ„ฐํŽ˜์ด์Šค์˜ ๊ฒฝ์šฐ Java Bean ์ŠคํŽ™์„ ๋”ฐ๋ฅด์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ์ •์˜๋˜์–ด ์žˆ๋Š” Seriazlier๊ฐ€ ์—†์–ด Serialization ํ•  ์ˆ˜ ์—†๋‹ค. ๋”ฐ๋ผ์„œ ์œ„์™€ ๊ฐ™์ด 'No serializer found for class' ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•œ ๊ฒƒ์ด๋‹ค.

     

    โ–ถ๏ธ ํ•ด๊ฒฐ

    @JsonComponent์™€ JsonSerializer์˜ serialize() ๋ฉ”์„œ๋“œ๋ฅผ ์˜ค๋ฒ„๋ผ์ด๋”ฉํ•œ ํด๋ž˜์Šค๋ฅผ ์ •์˜ํ•˜์—ฌ Errors์— ๋Œ€ํ•œ Serializer๋ฅผ ๋งŒ๋“ค์–ด ํ•ด๊ฒฐ์ด ๊ฐ€๋Šฅํ•˜๋‹ค.

    @JsonComponent
    public class ErrorsSerializer extends JsonSerializer<Errors> {
    
        @Override
        public void serialize(Errors errors, JsonGenerator gen, SerializerProvider serializers) throws IOException {
            gen.writeStartArray();
    
            errors.getFieldErrors().forEach(e -> {  //field
                try {
                    gen.writeStartObject();
    
                    gen.writeStringField("field", e.getField());
                    gen.writeStringField("defaultMessage", e.getDefaultMessage());
    
                    Object rejectedValue = e.getRejectedValue();
                    if(rejectedValue != null) {
                        gen.writeStringField("rejectedValue", rejectedValue.toString());
                    }
    
                    gen.writeEndObject();
                } catch (IOException ioException) {
                    ioException.printStackTrace();
                }
            });
    
            errors.getGlobalErrors().forEach(e -> { //global
                try {
                    gen.writeStartObject();
    
                    gen.writeStringField("defaultMessage", e.getDefaultMessage());
    
                    gen.writeEndObject();
                } catch (IOException ioException) {
                    ioException.printStackTrace();
                }
            });
    
            gen.writeEndArray();
        }
    }
    • @JsonComponent - ObjectMapper์— ํ•ด๋‹น Serializer๋ฅผ ๋“ฑ๋กํ•ด์ค€๋‹ค.
    • JsonGenerator์˜ writeArray()๋ฅผ ํ†ตํ•ด ์—ฌ๋Ÿฌ ์—๋Ÿฌ ๋‚ด์šฉ์„ ๋‹ด๋Š”๋‹ค.
    • ๋ฐœ์ƒํ•œ fieldErrors๋“ค์˜ field, defaultMessage, rejectedValue ๊ฐ’๋งŒ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ๊ตฌ์„ฑํ•˜์˜€๋‹ค.

     

    ๐Ÿ‘ฉ๐Ÿป‍๐Ÿ’ป ์ „์ฒด ์ฝ”๋“œ ํ™•์ธ → spring-validation-practice

     

    ๐Ÿ“Œ ์œ„์™€ ๊ฐ™์ด ๊ตฌ์„ฑํ•œ ์ด์œ 

    ์ฒ˜์Œ์—๋Š” ์œ„์™€ ๊ฐ™์ด ErrorsSerializer๋ฅผ ๊ตฌ์„ฑํ•˜์ง€ ์•Š๊ณ , Errors ํƒ€์ž… ๊ฐ์ฒด์—์„œ ํ•„์š”ํ•œ ๊ฐ’๋“ค๋งŒ ๋ฝ‘์•„์„œ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ์ƒ์„ฑํ•ด์„œ ์‚ฌ์šฉํ–ˆ๋‹ค. ํ•˜์ง€๋งŒ Errors ํƒ€์ž… ๊ฐ์ฒด๊ฐ€ ์ƒ์„ฑ๋˜๋Š” ๋ชจ๋“  ์ƒํ™ฉ์—์„œ ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜์ง€ ์•Š๊ณ  ๋™์ผํ•œ ํ˜•ํƒœ๋กœ ์—๋Ÿฌ๋ฅผ ๋ฐ˜ํ™˜ํ•ด์ฃผ๊ธฐ ์œ„ํ•ด์„œ๋Š” Serializer๋ฅผ ๊ตฌ์„ฑํ•˜๋Š”๊ฒŒ ๋” ์šฉ์ดํ•˜๋‹ค๋Š” ์ƒ๊ฐ์ด ๋“ค์–ด ํ•ด๋‹น ๋ฐฉ์•ˆ์œผ๋กœ ์ˆ˜์ •ํ•˜์˜€๋‹ค.

     

     

     

    ๐ŸŽˆ์ฐธ๊ณ 

    ๋Œ“๊ธ€

Designed by Tistory.