[스프링 로드맵] E-1. 전역 예외 처리 기본 - 응답을 하나로 묶는 이유

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

이전 글에서 Validation 실패가 전역에서 처리되는 구조를 봤다.

그럼 자연스럽게 다음 질문이 나온다.

  • “검증 말고, 서비스 로직에서 터지는 에러는?”
  • “없는 리소스를 조회하면?”
  • “예외마다 응답 형태가 달라지면 어떻게 하지?”

그래서 이번 단계에서는 👉 컨트롤러/서비스에서 발생한 예외를 한 곳으로 모아, 응답 포맷을 통일하는 구조를 만든다.


0. 전체 흐름

이 글의 핵심은 이 흐름이다.

클라이언트 요청
   ↓
Controller (요청 매핑, 서비스 호출)
   ↓
Service (비즈니스 판단)
   ↓
예외 발생 (throw)
   ↓
DispatcherServlet
   ↓
@RestControllerAdvice + @ExceptionHandler
   ↓
JSON 에러 응답 (400 / 404 / 500)

컨트롤러와 서비스는 예외를 “처리하지 않는다”.

예외는 위로 던지고, 응답으로 바꾸는 책임은 전역 예외 처리기가 가진다.


1. 이 글에서 해결하려는 문제

컨트롤러/서비스 곳곳에서 예외는 언제든 발생할 수 있다.

  • 잘못된 요청 → 400
  • 없는 데이터 → 404
  • 예상 못 한 오류 → 500

아무 구조 없이 두면:

  • 응답 포맷이 제각각
  • 어떤 API는 message 있고, 어떤 API는 없고
  • 프론트/클라이언트에서 공통 처리하기 어려움

어떤 예외가 터지든, 에러 응답은 항상 같은 JSON 구조로 내려보내기


2. 기본 개념: 전역 예외 처리기

@ControllerAdvice / @RestControllerAdvice

  • 모든 컨트롤러에서 발생한 예외를 가로채는 클래스
  • 컨트롤러마다 try-catch를 쓰지 않기 위한 장치
@RestControllerAdvice
public class GlobalExceptionHandler {
}
  • @RestControllerAdvice
    • @ControllerAdvice + @ResponseBody
    • 에러 응답도 JSON으로 내려보내기 위함

@ExceptionHandler

  • 특정 예외 타입을 잡아서
  • HTTP 응답으로 변환하는 메서드
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<?> handleIllegalArgument(...) { }

3. 에러 응답 형태부터 고정

먼저 공통 에러 응답 DTO를 정의한다.

public record ErrorResponse(
        int status,
        String message
) {}

지금 단계에서는 단순하게:

  • status: HTTP 상태 코드
  • message: 에러 설명

(필드 에러 목록, 에러 코드 등은 다음 단계에서 확장)


4. 전역 예외 처리 클래스 구현

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<ErrorResponse> handleBadRequest(
            IllegalArgumentException ex
    ) {
        ErrorResponse body = new ErrorResponse(
                400,
                ex.getMessage()
        );
        return ResponseEntity.badRequest().body(body);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleServerError(
            Exception ex
    ) {
        ErrorResponse body = new ErrorResponse(
                500,
                "서버 내부 오류가 발생했습니다."
        );
        return ResponseEntity.status(500).body(body);
    }
}

 

여기서 핵심

  • 컨트롤러/서비스는 예외를 잡지 않는다
  • 예외를 HTTP 응답으로 바꾸는 책임은 전부 여기 있다
  • Exception.class는 최종 안전망

5. 서비스 → 예외 → 전역 처리 흐름 예시

5-1. 커스텀 예외 정의 (404 용도)

public class NotFoundException extends RuntimeException {

    public NotFoundException(String message) {
        super(message);
    }
}

5-2. 서비스에서 예외를 “던진다”

@Service
public class ShiftService {

    public Shift getById(Long id) {
        return repository.findById(id)
                .orElseThrow(() ->
                        new NotFoundException("해당 근무 기록이 없습니다.")
                );
    }
}

여기서 서비스는:

  • HTTP 상태 코드 모름
  • JSON 응답 구조 모름
  • 비즈니스 의미만 표현 → “이 데이터는 존재하지 않는다”

5-3. 컨트롤러는 예외를 처리하지 않는다

@GetMapping("/{id}")
public ShiftResponse get(@PathVariable Long id) {
    return service.getById(id); // 예외 나면 여기서 멈춤
}
  • try-catch 없음
  • 예외는 그대로 위로 전달됨

5-4. 전역 예외 처리기가 응답으로 변환

@ExceptionHandler(NotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(
        NotFoundException ex
) {
    ErrorResponse body = new ErrorResponse(
            404,
            ex.getMessage()
    );
    return ResponseEntity.status(404).body(body);
}

6. 실제 요청 → 응답 흐름 요약

  1. 클라이언트가 /api/shifts/10 요청
  2. 컨트롤러 → 서비스 호출
  3. 서비스에서 NotFoundException 발생
  4. 컨트롤러는 잡지 않고 예외 전파
  5. DispatcherServlet이 예외 감지
  6. @RestControllerAdvice에서 해당 예외 매칭
  7. JSON 에러 응답 생성 후 반환

7. Validation과 Exception의 관계

  • Validation 실패
    → 전역 예외 처리에서 400 변환
    → MethodArgumentNotValidException
  • 비즈니스 오류
    → 커스텀 예외 (NotFoundException 등)
  • 예상 못 한 오류
    → Exception.class → 500

👉 모든 에러가 하나의 전역 처리 파이프라인으로 모인다


8. 요약 3줄

  1. 컨트롤러/서비스는 예외를 처리하지 않는다
  2. 예외는 위로 던지고, 전역에서 HTTP 응답으로 바꾼다
  3. 그래서 에러 응답 구조를 한 곳에서 통제할 수 있다

'백엔드' 카테고리의 다른 글

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

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

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
samsam031
[스프링 로드맵] E-1. 전역 예외 처리 기본 - 응답을 하나로 묶는 이유
상단으로

티스토리툴바