예외 처리 계층

3줄 요약

  • Spring MVC의 예외 처리는 Controller 주변의 명시적 처리, 전역 Advice 처리, Boot 기본 에러 처리로 이어지는 계층이다.
  • 애플리케이션이 의도한 예외는 도메인/유스케이스 의미를 담아 공통 예외 처리에서 HTTP 응답으로 변환한다.
  • 미처리 예외는 마지막 방어선에서 500 계열 응답으로 처리하되 내부 정보가 외부로 노출되지 않게 해야 한다.

핵심 정리

  • 예외는 단순한 실패가 아니라 계층 간 실패 계약이다. Service 예외를 Controller가 어떤 HTTP 상태와 응답으로 바꿀지 정해야 한다.
  • @ExceptionHandler는 특정 Controller 또는 @ControllerAdvice 안에서 예외를 처리하는 메서드다.
  • @ControllerAdvice@RestControllerAdvice는 여러 Controller에 공통 예외 처리 정책을 적용한다.
  • Spring MVC 6 계열은 ErrorResponse, ProblemDetail, ResponseEntityExceptionHandler를 통해 RFC 9457 계열 문제 상세 응답도 지원한다. 커스텀 DTO를 쓸지 표준 Problem Details를 쓸지는 API 계약으로 정해야 한다.
  • Spring Boot는 별도 처리가 없는 오류를 /error 경로와 BasicErrorController, ErrorAttributes를 통해 기본 응답으로 처리한다.
  • API에서는 HTML 에러 페이지보다 상태 코드, 오류 코드, 메시지, 필드 오류, 추적 ID 같은 구조화된 응답이 중요하다.

헷갈리는 지점

  • 예외를 모두 잡으면 안정적이라고 생각하기 쉽다. 장애가 화면에 덜 드러나기 때문이다.
    • 핵심은 잡아야 할 예외와 터뜨려야 할 예외를 구분하는 것이다.
    • 복구나 변환이 가능한 예외만 의도적으로 처리하고, 예상 못한 예외는 로깅 후 일관된 500 응답으로 보낸다.
  • Service 예외가 HTTP 상태를 직접 알아야 한다고 생각하기 쉽다. 결국 API 응답으로 나가기 때문이다.
    • 핵심은 Service는 유스케이스 실패 의미를 표현하고, Web 계층이 HTTP로 변환하는 것이다.
    • 도메인 예외와 HTTP 응답 모델을 분리하면 같은 Service를 웹 밖에서도 재사용하기 쉽다.
  • Boot 기본 에러 처리가 있으니 전역 예외 처리가 필요 없다고 보기 쉽다. 에러 응답이 자동으로 나오기 때문이다.
    • 핵심은 기본 처리는 마지막 방어선이고 애플리케이션 계약은 직접 설계해야 한다는 점이다.
    • 클라이언트가 의존할 API 오류 형식은 공통 Advice에서 명확히 잡는 편이 좋다.
  • @ExceptionHandler는 단순히 예외 클래스가 같으면 항상 같은 handler가 잡는다고 생각하기 쉽다. 예외 상속과 cause wrapping이 있기 때문이다.
    • 핵심은 root exception과 cause exception 매칭, Advice 우선순위, media type 조건이 함께 작동할 수 있다는 점이다.
    • 여러 Advice가 있다면 어떤 handler가 먼저 적용되는지 테스트로 고정해야 한다.

확인 질문

  • Spring MVC에서 공통 예외 처리 정책을 여러 Controller에 적용하려면 무엇을 사용하는가?
    • @ControllerAdvice 또는 @RestControllerAdvice를 사용한다.
  • Service 계층 예외에 HTTP 상태를 직접 넣는 것이 항상 좋은가?
    • 아니다. Service는 실패 의미를 표현하고 Web 계층이 HTTP 응답으로 변환하는 편이 계층 분리에 좋다.
  • Spring Boot의 기본 에러 처리는 어떤 역할인가?
    • 직접 처리되지 않은 오류를 마지막으로 받아 /error 경로에서 기본 에러 응답이나 페이지로 변환하는 역할이다.

@ExceptionHandler와 ControllerAdvice

3줄 요약

  • @ExceptionHandler는 특정 예외가 발생했을 때 실행할 처리 메서드를 선언한다.
  • @ControllerAdvice는 여러 Controller에 공통으로 적용되는 예외 처리와 바인딩 설정, 모델 속성 처리를 제공한다.
  • API 서버에서는 @RestControllerAdvice로 예외를 일관된 JSON 응답으로 바꾸는 패턴이 가장 자주 쓰인다.

핵심 정리

  • Controller 내부의 @ExceptionHandler는 해당 Controller에 가까운 예외 정책을 표현한다.
  • 전역 @ControllerAdvice는 여러 Controller에서 반복되는 예외 변환을 모은다.
  • 여러 Advice가 있거나 예외 상속 관계가 얽히면 적용 순서와 예외 매칭을 명확히 해야 한다.
  • ResponseEntity, ProblemDetail, 커스텀 응답 DTO를 이용해 상태 코드와 본문을 함께 제어할 수 있다.
  • @RestControllerAdvice@ControllerAdvice@ResponseBody를 합친 형태로 REST 응답에 적합하다.
  • @ExceptionHandler는 예외 타입뿐 아니라 produces 조건으로 JSON 오류와 HTML 오류 페이지를 나누는 방식도 지원한다.

헷갈리는 지점

  • @ExceptionHandler가 모든 예외를 자동으로 처리한다고 생각하기 쉽다. 애너테이션 이름이 전역 기능처럼 들리기 때문이다.
    • 핵심은 선언된 위치와 대상 예외에 따라 적용 범위가 달라진다는 점이다.
    • Controller 내부인지 Advice인지, 어떤 예외 타입을 받는지 확인해야 한다.
  • 전역 Advice 하나에 모든 예외를 넣는 것이 깔끔하다고 느끼기 쉽다. 파일 하나에 정책이 모이기 때문이다.
    • 핵심은 예외 정책도 도메인과 계층에 따라 분리될 수 있다는 점이다.
    • 인증, 검증, 비즈니스, 시스템 예외를 역할별로 나누면 변경 지점이 더 선명하다.
  • 예외 클래스 이름을 그대로 API 오류 코드로 쓰면 충분하다고 생각하기 쉽다. 구현이 빠르기 때문이다.
    • 핵심은 예외 클래스는 내부 구현이고 오류 코드는 외부 계약이라는 점이다.
    • 클래스 이름 변경이 클라이언트 계약 변경으로 이어지지 않게 분리해야 한다.

확인 질문

  • @RestControllerAdvice는 무엇을 합친 애너테이션인가?
    • @ControllerAdvice@ResponseBody 성격을 합친 REST 응답용 애너테이션이다.
  • Controller 내부 @ExceptionHandler와 전역 Advice의 차이는 무엇인가?
    • Controller 내부 handler는 해당 Controller에 가깝고, 전역 Advice는 여러 Controller에 공통으로 적용된다.
  • API 오류 코드와 예외 클래스 이름을 분리하는 이유는 무엇인가?
    • 예외 클래스는 내부 구현이고 오류 코드는 클라이언트가 의존하는 외부 계약이기 때문이다.

Spring Boot 기본 에러 처리

3줄 요약

  • Spring Boot는 미처리 오류를 /error로 보내고 BasicErrorControllerErrorAttributes로 기본 응답을 만든다.
  • 브라우저 요청에는 에러 페이지, API 요청에는 JSON 형태의 오류 응답이 선택될 수 있다.
  • 기본 에러 처리는 편리하지만 서비스의 공식 API 오류 계약을 대신하지는 않는다.

핵심 정리

  • Boot의 에러 처리 자동 설정은 애플리케이션이 처리하지 않은 예외와 오류 상태를 마지막으로 처리한다.
  • BasicErrorController/error 요청을 받아 HTML 또는 다른 응답 형식으로 에러 정보를 반환한다.
  • ErrorAttributes는 오류 응답에 들어갈 기본 속성을 제공하며, 필요하면 커스터마이징할 수 있다.
  • Boot에서 Problem Details 응답을 활성화하면 built-in MVC 예외가 ProblemDetail 기반 응답으로 렌더링될 수 있다. 이때도 애플리케이션 고유 오류 코드는 별도 정책으로 맞춰야 한다.
  • Whitelabel Error Page는 별도 에러 페이지가 없을 때 보이는 기본 HTML 에러 화면이다.
  • 운영 API에서는 기본 에러 응답에 의존하기보다 공통 예외 처리와 관측용 로그/추적 ID를 함께 설계한다.

헷갈리는 지점

  • Whitelabel Error Page가 보이면 Spring MVC 예외 처리가 실패한 것이라고 생각하기 쉽다. 낯선 기본 화면이 나오기 때문이다.
    • 핵심은 처리되지 않은 오류가 Boot의 기본 에러 경로까지 도달했다는 점이다.
    • 의도한 예외라면 전역 Advice에서 먼저 처리되도록 설계해야 한다.
  • /error가 일반 Controller와 완전히 별개라고 보기 쉽다. 자동으로 동작해서 잘 보이지 않기 때문이다.
    • 핵심은 Boot가 오류 디스패치를 /error 경로로 연결한다는 점이다.
    • BasicErrorControllerErrorAttributes가 마지막 응답을 만든다.
  • 기본 JSON 에러 응답을 그대로 API 계약으로 써도 된다고 생각하기 쉽다. 당장 필요한 필드가 보이기 때문이다.
    • 핵심은 기본 응답은 프레임워크 제공값이고 애플리케이션의 장기 계약은 아니라는 점이다.
    • 클라이언트가 의존할 오류 구조는 별도로 안정화하는 편이 좋다.

확인 질문

  • Spring Boot에서 미처리 오류를 받는 기본 경로는 무엇인가?
    • /error이다.
  • ErrorAttributes의 역할은 무엇인가?
    • 에러 응답에 들어갈 기본 속성을 제공하고 필요하면 커스터마이징할 수 있게 한다.
  • Boot 기본 에러 처리만으로 API 오류 설계가 충분한가?
    • 아니다. 기본 처리는 마지막 방어선이고, 공식 API 오류 계약은 별도로 설계해야 한다.

API 에러 응답 설계

3줄 요약

  • 좋은 API 에러 응답은 HTTP 상태 코드, 안정적인 오류 코드, 사람이 읽을 메시지, 필요한 상세 정보를 분리한다.
  • 검증 실패, 인증 실패, 권한 실패, 리소스 없음, 비즈니스 충돌은 서로 다른 상태와 코드로 표현해야 한다.
  • 예외 처리의 목표는 예외를 숨기는 것이 아니라 실패를 클라이언트가 예측 가능한 계약으로 받게 하는 것이다.

핵심 정리

  • HTTP 상태 코드는 실패의 큰 범주를 나타낸다. 400, 401, 403, 404, 409, 422, 500의 의미를 섞지 않는다.
  • 오류 코드는 애플리케이션 내부에서 안정적으로 관리하는 식별자다. 클라이언트 분기, 문서, 테스트는 메시지가 아니라 코드에 의존해야 한다.
  • 메시지는 사용자나 개발자가 이해하기 위한 설명이다. 다국어, 화면 맥락, 보안 수준에 따라 바뀔 수 있다.
  • 상세 정보는 검증 필드 오류, 현재 요청 식별자, 문서 링크처럼 문제 해결에 필요한 정보만 담는다.
  • 내부 스택트레이스, SQL, 인증 토큰, 서버 경로는 응답에 노출하지 않는다.
  • Spring 기본 ProblemDetail을 그대로 쓸지, 조직 표준 에러 DTO를 쓸지 먼저 정해야 응답 형식이 흔들리지 않는다.

헷갈리는 지점

  • HTTP 상태 코드만 맞으면 충분하다고 생각하기 쉽다. REST에서 상태 코드가 중요하게 설명되기 때문이다.
    • 핵심은 상태 코드는 범주이고 애플리케이션별 세부 원인은 오류 코드가 표현한다는 점이다.
    • 클라이언트가 복구 행동을 정하려면 안정적인 코드가 필요하다.
  • 모든 비즈니스 실패를 400 Bad Request로 처리하기 쉽다. 클라이언트 요청이 잘못되었다고 넓게 볼 수 있기 때문이다.
    • 핵심은 문법적 요청 오류와 상태 충돌, 권한 실패, 리소스 없음은 의미가 다르다는 점이다.
    • 실패 원인에 맞는 상태 코드를 고르면 로그와 클라이언트 처리가 선명해진다.
  • 상세 정보를 많이 줄수록 친절하다고 생각하기 쉽다. 디버깅에는 정보가 많을수록 편하기 때문이다.
    • 핵심은 외부 응답과 내부 로그의 역할을 나누는 것이다.
    • 민감한 정보는 로그의 보안 정책 안에서 관리하고 응답에는 필요한 최소 정보만 둔다.

확인 질문

  • API 오류 응답에서 클라이언트가 분기 기준으로 삼아야 하는 값은 무엇인가?
    • 안정적인 오류 코드다.
  • 401403의 차이는 무엇인가?
    • 401은 인증이 필요하거나 실패한 경우이고, 403은 인증은 되었지만 권한이 부족한 경우다.
  • 내부 스택트레이스를 응답에 노출하면 안 되는 이유는 무엇인가?
    • 보안 정보와 내부 구현이 노출되고 클라이언트 계약이 내부 구조에 의존하게 되기 때문이다.