[SpringBoot] @Valid์ ์ํ Validation Errors ๋ฐ์ ์ ์์ธ ์ฒ๋ฆฌ ๋ฐ ๋ฐํ
๐ 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๋ฅผ ๊ตฌ์ฑํ๋๊ฒ ๋ ์ฉ์ดํ๋ค๋ ์๊ฐ์ด ๋ค์ด ํด๋น ๋ฐฉ์์ผ๋ก ์์ ํ์๋ค.
๐์ฐธ๊ณ