이전 글에서 @Valid로 요청 DTO 검증을 컨트롤러 입구에서 실행하는 흐름을 잡았다.
이제 다음 문제가 바로 나온다.
- 검증 실패 시 응답 형태가 제각각이면 클라이언트가 처리하기 어렵다
- 어떤 필드가 왜 실패했는지를 일관된 구조로 내려주고 싶다
그래서 이번 글에서는 👉 Validation 실패 응답을 통일하는 두 가지 방식을 비교한다.
0. 먼저 정리: 선택지는 두 가지
Validation 에러를 다루는 방식은 크게 두 갈래다.
- 컨트롤러에서 직접 처리 (BindingResult)
- 예외로 터뜨리고 전역에서 처리 (@RestControllerAdvice)
둘 다 가능하고, 동작 원리는 다르다. 이 글에서는 “어떻게 다른지”를 이해하는 게 목표다.
1. 방식 1: BindingResult를 컨트롤러에서 직접 처리
개념
- @Valid 바로 뒤에 BindingResult를 선언하면
- 검증 실패 시 예외를 던지지 않고, 에러 정보가 BindingResult에 담긴다
- 컨트롤러 메서드 안에서 직접 분기 처리한다
예제 코드
@PostMapping
public ResponseEntity<?> create(
@RequestBody @Valid ShiftCreateRequest req,
BindingResult bindingResult
) {
if (bindingResult.hasErrors()) {
List<String> errors = bindingResult.getFieldErrors()
.stream()
.map(e -> e.getField() + ": " + e.getDefaultMessage())
.toList();
return ResponseEntity
.badRequest()
.body(errors);
}
shiftService.create(req);
return ResponseEntity.status(201).build();
}
이 방식의 특징
- 검증 실패를 컨트롤러에서 직접 처리
- 에러 응답 구조를 즉석에서 만들 수 있음
- 흐름이 코드로 눈에 보이기 때문에 이해하기 쉽다
단점도 분명하다
- 컨트롤러마다 같은 코드가 반복된다
- 엔드포인트가 많아질수록 중복 처리 로직이 늘어난다
- 컨트롤러가 “요청 처리 + 에러 포맷팅”까지 떠안게 된다
👉 개념 학습용으로는 좋지만, 프로젝트가 커질수록 관리가 어려워진다.
2. 방식 2: 예외로 터뜨리고 전역 처리(@RestControllerAdvice)
개념
- @Valid 검증 실패 시 스프링은 예외를 던진다 (MethodArgumentNotValidException)
- 이 예외를 전역에서 한 번만 받아서 응답을 만든다
- 컨트롤러는 “정상 흐름”만 신경 쓴다
컨트롤러는 이렇게 단순해진다
@PostMapping
public ResponseEntity<Void> create(
@RequestBody @Valid ShiftCreateRequest req
) {
shiftService.create(req);
return ResponseEntity.status(201).build();
}
검증 실패 로직은 컨트롤러에 없다.
전역 예외 처리 클래스
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidation(
MethodArgumentNotValidException ex
) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult()
.getFieldErrors()
.forEach(error ->
errors.put(error.getField(), error.getDefaultMessage())
);
return ResponseEntity
.badRequest()
.body(errors);
}
}
응답 예시
{
"employerName": "근무처는 필수입니다.",
"hourlyWage": "시급이 너무 낮습니다."
}
이 방식의 특징
- Validation 실패 응답을 한 곳에서 통제
- 모든 컨트롤러에 동일한 에러 포맷 적용 가능
- 컨트롤러는 요청/응답 흐름만 담당
👉 구조를 이해하면 훨씬 깔끔한 방식이다.
3. 두 방식 비교 정리
| 구분 | BindingResult 방식 | 전역 예외 처리 방식 |
| 처리 위치 | 컨트롤러 내부 | 전역(@RestControllerAdvice) |
| 컨트롤러 코드 | 길어짐 | 짧고 단순 |
| 중복 가능성 | 높음 | 낮음 |
| 구조 이해 난이도 | 낮음 | 약간 높음 |
| 응답 포맷 통일 | 어렵다 | 쉽다 |
4. 한 문장 요약
Validation 에러는 컨트롤러에서 직접 처리할 수도 있고, 예외로 터뜨린 뒤 전역에서 한 번에 처리할 수도 있으며, 에러 응답 포맷을 통일하려면 전역 예외 처리 방식이 더 구조적이다.
'백엔드' 카테고리의 다른 글
| [스프링 로드맵] E-2. 전역 예외 처리 - 에러 코드와 로그 기준 (0) | 2026.01.20 |
|---|---|
| [스프링 로드맵] E-1. 전역 예외 처리 기본 - 응답을 하나로 묶는 이유 (1) | 2026.01.20 |
| [스프링 로드맵] D-1. Validation 기본 - DTO 검증이 필요한 이유 (0) | 2026.01.20 |
| [스프링 로드맵] C-3. Repository 실전 - JpaRepository가 해주는 것들 (1) | 2026.01.19 |
| [스프링 로드맵] C-2. 엔티티 설계 - @Entity와 기본 생성자의 의미 (0) | 2026.01.19 |