Back-end/TIL

[SpringBoot] @Valid์— ์˜ํ•œ Validation Errors ๋ฐœ์ƒ ์‹œ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๋ฐ ๋ฐ˜ํ™˜

sw_develop 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๋ฅผ ๊ตฌ์„ฑํ•˜๋Š”๊ฒŒ ๋” ์šฉ์ดํ•˜๋‹ค๋Š” ์ƒ๊ฐ์ด ๋“ค์–ด ํ•ด๋‹น ๋ฐฉ์•ˆ์œผ๋กœ ์ˆ˜์ •ํ•˜์˜€๋‹ค.

 

 

 

๐ŸŽˆ์ฐธ๊ณ