[Java] 자바의 예외(2) - 예외를 처리하는 방법, Checked Exception과 Unchecked Exception은 언제 사용해야 할까?

sewonK·2022년 10월 4일
1

본 글은 자바의 예외(1)과 이어지는 글입니다.

2. 일반적인 예외처리 전략

Error의 경우 JVM에서 주로 발생시키기 때문에 애플리케이션 코드단에서 잡으려고 하면 안됩니다. catch 블록으로 잡아봤자 대응 방법이 없기 때문에 애플리케이션에서 Error는 신경쓰지 않아도 됩니다.

checked, unchecked 예외를 빠르게 복구/회피/전환하는 방법에 대해 알아보겠습니다.

(1) 예외 복구하기

예외 상황을 파악하고 문제를 해결하여 정상 상태로 돌려놓는 방법입니다.

정해진 횟수만큼 재시도하는 등으로 문제를 해결하는 것입니다. (Spring의 @Retryable을 사용하면 편리하게 예외가 발생했을 때 재시도하는 로직을 구현할 수 있습니다.)

예시로, Spring Retry template의 doExecute 로직은 아래와 같이 되어있습니다.

while (canRetry(retryPolicy, context) && !context.isExhaustedOnly()) {

    try {
        ...로깅...
        T result = retryCallback.doWithRetry(context);
		doOnSuccessInterceptors(retryCallback, context, result);
		return result;
    }
    catch (Throwable e) {
        .. 에러 로깅...
    }

    if (state != null && context.hasAttribute(GLOBAL_STATE)) {
        break;
    }
}

if (state == null && this.logger.isDebugEnabled()) {
    this.logger.debug(
            "Retry failed last attempt: count=" + context.getRetryCount());
}

exhausted = true;
return handleRetryExhausted(recoveryCallback, context, state);

while문을 돌면서 재시도하며, 특정 조건에 해당할 경우 while문을 빠져나와 복구 실패 처리를 하고 있습니다.

(2) 예외처리 회피하기

1편의 throws 부분에서 설명했던 것처럼, 예외를 발생시키는 메서드가 아닌 그 메서드를 호출하는 호출자에서 예외를 처리하도록 하는 것입니다. throws를 통해 checked exception을 호출자가 try-catch 또는 throws로 예외를 처리하도록 강제하는 방법입니다.

회피하는 방법으로는 throws 키워드를 통해 알아서 호출자에게 예외를 책임지도록 하는 방법이 있고, 두번째로는 일단 catch에서 예외를 잡은 뒤 로그를 남기는 등의 동작 후 다시 예외를 던지는(rethrow) 방법이 있습니다.

(3) 예외 전환하기

예외 전환은 개발자가 의도한 적절한 예외로 전환하는 것입니다.

1편에서 Exception을 BalanceInsufficientException으로 전환한 것처럼, 예외에 적절한 의미를 부여하기 위해 예외를 전환할 수 있습니다.

예외를 전환할 때에는
throw new RuntimeException(new CustomCheckedException("checked예외")); 처럼 중첩 예외로 만들어 getCause() 메서드로 처음 발생한 예외를 확인할 수 있도록 던지는 것이 좋습니다.

또한 복구 불가능한 예외라면 예외 처리를 강제하는 checked exception 을 unchecked exception으로 포장하여 불필요한 예외 처리 로직이 들어가지 않도록 하는 것이 좋습니다.

3. 효과적인 예외처리 전략

모든 예외는 적절하게 복구되든지 아니면 작업을 중단시키고 운영자 또는 개발자에게 분명하게 통보돼야 한다.

토비의 스프링 서적에서는 예외 처리의 핵심 원칙으로 위와 같이 설명하고 있습니다.

여기서 적절하게 복구되는 경우에 주목하여 예외처리 전략을 어떻게 세우면 좋을지 고민해보았습니다.

적절하게 복구된다는 것은, 예외를 발생시키는 잘못된 요청을 재시도하는 방법 등으로 코드를 통해서 정상인 상태로 해결하는 것을 의미합니다. 토비의 스프링의 예외 처리 전략에 기술된 내용에 따르면, 이전의 독립형 애플리케이션에서는 통제 불가능한 시스템 예외라 할지라도 작업이 중단되지않도록 상황을 복구해야 했다고 합니다.

그러나 최근 대두되는 Stateless한 HTTP API를 사용하는 경우를 살펴봅시다. 수많은 사용자가 동시에 요청을 보내며 하나의 요청은 독립적인(Stateless한) 작업으로 취급됩니다. 하나의 요청에서 예외가 발생했다면, 예외 상황을 복구할 수 있는 방법이 없습니다. HTTP API에서는 잘못된 요청 및 서버 에러의 경우 400(Bad Request), 404(Not Found), 500(Internal Server Error) 등 에러코드를 반환하는 방식으로 응답이 종료되며 클라이언트는 이러한 오류코드를 기반으로 새로운 요청을 보내는 등의 작업을 할 뿐입니다.

잘못한 요청으로부터 복구나 회복이 불가능하다면, 해당 작업을 중지시키고 예외를 발생시켜 로그를 쌓고 운영자나 개발자에게 빠르게 통보하는 편이 낫습니다. 그렇다면 checked exception과 unchecked exception는 어떤 상황에서 어떻게 사용하는 것이 바람직할까요?

정답은 없지만 checked exception을 사용하는 것을 되도록 지양해야하는 것이 좋다고 합니다. 가장 큰 이유는 Open Closed Principle(OCP)에 위배된다는 것입니다.

checked 예외를 사용할 경우에는 반드시 예외 처리 로직(try-catch or throws)을 포함해야합니다. 만약 메서드에서 예외를 throws 키워드를 통해 상위 메서드로 넘긴다면, 호출자는 메서드를 사용하는 것으로 인해 예외 처리 로직(try-catch or throws)을 추가해야 합니다(수정에 있어서 닫혀있지 않음). 또한 호출자에서 예외를 처리하지 않고 상위 메서드로 예외를 넘기는 경우 처리하는 메서드 계층까지의 모든 메서드의 시그니처를 throws를 포함하도록 수정해야만 합니다. 이는 상위 레벨 메서드에서 하위 레벨 메서드의 디테일을 포함하는 것으로 캡슐화에 반한다고 할 수 있습니다.

오라클 자바 공식 문서에서는 "클라이언트가 예외로부터 회복하기를 기대한다면 checked exception으로 만들고, 그렇지 않다면 unchecked exception으로 만들어라" 라고 제안하고 있습니다.

다시 돌아가서, HTTP API가 아닌 경우 대체 회복 가능하다는 의미는 무엇일까 헷갈리기 시작했습니다.

구글링 중 흥미로운 예시가 있어 가져와봤습니다.

<예시 상황>
1. 문자열을 신용카드 숫자로 파싱하는 팩토리 메서드를 디자인하는 상황입니다.
2. 팩토리 메서드는 문자열을 파싱하여 CreditCardNumber 객체를 생성합니다.
3. 팩토리 메서드를 사용하는 클라이언트 중 GUI가 있어, 한 개발자는 이렇게 생각했습니다.
"신용카드 숫자를 잘못 입력하는 실수를 사용자가 저질렀다면, 이것은 회복가능한 에러입니다. 왜냐하면 GUI로 사용자에게 재입력받을 수 있기 때문이죠. checked 예외를 사용할 겁니다."
4. 이 개발자는 BadCreditCardNumberException이라는 checked예외를 만들어 던졌습니다.
5. 2주 뒤, 한 개발자는 신용카드 숫자를 문자열로 db에 저장해야했습니다.
6. db를 읽을 때, 개발자는 문자열을 CreditCardNumber 객체로 변환해야했고 마침 생성한 팩토리 메서드를 재사용하기로 했습니다.
7. 그러나 팩토리 메서드로 객체를 생성하려했던 개발자는 db에 저장되어있던 형식에 맞지 않는 문자열로 인해 DAO에서 BadCreditCardNumber 예외를 잡고 맙니다.
8. 이는 원치 않았던 상황이었고, DAO에서는 회복 불가능한 치명적 에러였습니다 ...

이처럼 회복 가능하다고 생각한 상황에서도 회복 불가능한 상황이 발생할 수 있기 마련이라는 걸 이 예시를 통해 깨달을 수 있었습니다.

이제는 토비의 스프링 서적에서

"예전에는 복구할 가능성이 조금이라도 있다면 checked 예외로 만든다고 생각했는데, 지금은 항상 복구할 수 있는 예외가 아니라면 일단 unchecked 예외로 만드는 경향이 있다."

라고 이야기하는 것의 의미를 조금은 이해할 수 있게 되었습니다!😋

📄 참고

  1. spring-retry/RetryTemplate github source

  2. java-checked-unchecked-exceptions

  3. checked exception example

0개의 댓글