1. 소개
이 강의에서는 예외 처리의 중요한 기법인 — 예외 체이닝 (exception chaining)을 다룹니다. 이 기법은 한 예외를 다른 예외로 “감싸”더라도 오류의 근본 원인에 대한 정보를 잃지 않도록 해 줍니다.
실제 애플리케이션에서는 오류가 호출 스택의 깊은 곳 — 예를 들어 데이터베이스, 파일 시스템 또는 네트워크 작업 중 — 에서 발생하는 경우가 흔합니다. 예를 들어 데이터베이스에 접근하는 메서드가 있고 SQLException을 던질 수 있다고 합시다. 그러나 비즈니스 로직 수준에서는 기술적인 세부사항으로 코드를 “어지럽히고” 싶지 않아서 UserManagementException 같은 자체 예외를 던지고자 할 수 있습니다.
단순히 새 예외만 던지면 어떻게 될까요?
try {
// 데이터베이스 관련 작업
} catch (SQLException e) {
throw new UserManagementException("사용자 작업 중 오류");
}
문제:
이 경우 데이터베이스에서 구체적으로 무엇이 발생했는지(그리고 호출 스택까지!)에 대한 정보가 사라집니다. 로그에는 UserManagementException만 보이고, 그 원인이 무엇이었는지는 알 수 없습니다.
2. 해결: 원래 예외를 감싸기 (chaining)
Java에서는 새 예외의 생성자에 원래 예외를 원인(cause)으로 전달하여 한 예외를 다른 예외로 “감쌀” 수 있습니다. 이를 예외 체이닝이라고 합니다.
어떻게 하나요?
대부분의 표준 및 사용자 정의 예외에는 두 번째 매개변수로 Throwable cause를 받는 생성자가 있습니다:
public UserManagementException(String message, Throwable cause) {
super(message, cause);
}
사용 예:
try {
// 데이터베이스 관련 작업
} catch (SQLException e) {
throw new UserManagementException("사용자 작업 중 오류", e);
}
이제 호출 스택(printStackTrace())을 확인해 보면, 여러분의 예외와 근본 원인까지의 전체 체인을 모두 볼 수 있습니다!
3. 예외의 원인 가져오기
Throwable 타입의 모든 객체에는 getCause() 메서드가 있으며, 원래 예외를 반환합니다(또는 원인이 없으면 null).
예:
try {
// ...
} catch (UserManagementException e) {
Throwable cause = e.getCause();
if (cause != null) {
System.out.println("근본 원인: " + cause);
}
e.printStackTrace();
}
왜 필요할까요?
- 디버깅용: 상위 수준에서 “무엇이 잘못되었는지”뿐 아니라 스택 깊은 곳에서 오류가 정확히 어디서 발생했는지도 볼 수 있습니다.
- 로깅용: 오류의 전체 체인을 로그로 남길 수 있습니다.
- 계층 간 정보 전달: 비즈니스 계층은 기술적 예외를 자체 예외로 감싸되, 세부 정보를 잃지 않습니다.
4. 예시: 실제 애플리케이션에서의 예외 체이닝
예를 들어, 데이터베이스에서 사용자를 로드하는 메서드가 있다고 합시다:
public User loadUser(String username) throws UserManagementException {
try {
// SQLException을 던질 수 있는 코드
// ...
} catch (SQLException e) {
throw new UserManagementException("사용자를 로드하지 못했습니다: " + username, e);
}
}
여기서 UserManagementException은 여러분이 정의한 사용자 예외입니다:
public class UserManagementException extends Exception {
public UserManagementException(String message, Throwable cause) {
super(message, cause);
}
}
오류가 발생하면 어떻게 될까요?
- 로그에는 여러분의 예외와 원래의 SQLException이 모든 세부사항과 함께 표시됩니다.
- 필요하다면 getCause()를 통해 근본 원인에 접근할 수 있습니다.
5. 체이닝 시 호출 스택은 어떻게 보이나요
출력 예:
UserManagementException: 사용자를 로드하지 못했습니다: vasya
at UserService.loadUser(UserService.java:15)
...
Caused by: java.sql.SQLException: Connection refused
at ...
여기서 모든 것이 보입니다: 전체 호출 체인, 비즈니스 오류가 어디에서 발생했는지, 어떤 기술적 예외가 원인이었는지까지 확인할 수 있습니다.
6. 실습: 예외 체이닝 구현하기
1단계. 사용자 정의 예외 만들기:
public class UserManagementException extends Exception {
public UserManagementException(String message) {
super(message);
}
public UserManagementException(String message, Throwable cause) {
super(message, cause);
}
}
2단계. 체이닝 사용하기:
try {
// 위험한 작업
} catch (SQLException e) {
throw new UserManagementException("DB 작업 중 오류", e);
}
3단계. 최상위에서 처리하기:
프로그램의 최상위 레벨에서 사용자 정의 예외를 포착하고, 전체 원인 체인과 함께 오류 메시지를 출력합니다.
public class Main {
public static void main(String[] args) {
try {
runUserManagement();
} catch (UserManagementException e) {
System.err.println("오류가 발생했습니다: " + e.getMessage());
// 원인 체인 출력
Throwable cause = e.getCause();
while (cause != null) {
System.err.println("원인: " + cause.getMessage());
cause = cause.getCause();
}
}
}
private static void runUserManagement() throws UserManagementException {
try {
// DB 오류 시뮬레이션
throw new SQLException("DB 연결이 없습니다");
} catch (SQLException e) {
throw new UserManagementException("DB 작업 중 오류", e);
}
}
}
7. 예외 체이닝에서 흔히 하는 실수
실수 1: cause 없이 새 예외를 던짐.
catch (SQLException e) {
throw new UserManagementException("오류", /* cause 없음! */);
}
문제: 근본 원인 정보가 사라집니다.
실수 2: 사용자 정의 예외에 cause를 받는 생성자가 없음.
사용자 정의 예외 클래스에 Throwable cause를 받는 생성자가 없으면 원인을 전달할 수 없습니다 — 직접 추가해야 합니다.
실수 3: 예외를 잡기만 하고, 상위로 전달하지 않음(묵살).
catch (SQLException e) {
// 로그만 남기고 아무 말도 하지 않음
}
문제: 오류가 “사라져”, 프로그램이 잘못된 상태로 계속 실행됩니다.
try {
userService.loadUser("vasya");
} catch (UserManagementException e) {
System.err.println("오류: " + e.getMessage());
if (e.getCause() != null) {
System.err.println("근본 원인: " + e.getCause());
}
e.printStackTrace();
}
GO TO FULL VERSION