CodeGym /행동 /JAVA 25 SELF /예외 체이닝 (Exception Chaining)

예외 체이닝 (Exception Chaining)

JAVA 25 SELF
레벨 24 , 레슨 2
사용 가능

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();
}
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION