스프링부트에서 예외를 custom 하여 처리할 때 흐름과 그에 따른 구현을 알아보자!
1️⃣ 클라이언트 요청 -> Dispatcher Servlet 을 거치게 됨
2️⃣ Dispatcher Servlet 은 요청을 적절한 Controller 에 전달
3️⃣ Controller 내부에서 예외 발생
ex) (AuthException : 개발자가 구현한 Exception 클래스)
throw new AuthException(AuthError.INVALID_EMAIL_FORMAT);
4️⃣ Dispatcher Servlet 이 AuthException 처리를 위해 예외 핸들러를 찾기 시작
-> 스프링 컨테이너에 등록된 @ControllerAdvice / @RestControllerAdvice Bean 중에서 해당 예외를 처리할 수 있는 @ExceptionHandler 메서드를 탐색
5️⃣ GlobalExceptionHandler 클래스 내에 정의된 @ExceptionHandler(AuthException.class) 메서드를 발견하고 실행
6️⃣ 응답 객체를 생성하여 반환
1. 반환할 응답 객체 (ErrorResponse) 생성
public record ErrorResponse(
String message,
String code
) {
}
2. ErrorCode 인터페이스 생성
도메인 별 오류 코드를 일관된 구조로 관리하기 위한 인터페이스
public interface ErrorCode {
String getMessage();
String getCode();
}
3. enum class 생성 (ex. CommonError)
@Getter
@RequiredArgsConstructor
public enum CommonError implements ErrorCode {
INTERNAL_SERVER_ERROR("서버 내부 오류가 발생했습니다. 잠시 후 다시 시도해주세요.", "S_001", HttpStatus.INTERNAL_SERVER_ERROR),
UNEXPECTED_ERROR("예상치 못한 오류가 발생했습니다. 시스템 관리자에게 문의해주세요.", "S_002", HttpStatus.INTERNAL_SERVER_ERROR),
INVALID_INPUT_VALUE("요청 값이 올바르지 않습니다.", "C_001", HttpStatus.BAD_REQUEST),
RESOURCE_NOT_FOUND("요청하신 리소스를 찾을 수 없습니다.", "C_002", HttpStatus.NOT_FOUND);
private final String message;
private final String code;
}
4. 처리하고자 하는 ExceptionClass 구현
ex)
public class CustomException extends RuntimeException {
private final ErrorCode errorCode;
public CustomException(ErrorCode errorCode) {
super(errorCode.getMessage()); // 메시지를 RuntimeException에 전달
this.errorCode = errorCode;
}
public ErrorCode getErrorCode() {
return errorCode;
}
}
💡RuntimeException 을 상속하는 이유 !
RuntimeException은 Unchecked Exception으로, 예외 발생 시 try-catch나 throws를 명시적으로 강제하지 않아도 됨
→ 코드가 간결해지고, 비즈니스 로직에서 유연하게 예외 처리 가능cf. Checked Exception은 예외 발생 시 반드시 명시적 처리(try-catch 또는 throws)가 필요하여 코드가 복잡해질 수 있음
5. GlobalExceptionHandler 구현
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
// CustomException 처리
@ExceptionHandler(CustomException.class)
public ResponseEntity<ErrorResponse> handleCustomException(CustomException e) {
ErrorCode errorCode = e.getErrorCode();
log.warn("CustomException 발생: code = {}, message = {}", errorCode.getCode(), errorCode.getMessage());
// CommonError enum 내에 HttpStatus가 있으므로 여기서 직접 사용
HttpStatus status = null;
if (errorCode instanceof CommonError commonError) {
status = commonError.getHttpStatus();
} else {
status = HttpStatus.BAD_REQUEST; // 기본 fallback
}
return ResponseEntity
.status(status)
.body(new ErrorResponse(errorCode.getMessage(), errorCode.getCode()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(Exception e) {
log.error("서버 내부 오류 발생 : message = {}", e.getMessage(), e);
return ResponseEntity
.internalServerError()
.body(new ErrorResponse("서버 내부 오류가 발생했습니다.", "S_001"));
}
}
여기서 handleCustomException은 직접 정의한 CustomException.class 를 처리하기 위한 핸들러이고,
handleException은 예상하지 못한 일반 예외 처리를 위한 핸들러이다.
💡 @RestControllerAdvie 란,
@ControllerAdvice + @ResponseBody의 조합으로,
전역(Global) 예외 처리를 위한 클래스에 붙이며, 모든 @RestController의 예외를 JSON 형태로 일괄 처리할 수 있게 해줌.
💡 @ExceptionHandler 란,
특정 예외 클래스에 대해 실행할 예외 처리 메서드를 지정하는 어노테이션.
예외 타입별로 다른 응답을 만들 수 있음.