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로 보내고 BasicErrorController와 ErrorAttributes로 기본 응답을 만든다.
브라우저 요청에는 에러 페이지, 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 경로로 연결한다는 점이다.
BasicErrorController와 ErrorAttributes가 마지막 응답을 만든다.
기본 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 오류 응답에서 클라이언트가 분기 기준으로 삼아야 하는 값은 무엇인가?
안정적인 오류 코드다.
401과 403의 차이는 무엇인가?
401은 인증이 필요하거나 실패한 경우이고, 403은 인증은 되었지만 권한이 부족한 경우다.