| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 |
- axios
- 클라우드아키텍처
- DevOps
- 코딩테스트
- OpenSearch
- 글또10기
- UserLand
- builder-pattern
- Level2
- 클린 아키텍처
- 레벨1
- object-creation
- QueryDSL
- 가용영역
- constructor
- 3계층 아키텍처
- 코엑스그랜드볼룸
- SpringBoot
- React
- 헥사고날 아키텍처
- 다짐글
- 프로그래머스
- 글또
- static-factory-method
- HashMap
- ReverseNested
- 글쓰기세미나
- design-pattern
- 회고
- 포트앤어댑터 아키텍처
- Today
- Total
oguri's garage
@ControllerAdvice와 @ExceptionHandler로 전역 예외 처리하기 본문
Spring에서 예외 처리가 어떻게 동작하는지를 실무 관점에서 정리합니다.
서비스 계층에서 발생한 예외를 전역적으로 처리하고, 일관된 HTTP 응답으로 변환하는 과정을 중심으로 살펴봅니다.
예외 처리 흐름 요약
Service에서 예외 발생
↓
throw new CustomException("...")
↓
@ExceptionHandler가 예외를 캐치
↓
handleException() 메소드 실행
↓
ResponseEntity<ErrorResponse> 반환
↓
Spring이 JSON으로 변환 후 HTTP 응답
핵심 구조 설명
1. @ExceptionHandler - 예외를 처리할 메소드 지정
@ExceptionHandler({
CustomException.class,
DuplicateDataException.class,
InvalidParameterException.class
})
public ResponseEntity<Object> handleException(Exception ex, WebRequest request)
@ExceptionHandler는 "이 예외들이 발생하면 이 메소드로 처리한다" 는 선언입니다.
배열 형태로 여러 예외 클래스를 지정할 수 있으며, 해당 예외가 발생하면 Spring이 자동으로 이 메소드를 실행합니다.
2. 예외 타입별 응답 분기
if (ex instanceof CustomException subEx) {
errorResponse = subEx.getErrorResponse();
statusCode = HttpStatus.BAD_REQUEST;
} else if (ex instanceof DuplicateDataException subEx) {
errorResponse = subEx.getErrorResponse();
statusCode = HttpStatus.CONFLICT;
}
하나의 핸들러 메소드에서 여러 예외를 처리하면서도, 각 예외의 성격에 맞는 HTTP 상태 코드를 반환할 수 있습니다.
공통 로직은 유지하면서 세부 응답만 다르게 처리하는 구조입니다.
3. HTTP 응답 생성
return new ResponseEntity<>(errorResponse, new HttpHeaders(), statusCode);
ResponseEntity를 사용하면 HTTP 상태 코드, 헤더, 응답 본문을 명시적으로 제어할 수 있습니다.
Spring이 이를 자동으로 JSON으로 직렬화하여 클라이언트에게 전달합니다.
예시 코드
Service 계층에서 예외 발생
@Service
public class UserService {
public User findUser(String id) {
User user = repository.findById(id);
if (user == null) {
throw new CustomException("User not found: " + id, "USER_NOT_FOUND");
}
return user;
}
}
Global Exception Handler 구현
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(CustomException.class)
public ResponseEntity<Object> handleCustomException(
CustomException ex,
WebRequest request
) {
ErrorResponse body = new ErrorResponse(
ex.getCode(),
ex.getMessage(),
request.getDescription(false)
);
return new ResponseEntity<>(body, HttpStatus.BAD_REQUEST);
}
}
클라이언트가 받는 응답
{
"errorCode": "USER_NOT_FOUND",
"message": "User not found: abc123",
"path": "/api/user/abc123",
"method": "GET"
}
CustomException 설계
기본 구조
public class CustomException extends RuntimeException {
private final String code;
public CustomException(String message, String code) {
super(message);
this.code = code;
}
public String getCode() {
return code;
}
public ErrorResponse getErrorResponse() {
return new ErrorResponse(code, getMessage());
}
}
Exception vs RuntimeException 선택
Custom 예외를 만들 때 Exception과 RuntimeException 중 어떤 것을 상속할지 선택해야 합니다.
| 구분 | Exception (Checked) | RuntimeException (Unchecked) |
|---|---|---|
| 예외 처리 강제 여부 | 컴파일 타임에 명시적으로 처리해야 함 | 처리 강제 없음 |
| 사용 목적 | 예상 가능한 예외 (비즈니스 로직상 예외) | 프로그래밍 오류나 예상치 못한 상황 |
| 코드 안정성 | 호출부에서 반드시 예외 처리 필요 | 단순하고 유연하지만 처리 누락 가능 |
| 추천 케이스 | "비즈니스 오류"를 명확히 표현할 때 | 대부분의 REST API 서비스 로직 |
RuntimeException을 주로 사용
@ControllerAdvice를 통한 전역 예외 처리 환경에서는 RuntimeException을 상속하는 방식 이 더 일반적입니다.
Unchecked 예외여도 @ControllerAdvice가 자동으로 처리하기 때문에, 매번 throws 선언이나 try-catch를 강제할 필요가 없습니다.
// 대부분의 REST API에서 사용하는 방식
public class CustomException extends RuntimeException {
private final String code;
public CustomException(String message, String code) {
super(message);
this.code = code;
}
}
Checked 예외를 사용하는 경우
반면 핵심 도메인 로직이나 외부 시스템 연동처럼 예외 처리를 명시적으로 강제해야 하는 상황에서는 Exception 기반의 checked 예외도 유용합니다.
// 중요한 비즈니스 로직에서 예외 처리를 강제
public void processPayment(Order order) throws PaymentException {
// 호출하는 쪽에서 반드시 예외 처리를 해야 함
}
동작 원리: Service 예외를 어떻게 처리하는가?
예외 전파 메커니즘
Spring MVC에서 요청이 처리되는 흐름은 다음과 같습니다:
[요청 흐름]
Client → Controller → Service → Repository
[예외 발생 시 전파 흐름]
Repository (예외 발생)
↓
Service (예외 전파)
↓
Controller (예외 전파)
↓
DispatcherServlet
↓
@ControllerAdvice가 감지하여 처리
Java에서 예외가 발생하면 호출 스택을 거슬러 올라가며 전파됩니다.
Service에서 발생한 예외는 이를 호출한 Controller로 전파되고, Controller에서 처리되지 않으면 최종적으로 Spring의 DispatcherServlet까지 도달합니다.
@ControllerAdvice는 이 시점에 개입하여 예외를 가로채고 처리합니다.
즉, Controller 계층을 벗어나기 직전의 마지막 방어선 역할을 하는 것입니다.
전역 예외 처리가 가능한 이유
@ControllerAdvice // 모든 @Controller에 대해 적용
public class GlobalExceptionHandler {
@ExceptionHandler(CustomException.class) // 이 예외 타입을 감지
public ResponseEntity<Object> handleCustomException(CustomException ex) {
// 통합된 응답 생성
}
}
@ControllerAdvice는 Spring이 제공하는 AOP(Aspect-Oriented Programming) 기반의 횡단 관심사 처리 방식입니다.
모든 Controller를 감싸는 계층으로 동작하며, 각 Controller에서 예외 처리 코드를 중복 작성하지 않아도 됩니다.
이를 통해:
- Service, Repository 어디서 예외가 발생하든 일관된 처리 가능
- Controller마다 try-catch 블록을 작성할 필요 없음
- 예외 처리 로직을 한 곳에서 관리 가능
정리
- Service 계층에서 발생한 예외는 호출 스택을 따라 Controller로 전파되고,
@ControllerAdvice가 이를 감지하여 처리합니다. - @ExceptionHandler를 통해 특정 예외 타입마다 다른 응답을 생성할 수 있습니다.
- HTTP 상태 코드와 JSON 응답 형식을 일관성 있게 관리할 수 있습니다.
- RuntimeException 상속이 일반적이지만, 상황에 따라 checked 예외도 선택할 수 있습니다.
이 구조를 설정해두면 전역적으로 예외를 통합 관리할 수 있고, 클라이언트는 항상 정의된 형식의 에러 응답을 받을 수 있습니다.
참고 자료
'개발하다 > Spring' 카테고리의 다른 글
| JPA에서 DISTINCT가 필요한 경우와 필요하지 않은 경우 (0) | 2025.10.18 |
|---|---|
| Spring에서 실행 시간 측정하기 (0) | 2025.10.17 |
| MyBatis에서 JPA로 점진적 마이그레이션하기 (0) | 2025.10.12 |
| (Spring) @Transactional을 Service 계층에 적용해야 하는 이유와 올바른 사용법 (0) | 2025.10.10 |
| (Spring) Jackson 역직렬화 동작 방식과 안전한 코딩 패턴 (0) | 2025.10.04 |