🔍 들어가며
Spring Boot 3에서는 @Valid 또는 @Validated 어노테이션을 통해 컨트롤러에 전달되는 요청 객체의 유효성을 자동으로 검사할 수 있습니다. 하지만 검증 실패 시 발생하는 예외를 그대로 방치하면, 지나치게 긴 에러 메시지 또는 구조화되지 않은 응답으로 인해 클라이언트에서 처리하기 어려워지는 문제가 발생합니다.
이 글에서는 Spring Boot 3에서 @Valid를 사용할 때 발생하는 대표적인 예외 MethodArgumentNotValidException, BindException에 대해 알아보고, 이를 구조화된 응답으로 변환하여 깔끔하고 직관적인 예외 처리를 구현하는 방법을 단계별로 설명하겠습니다.
✅ 1. 유효성 검증 실패 시 발생하는 예외 알아보기
1-1. @NotBlank 유효성 검증 실패 예시
예를 들어 다음과 같은 요청 객체가 있다고 가정해보겠습니다:
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class ValidObject {
@NotBlank(message = "문자열은 비어 있을 수 없습니다.")
private String string;
}
이 객체를 @Valid와 함께 사용하여 컨트롤러에서 검증하면, 다음과 같은 로그가 출력됩니다:
Resolved [org.springframework.web.bind.MethodArgumentNotValidException:
Validation failed for argument [0] in ...
Field error in object 'validObject' on field 'string': rejected value [ ];
...
default message [문자열은 비어 있을 수 없습니다.]]
로그를 보면, 유효성 검사를 통과하지 못했을 때 MethodArgumentNotValidException이 발생하는 것을 확인할 수 있습니다.
⚙️ 2. MethodArgumentNotValidException을 이용한 예외 처리
가장 기본적인 방식은 @RestControllerAdvice에서 해당 예외를 캐치하여 응답을 조작하는 것입니다.
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Response> handleValidationException(MethodArgumentNotValidException e) {
return ResponseEntity
.badRequest()
.body(new Response("E", e.getMessage()));
}
@Getter
@AllArgsConstructor
public static class Response {
private String code;
private String message;
}
}
✅ 장점
- 빠르게 구현 가능
- 모든 컨트롤러에서 재사용 가능
❌ 단점
- e.getMessage()는 지나치게 상세하고 복잡한 문자열을 반환해 가독성이 떨어짐
- 클라이언트에서 JSON 파싱이 어려움
🔁 3. BindingResult와 BindException 활용하기
보다 구조화된 예외 응답을 원한다면, BindingResult를 활용해 직접 유효성 검사를 하고, 실패 시 BindException을 던질 수 있습니다.
@PostMapping("/valid/exception")
public ValidObject bindException(@Valid @RequestBody ValidObject object, BindingResult result) throws BindException {
if (result.hasErrors()) {
throw new BindException(result);
}
return object;
}
이 경우, 예외는 MethodArgumentNotValidException이 아닌 BindException으로 발생합니다.
🛠️ 4. BindException에 대한 사용자 정의 예외 처리
이번에는 BindException을 캐치하여 필요한 정보만 추려 응답을 구성해보겠습니다.
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BindException.class)
public ResponseEntity<Response> handleBindException(BindException e) {
List<FieldError> fieldErrors = e.getFieldErrors();
List<Response.ErrorField> errors = new ArrayList<>();
for (FieldError fieldError : fieldErrors) {
errors.add(new Response.ErrorField(
fieldError.getRejectedValue(),
fieldError.getDefaultMessage()));
}
return ResponseEntity
.badRequest()
.body(new Response("BE", "유효성 검증 실패", errors));
}
@Getter
@AllArgsConstructor
public static class Response {
private String code;
private String message;
private List<ErrorField> errors;
@Getter
@AllArgsConstructor
public static class ErrorField {
private Object value;
private String message;
}
}
}
🧼 예외 응답 예시
{
"code": "BE",
"message": "유효성 검증 실패",
"errors": [
{
"value": "",
"message": "문자열은 비어 있을 수 없습니다."
},
{
"value": null,
"message": "숫자는 필수 항목입니다."
}
]
}
📌 5. 두 방식 비교 요약
구분 MethodArgumentNotValidException BindException
발생 시점 | Spring 내부에서 자동 처리 | 컨트롤러에서 수동 처리 |
유연성 | 낮음 | 높음 |
응답 커스터마이징 | 어려움 | 쉬움 |
사용 용도 | 간단한 API | 커스터마이징된 응답 필요 시 |
💡 실전 예시로 보는 사용 시나리오
예시 1: 클라이언트에서 어떤 필드가 잘못되었는지 알려줘야 할 때
✅ BindException 방식이 유리합니다.
사용자가 어떤 값을 잘못 입력했는지 rejectedValue와 함께 메시지를 주면 UX 향상에 도움이 됩니다.
예시 2: 단순히 유효성 통과 여부만 빠르게 확인하고 싶은 경우
✅ MethodArgumentNotValidException 방식으로도 충분합니다.
예시 3: 프론트엔드에서 필드 단위 에러 표시가 필요한 경우
✅ BindException + 커스터마이징된 ErrorField 객체 사용이 정답입니다.
✨ 마무리하며
Spring Boot 3에서 @Valid를 사용할 때는 검증 실패에 따른 예외 처리를 반드시 설계해야 합니다. 단순한 메시지 반환에 그치지 않고, 클라이언트에서 실제로 활용할 수 있는 구조화된 예외 응답을 설계한다면 훨씬 더 품질 높은 API를 제공할 수 있습니다.
특히 BindException을 활용한 방식은 실무에서 프론트와의 연동 시 유용하게 사용될 수 있으므로 반드시 익혀두는 것을 추천드립니다.
🔖 관련 태그
#SpringBoot3 #Valid #예외처리 #BindException #MethodArgumentNotValidException #Validation #RestAPI #JavaSpring #API응답구조화 #SpringValidation
'IT' 카테고리의 다른 글
[Spring Boot 3] @Pattern 정규표현식 검증 완벽 가이드 – 실전 예제로 배우는 유효성 검증 (0) | 2025.05.09 |
---|---|
Spring Boot 3에서 예외 처리를 책임지는 @RestControllerAdvice와 @ExceptionHandler 완벽 가이드 (0) | 2025.05.09 |
[Spring Boot 3] @DateTimeFormat을 활용한 날짜 유효성 검사 완전 정복 (0) | 2025.05.09 |
[Spring Boot 3] JPA를 이용한 데이터 등록(insert) 완벽 가이드 (0) | 2025.05.09 |
Spring Boot 3 JPA에서 데이터 수정(Update)하는 가장 깔끔한 방법과 예제 (0) | 2025.05.09 |