Java/Java

예외 처리 방법(Exception Handling) - 복구, 회피, 전환

ysk(0soo) 2022. 12. 7. 13:43

예외 처리 방법(Exception Handling)

예외를 처리하는 방법에는 예외 복구, 예외 처리 회피, 예외 전환 방법이 있다.

 

예외 복구

  • 예외 상황을 파악하고 문제를 해결해서 정상 상태로 돌려놓는 방법
  • 예외를 잡아서 일정 시간, 조건만큼 대기하고 다시 재시도를 반복한다.
  • 최대 재시도 횟수를 넘기게 되는 경우 예외를 발생시킨다.
final int MAX_RETRY = 100;
public Object someMethod() {
    int maxRetry = MAX_RETRY;
    while(maxRetry > 0) {
        try {
            ...
        } catch(SomeException e) {
            // 로그 출력. 정해진 시간만큼 대기한다.
        } finally {
            // 리소스 반납 및 정리 작업
        }
    }
    // 최대 재시도 횟수를 넘기면 직접 예외를 발생시킨다.
    throw new RetryFailedException();
}

예외복구는 예외가 발생하더라도 어플리케이션의 로직은 정상적으로 실행이 되게 하도록 처리한다는 의미 .

예외가 발생하면 일정 시간동안 대기를 시킨 후 다시 해당 로직을 재시도.

일정 횟수동안 재시도를 이런식으로 진행하며, 그래도 정상적인 응답이 오지 않는 경우 fail 처리하는 로직을 생각할 수 있다.

대부분의 상황에서 예외를 복구할 수 있는 경우는 거의 없기 때문에 자주 사용되지 않는다.

 

예외 처리 회피

  • 예외 처리를 직접 담당하지 않고 호출한 쪽으로 던져 회피하는 방법
  • 그래도 예외 처리의 필요성이 있다면 어느 정도는 처리하고 던지는 것이 좋다.
  • 긴밀하게 역할을 분담하고 있는 관계가 아니라면 예외를 그냥 던지는 것은 무책임하다.
// 예시 1
public void add() throws SQLException {
    // ...생략
}

// 예시 2 
public void add() throws SQLException {
    try {
        // ... 생략
    } catch(SQLException e) {
        // 로그를 출력하고 다시 날린다!
        throw e;
    }
}

예외 되던지기 - re-throwing

  • 한 메서드에서 발생할 수 있는 예외가 여럿인 경우 몇 개는 try-catch문을 통해서 메서드 내에서 자체적으로 처리
  • 나머지는 선언부에 지정하여 호출한 메서드에서 처리하도록 함으로써 양쪽에서 나눠서 처리되도록 함
  • 단 하나의 예외에 대해서도 예외가 발생한 메서드와 호출한 메서드 양쪽에서 처리하도록 할 수 있다.
    • 예외를 처리한 후에 인위적으로 다시 발생시키는 방법
void someMethod() throws Exception; // 선언부에 예외 지정
// 다음과 같이도 가능

void someMethod() throws Exception1, Exception2;

 

예외를 처리한 후에 인위적으로 다시 발생시키는 방법

public void someMethod() throws Exception {
  try {
    callMethod();
  } catch (Exception e) {
    System.out.prinltln(e.getName() + "예외 처리");
    throw e;
  }

} 

반환값이 있는 return 문의 경우 try블럭과 catch블럭에서 에외를 처리하고도 return 해야 한다.

public int someMethod() throws Exception {
  try {
    return callMethod();

  } catch (Exception e) {
    System.out.prinltln(e.getName() + "예외 처리");
    return -1; // 또는
    // throw new Exception(); // 대신 예외를 호출한 메서드로 전달
  }
} 
  • finally 문이 잇따면, 최종적으로 finally 블럭 내의 return문 값이 반환된다.

 

예외 전환

  • 예외 회피와 비슷하게 메서드 밖으로 예외를 던지지만, 그냥 던지지 않고 적절한 예외로 전환해서 넘기는 방법
  • 조금 더 명확한 의미로 전달되기 위해 적합한 의미를 가진 예외로 변경한다.
  • 예외 처리를 단순하게 만들기 위해 포장(wrap) 할 수도 있다.
  • 또한 Checked Exception이 발생했을 경우 이를 Unchecked Exception으로 전환하여 호출한 메서드에서 예외처리를 일일이 선언하지 않아도 되도록 처리할 수 도 있다.
// 조금 더 명확한 예외로 던진다.
public void add(User user) throws DuplicateUserIdException, SQLException {
    try {
        // ...생략
    } catch(SQLException e) {
        if(e.getErrorCode() == MysqlErrorNumbers.ER_DUP_ENTRY) {
            throw DuplicateUserIdException();
        }
        else throw e;
    }
}

// 예외를 단순하게 포장한다.
public void someMethod() {
    try {
        // ...생략
    }
    catch(NamingException ne) {
        throw new EJBException(ne);
        }
    catch(SQLException se) {
        throw new EJBException(se);
        }
    catch(RemoteException re) {
        throw new EJBException(re);
        }
}

즉 , 예외 전환이란 발생한 예외에 대해서 또 다른 예외로 변경하여 던지는 것

 

연결된 예외 (chained exception) : 어떤 예외를 다른 예외로 감싸는 것

  • 발생한 예외에 대해서 또 다른 예외로 변경할 때 사용한다 .
  • 언제 사용?
  1. 세부적인 사항을 포괄적인 사항으로 포함시킬 때 사용
  2. cheked 예외를 unchecked 예외로 변경시 사용(try-catch문 사용을 줄일 수 있음)
  • 한 예외가 다른 예외를 발생시킬 수 있다.
  • 예외 A가 예외 B를 발생시키면, A는 B의 원인 예외(cause exception)

다음 예는 SpaceException을 원인 예외로 하는 InstallException을 발생시키는 예제이다.

  • SpaceException이 InstallException 발생시킨다.
try {
    startInstall();
    copyFiles();
} catch (SpaceException e) {
    InstallException ie = new InstallException("설치중 예외발생"); // 예외 생성
    ie.initCause(e); // InstallException의 원인 예외를 SpaceException으로 지정
    throw ie; // InstallException을 발생시킨다.
} catch (MemoryException me) {
    ...
}
  • ie.initCause(e) : InstallException의 발생 원인을 SpaceException로 지정

Throwable initCause(Throwable cause)

  • 지정한 예외(cause)를 원인 예외로 등록
  • Exception클래스의 조상인 Throwable클래스에 정의되어 있기 때문에 모든 예외에서 사용이 가능

Throwable getCause()

  • 원인 예외를 반환

 

발생한 예외를 그냥 처리하면 될 텐데, 왜 원인 예외로 등록해서 다시 예외를 발생시킬까?

1. 큰 분류의 예외로 묶어서 다루기 위함

2. 상속관계를 무시할 수 있음

3. checked 예외를 unchecked 예외로 바꿀 수 있다

try {
    startInstall(); // SpaceException 발생
    copyFiles();
} catch (InstallException e) { // InstallException은
    e.printStackTrace(); // SpaceException과 MemoryExcpetion의 조상
}

위와 같이 InstallException을 SpaceException과 MemoryException의 조상으로 해서 catch 블럭을 작성하면, 실제로 발생한 예외가 어떤 것인지 알 수 없다는 문제가 있다.

  • SpaceException과 MemoryException의 상속관계를 변경해야 한다는 것도 부담이다.
  • 만약 원인 예외를 포함하게 된다면 두 예외는 상속관계가 아니여도 상관없다.

checked 예외를 unchecked 예외로 바꿀 수 있다.

static void startInstall() throws SpaceException, MemoryException {
    if (!enoughSpace()) // 충분한 설치 공간이 없으면
        throw new SpaceException("설치할 공간이 부족합니다.");
    if (!enoughMemory()) // 충분한 메모리가 없으면
        throw new MemoryException("메모리가 부족합니다."); 
}

static void startInstall() throws SpaceException {
    if (!enoughSpace()) // 충분한 설치 공간이 없으면
        throw new SpaceException("설치할 공간이 부족합니다.");
    if (!enoughMemory()) // 충분한 메모리가 없으면
        throw new RuntimeException(new MemoryException("메모리가 부족합니다.")); 
}

MemoryException은 Exception의 자손이므로 반드시 예외를 처리해야하는데, 이 예외를 RuntimeException으로 포장했기 때문에 unchecked 예외가 되었다.

  • 그래서 startInstall()의 선언부에 MemoryException을 선언하지 않아도 된다.

RuntimeException 생성자에는 원인 예외를 등록하는 생성자가 존재한다

RuntimeException(Throwable cause);

 

  • checked 예외를 unchecked 예외로 바꾸면 예외처리가 선택적이 되므로 억지로 예외처리 하지 않아도 된다.

 

정리

  • 예외 복구 전략이 명확하고 복구가 가능하면 Checked Exception을 try-catch로 예외 복구를 하거나, 코드의 흐름을 제어하는 것이 좋다.
  • 또한 Checked Exception이 발생했을 경우 이를 Unchecked Exception으로 전환하여 호출한 메서드에서 예외처리를 일일이 선언하지 않아도 되도록 처리할 수 도 있다.
    • 더 구체적인 UnChecked Exception을 발생시키고 예외에 대한 메시지를 명확하게 전달하는 것이 효과적이다.
  • 무책임하게 상위 메서드에 throw로 예외를 던지는 행위를 하지 않는 것이 좋다.
    • 상위 메서드들의 책임이 그만큼 증가하기 때문이다.
  • Checked Exception은 기본 트랜잭션 속성에서 Rollback을 진행하지 않는다.

참조