예외처리(예외)에 대한 정리
1. 예외 구조의 본질과 분류 (사용자 입력 기준)
예외 최상위 계층 (Throwable - Exception)
자바의 모든 예외는 Throwable 클래스를 상속받으며, 실무적인 예외 처리는 최상위인 Exception 클래스를 중심으로 이루어집니다.
Checked vs Unchecked Exception의 핵심 차이
무분별하게 최상위 Exception으로 throws를 던지거나, 적절한 사후 조치(Action) 없이 예외를 방치하면 시스템 에러의 정확한 위치와 원인을 파악하기가 극도로 어려워집니다. 예외는 컴파일 시점과 런타임 시점의 동작 방식에 따라 두 가지로 엄격히 분류됩니다.
-
Checked Exception (예:
IOException****,SQLException****) -
Exception을 직접 상속받는 예외들입니다. -
컴파일 시점에 예외 처리(
try-catch또는throws)를 강제합니다. -
트랜잭션 주의점: 스프링 환경에서 기본적으로 롤백(Rollback)되지 않고 커밋되므로 설계 시 주의가 필요합니다.
-
Unchecked / Runtime Exception (예:
NullPointerException****,IllegalArgumentException****) -
RuntimeException을 상속받는 예외들입니다. -
컴파일 시점에 예외 처리를 강제하지 않으며, 런타임 중에 예외가 발생하면 별도의 처리가 없을 시 스레드가 중단되거나 시스템이 멈출 수 있습니다.
-
트랜잭션 주의점: 스프링 환경에서 예외 발생 시 기본적으로 트랜잭션 롤백이 수행됩니다.
2. 실무적인 해결책: 커스텀 예외와 전역 예외 처리 (AOP)
예외 처리의 파편화를 막고, 클라이언트에게 일관된 에러 응답 메세지를 전달하기 위해 커스텀 예외(Custom Exception)와 AOP 관점의 전역 예외 핸들러(Global Exception Handler)를 결합하여 시스템을 관리합니다.
전역 예외 처리의 가치
- 클라이언트 응답 용이성: 포맷이 통일된 에러 객체를 반환하여 클라이언트(프론트엔드/외부 API)가 에러를 일관되게 처리할 수 있도록 돕습니다.
- 커스텀 예외 관리의 비결: 커스텀 예외가 무분별하게 많아지면 클래스가 난립하여 오히려 관리가 어려워집니다. 따라서 공통적인 도메인 에러(예: 내부 서버 에러, 잘못된 요청 에러, 리소스 부재 에러)를 대표하는 몇 개의 커스텀 예외만 생성하고, 내부 메세지나 에러 코드로 상세 내용을 분기하는 것이 효율적입니다.
- 로깅(Logging)의 중요성: 핸들러 내부에서 반드시 에러 로그(
log.error)를 남겨 시스템 에러의 정확한 위치(Stack Trace)와 이유를 추적할 수 있도록 해야 합니다.
[예시]
[1] 대표 커스텀 예외 클래스 설계
클래스의 난립을 막기 위해 상위 커스텀 예외를 두고, 생성 시점에 ErrorCode를 주입받도록 설계합니다.
Java
// 최상위 커스텀 예외 (RuntimeException을 상속받아 Unchecked 예외로 설정)
// ErroCode는 커스텀 메세지를 담기위해 만든 class
public class BusinessException extends RuntimeException{
private final ErrorCode errorCode;
public BusinessException(ErrorCode errorCode){
super(errorCode.getMessage());
this.errorCode = errorCode;
}
public BusinessException(ErrorCode errorCode, String detailMessage){
super(detailMessage);
this.errorCode = errorCode;
}
public ErrorCode getErrorCode(){
return errorCode;
}
}// 특정 도메인 예외는 BusinessException을 상속받아 명칭만 명확히 부여
public class EntityNotFoundException extends BusinessException{
public EntityNotFoundException(String message){
super(ErrorCode.RESOURCE_NOT_FOUND, message);
}
}[2] AOP 기반 전역 예외 핸들러 (@RestControllerAdvice)
컨트롤러 레이어 하위에서 발생하는 모든 예외를 인터셉트하여 로그를 남기고 응답을 제어합니다.
Java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler{
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
// 개발자가 정의한 비즈니스 커스텀 예외 처리
@ExceptionHandler(BusinessException.class)
protected ResponseEntity<ErrorResponse> handleBusinessException(BusinessException e){
ErrorCode errorCode = e.getErrorCode();
// 에러 위치와 원인 파악을 위해 로그 기록 수행
log.error("BusinessException 발생 - Code: {}, Message: {}", errorCode.getCode(), e.getMessage());
ErrorResponse response = new ErrorResponse(errorCode, e.getMessage());
return new ResponseEntity<>(response, errorCode.getStatus());
}
// 예측하지 못한 시스템 최상위 예외 처리 (Exception.class)
@ExceptionHandler(Exception.class)
protected ResponseEntity<ErrorResponse> handleException(Exception e){
// Stack Trace를 전체 출력하여 심각한 서버 에러의 위치 파악 용이성 확보
log.error("알 수 없는 시스템 예외 발생", e);
ErrorResponse response = new ErrorResponse(ErrorCode.INTERNAL_SERVER_ERROR);
return new ResponseEntity<>(response, ErrorCode.INTERNAL_SERVER_ERROR.getStatus());
}
}[참고] 트랜잭션(Transaction) 관리와 롤백(Rollback) 전략
예외 처리 시 에러 메시지를 찾고 전송하는 것만큼 중요한 것이 데이터의 일관성을 지키는 후처리(롤백) 작업입니다.
스프링의 기본 롤백 규칙 (Declarative Transaction)
스프링의 @Transactional 어노테이션은 기본적으로 **RuntimeException**과 Error****가 발생했을 때만 트랜잭션을 롤백합니다. 앞서 언급한 Checked Exception이 발생하면 개발자가 예외를 인지하고 복구할 수 있다고 판단하여 데이터를 그대로 Commit 해버립니다.
데이터 손실 방지를 위한 롤백 제어 전략
만약 Checked Exception이 발생하는 비즈니스 로직(예: 외부 파일 I/O 작업 실패, 특정 SQL 연동 예외 등)에서 전체 데이터를 안전하게 롤백해야 한다면, 다음과 같은 두 가지 방법으로 후처리를 통제해야 합니다.
- 방법 1:
rollbackFor속성 명시 (추천)@Transactional(rollbackFor = Exception.class)처럼 명시적 설정을 추가하여 Checked Exception 발생 시에도 무조건 트랜잭션이 롤백되도록 유도합니다. - 방법 2: Unchecked 예외로 전환(Wrapping)
Checked Exception이 발생하는 구간을
try-catch로 감싸서 catch 블록 내부에서 방어적인 비즈니스 로직을 수행하거나, 시스템이 인지할 수 있는BusinessException으로 재투척(throw)하여 자연스럽게 롤백이 동작하도록 아키텍처를 유연하게 가져갑니다.
@Service
public class OrderService{
// Checked 예외까지 모두 포함하여 안전하게 트랜잭션 후처리(롤백)를 수행하도록 지정
@Transactional(rollbackFor = Exception.class)
public void processOrder(OrderDto orderDto){
// 1. 데이터 저장 로직 수행
orderRepository.save(orderDto.toEntity());
// 2. 외부 연동 또는 파일 생성 작업 (Checked Exception 발생 가정)
try {
fileService.createOrderReceipt(orderDto);
} catch (IOException e) {
// 로그를 남겨 정확한 에러 위치 및 사유 파악
log.error("주문 영수증 파일 생성 중 오류 발생 - 주문번호: {}", orderDto.getOrderId(), e);
// Unchecked 예외로 변환하여 던짐으로써 트랜잭션 롤백 트리거 작동
throw new BusinessException(ErrorCode.DATABASE_TRANSACTION_ERROR, "영수증 처리 실패로 주문이 취소됩니다.");
}
}
}
댓글
GitHub 계정으로 의견이나 질문을 남길 수 있습니다.