[스프링 로드맵] D-2. Validation 응답 설계 - BindingResult vs 예외 처리

2026. 1. 20. 16:04·백엔드

이전 글에서 @Valid로 요청 DTO 검증을 컨트롤러 입구에서 실행하는 흐름을 잡았다.

이제 다음 문제가 바로 나온다.

  • 검증 실패 시 응답 형태가 제각각이면 클라이언트가 처리하기 어렵다
  • 어떤 필드가 왜 실패했는지를 일관된 구조로 내려주고 싶다

그래서 이번 글에서는 👉 Validation 실패 응답을 통일하는 두 가지 방식을 비교한다.


0. 먼저 정리: 선택지는 두 가지

Validation 에러를 다루는 방식은 크게 두 갈래다.

  1. 컨트롤러에서 직접 처리 (BindingResult)
  2. 예외로 터뜨리고 전역에서 처리 (@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
'백엔드' 카테고리의 다른 글
  • [스프링 로드맵] E-2. 전역 예외 처리 - 에러 코드와 로그 기준
  • [스프링 로드맵] E-1. 전역 예외 처리 기본 - 응답을 하나로 묶는 이유
  • [스프링 로드맵] D-1. Validation 기본 - DTO 검증이 필요한 이유
  • [스프링 로드맵] C-3. Repository 실전 - JpaRepository가 해주는 것들
samsam031
samsam031
samsam031 님의 블로그 입니다.
  • samsam031
    samsam031 님의 블로그
    samsam031
  • 전체
    오늘
    어제
    • 분류 전체보기
      • 디지털포렌식
      • 드림핵 문제풀이
      • 대외활동
      • 개발 실습
      • 컴퓨터 보안
      • 클라우드
      • 자격증
      • 자연어처리
      • 백엔드
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
samsam031
[스프링 로드맵] D-2. Validation 응답 설계 - BindingResult vs 예외 처리
상단으로

티스토리툴바