프로젝트 규모가 커질수록 컨트롤러마다 try-catch를 작성하고 계신가요? 중복되는 에러 응답 코드는 수정요소가 발생할 경우 모두 확인해야하므로 유지보수를 어렵게 만듭니다. 오늘은 스프링 부트의 @RestControllerAdvice를 이용해, 모든 API의 예외를 한 곳에서 우아하게 처리할 것이며, 일관된 응답 구조를 만드는 방법에 대해 정리를 해보고자 합니다.
1. 글로벌 예외 처리란?
글로벌 예외 처리란, 애플리케이션의 여러 지점(Controller, Service 등)에서 발생하는 예외를 한곳으로 모아 공통적으로 처리하는 방식입니다.
스프링에서는 @RestControllerAdvice를 통해 이를 구현하며, 예외가 발생하면 중간에 낚아채서 정해진 규격에 맞는 에러 메시지를 응답으로 보내주는 역할을 합니다.
2. 왜 글로벌 예외 처리를 써야 하는가?
- 관심사의 분리(Separation of Concerns): 비즈니스 로직은 성공 케이스에만 집중하고, 예외 처리는 전담 클래스가 맡습니다.
- 코드 중복 제거: 수십 개의 컨트롤러에 try-catch를 작성할 필요가 없습니다.
- 응답 규격의 통일: 프론트엔드 개발자가 어떤 API를 호출하더라도 동일한 에러 JSON 구조를 받게 되어 연동 작업이 매우 수월해집니다.
3. 실무급 구현 A to Z
Step 1. 에러 코드와 메시지를 관리할 Enum 정의
에러를 단순히 텍스트로 던지는 것이 아니라, 관리하기 쉽게 코드로 정의합니다.
public enum ErrorCode {
INVALID_INPUT_VALUE(400, "C001", "잘못된 입력값입니다."),
ENTITY_NOT_FOUND(404, "C002", "찾을 수 없는 리소스입니다."),
INTERNAL_SERVER_ERROR(500, "C003", "서버 내부 오류입니다.");
private final int status;
private final String code;
private final String message;
ErrorCode(int status, String code, String message) {
this.status = status;
this.code = code;
this.message = message;
}
// Getter 생략
}
Step 2. 공통 에러 응답 객체 (ErrorResponse)
클라이언트가 받을 최종 JSON 구조입니다.
public record ErrorResponse(
String code,
String message,
int status
) {
public static ErrorResponse of(ErrorCode errorCode) {
return new ErrorResponse(errorCode.getCode(), errorCode.getMessage(), errorCode.getStatus());
}
}
Step 3. 커스텀 비즈니스 예외 정의
우리가 직접 제어할 수 있는 예외를 별도로 만듭니다.
public class BusinessException extends RuntimeException {
private final ErrorCode errorCode;
public BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
public ErrorCode getErrorCode() { return errorCode; }
}
Step 4. 글로벌 예외 핸들러 구현 (@RestControllerAdvice)
여기가 핵심입니다. 모든 예외를 낚아채는 집하장 역할을 합니다.
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// 우리가 정의한 비즈니스 예외 처리
@ExceptionHandler(BusinessException.class)
protected ResponseEntity<ErrorResponse> handleBusinessException(BusinessException e) {
log.error("BusinessException", e);
ErrorCode errorCode = e.getErrorCode();
ErrorResponse response = ErrorResponse.of(errorCode);
return new ResponseEntity<>(response, HttpStatus.valueOf(errorCode.getStatus()));
}
// 그 외 예상치 못한 모든 예외 처리
@ExceptionHandler(Exception.class)
protected ResponseEntity<ErrorResponse> handleException(Exception e) {
log.error("InternalServerException", e);
ErrorResponse response = ErrorResponse.of(ErrorCode.INTERNAL_SERVER_ERROR);
return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
4. 실제 사용 예시
글로벌 처리가 되어 있으면, 서비스 로직은 놀라울 정도로 깔끔해집니다.
@Service
public class UserService {
public User findUser(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
// try-catch 없이 예외만 던지면 끝!
}
}
5. 마무리하며
글로벌 예외 처리는 단순히 에러를 막는 것이 아니라, API의 사용자(프론트엔드, 앱 개발자)와 소통하는 방식을 정의하는 것입니다. 이제 여러분의 프로젝트에서 지저분한 try-catch를 걷어내고 @RestControllerAdvice를 도입해 보세요.
코드의 예외 상황을 미리 설계(Architecture)하는 것은, 투자에서 **리스크 관리(Risk Management)**를 하는 것과 같습니다. 예상치 못한 에러(하락장)에 당황하지 않도록 미리 시스템을 구축해 두세요.
'TECH > 웹개발' 카테고리의 다른 글
| [Java] 자바 성능의 키, JVM 메모리 구조와 가비지 컬렉션(GC) 완벽 이해 (0) | 2026.02.23 |
|---|---|
| [Java] 컬렉션 프레임워크 완벽 정리: List, Set, Map 차이점 모르면 면접 탈락? (0) | 2026.02.13 |
| [Java] 자바 예외 처리(Exception) 총정리: try-catch부터 Custom Exception까지 (0) | 2026.02.11 |
| [Java] 메서드 체이닝(Method Chaining) 완벽 가이드: 원리부터 실무 활용까지 (0) | 2026.02.10 |
| [Java] Optional 완벽 가이드: 더 이상 NullPointerException은 없다 (0) | 2026.02.09 |