본문 바로가기
프로그래밍/Java

[java] exception 처리하기 - 실전편

by 사바라다 2020. 4. 19.

안녕하세요. 우리는 이전 포스팅에서 JVM에서 예외(exception) 처리가 어떻게 이루어 지는지를 살펴보았습니다. 예외를 처리하는 방법으로 예외복구, 예외처리 회피, 예외 전환이 있다라는 알려드렸습니다. 오늘은 실제 코드로 exception을 어떻게 처리하는지 공유하고자 합니다.

예외복구

예외복구는 예외가 발생하더라도 어플리케이션의 로직은 정상적으로 실행이 되게 하도록 처리한다는 의미입니다. 한 예로 통신의 재시도를 들 수 있습니다. 예외가 발생하면 일정 시간동안 대기를 시킨 후 다시 해당 로직을 시도하는 것입니다. 일정 횟수동안 재시도를 이런식으로 진행하며, 그래도 정상적인 응답이 오지 않는 경우 fail 처리하는 로직을 생각할 수 있습니다.

실제로 쓰이는 예제를 보도록 하겠습니다. 아래 예제는 spring-retryRetryTemplate 클래스의 실제 통신을 진행하는 execute 메서드의 retry 부분을 일부 발췌한 것입니다.

/*
* We allow the whole loop to be skipped if the policy or context already
* forbid the first try. This is used in the case of external retry to allow a
* recovery in handleRetryExhausted without the callback processing (which
* would throw an exception).
*/
while (canRetry(retryPolicy, context) && !context.isExhaustedOnly()) {

    try {
        ...로깅...
        return retryCallback.doWithRetry(context);
    }
    catch (Throwable e) {
        .. 에러 로깅...
    }

    /*
        * A stateful attempt that can retry may rethrow the exception before now,
        * but if we get this far in a stateful retry there's a reason for it,
        * like a circuit breaker or a rollback classifier.
        */
    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);

위 코드를 보면 try-catch 구문을 while로 묶어두었으며, while을 도는 횟수는 재시도 정책을 따릅니다. try 구문에서 통신을 시도하며 Throwable로 묶여있기 때문에 error, exception, RuntimeException 어떤 에러가 발생하더라도 catch로 넘어가서 에러관련 처리를 합니다. 그리고 상태판단을 하여 while을 벗어날지 아닐지 확인합니다. 이렇게 retry 횟수만큼 while 구문을 돌면서도 처리도지 못하면 while 문을 벗어나게 되고 Retry failed 라는 메시지와 함께 실패처리하게 됩니다.

예외처리 회피

예외처리 회피는 예외를 직접처리하지 않고 예외가 발생한 메서드를 호출한 메서드에게 예외처리를 위임하는 것입니다. 코드적으로는 상당히 깔끔하게 예외처리가 되지만 예외를 던지는 것이 최선이 방법이라는 확신이 있을 때만 사용해야 합니다.

실제로 쓰이는 예제를 보도록 하겠습니다. File을 읽어서 데이터로 읽으려고 합니다. 그러면 우리는 java에서 일반적으로 FileInputStream을 사용합니다. 해당 예제를 보도록 하겠습니다.

    /**
     * ... 코드에 대한 설명 ...
     *
     * @param      file   the file to be opened for reading.
     * @exception  FileNotFoundException  if the file does not exist,
     *                   is a directory rather than a regular file,
     *                   or for some other reason cannot be opened for
     *                   reading.
     * @exception  SecurityException      if a security manager exists and its
     *               <code>checkRead</code> method denies read access to the file.
     * @see        java.io.File#getPath()
     * @see        java.lang.SecurityManager#checkRead(java.lang.String)
     */
    public FileInputStream(File file) throws FileNotFoundException {
        String name = (file != null ? file.getPath() : null);
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkRead(name);
        }
        if (name == null) {
            throw new NullPointerException();
        }
        if (file.isInvalid()) {
            throw new FileNotFoundException("Invalid file path");
        }
        fd = new FileDescriptor();
        fd.attach(this);
        path = name;
        open(name);
    }

FileInputStream의 생성자 코드를 보시면 File 객체를 생성하거나 받아서 내부에서 작업을 진행합니다. 이때 코드에 따르면 만약 이름이 Null인 경우 NullPointerException을 던지며 파일이 잘못된 경우는 FileNotFoundException을 던집니다. 그리고 이 중 FileNotFoundException만 throws를 통해 예외처리 회피를 하며 해당 객체를 생성하려고 했던 caller에게 exception 처리를 전가합니다. NullPointerException은 throws 하지 않고 왜 FileNotFoundException만 throws하는지는 다음 포스팅에서 한번 알아보도록 하겠습니다.

만약 아래가 FileInputStream을 호출한 caller의 코드는 아래와 같이 이루어질 것입니다.

FileInputStream fi = new FileInputStream(new File("check.csv"));

이렇게 코드를 작성 한 후 컴파일을 하면 컴파일 에러가 발생하며 메시지로 unreported exception FileNotFoundException; must be caught or declared to be thrown를 받습니다. 즉, FileNotFoundException 처리를 작성하지 않았다는 의미입니다.

따라서 해당 메서드에서 try-catch로 처리해 주던지 아니면 throws를 통해서 처리해 주어야합니다. 아래와 같이 처리하면 정상적으로 컴파일되며 오류 발생시 호출한 메서드의 try-catch 처리를 바라보게 됩니다.

try {
    FileInputStream fi = new FileInputStream(new File("check.csv"));
} catch (FileNotFoundException e) {
    e.printStackTrace();
}

예외 전환

예외 전환이란 발생한 예외에 대해서 또 다른 예외로 변경하여 던지는 것을 말합니다. 일반적으로 호출한 쪽에서 예외를 받아서 처리할 때 좀 더 명확하게 인지할 수 있도록 돕기위한 방법입니다. 또한 Checked Exception이 발생했을 경우 이를 Unchecked Exception으로 전환하여 호출한 메서드에서 예외처리를 일일이 선언하지 않아도 되도록 처리할 수 도 있습니다.

spring에서 통신을 추상화한 restTemplate에서 예제를 가져와 보도록 하겠습니다. 통신 메서드를 따라가다보면 doExecute 메서드가 있습니다. 해당 메서드를 한번 보도록 하겠습니다.

/**
* Execute the given method on the provided URI.
* <p>The {@link ClientHttpRequest} is processed using the {@link RequestCallback};
* the response with the {@link ResponseExtractor}.
* @param url the fully-expanded URL to connect to
* @param method the HTTP method to execute (GET, POST, etc.)
* @param requestCallback object that prepares the request (can be {@code null})
* @param responseExtractor object that extracts the return value from the response (can be {@code null})
* @return an arbitrary object, as returned by the {@link ResponseExtractor}
*/
@Nullable
protected <T> T doExecute(URI url, @Nullable HttpMethod method, @Nullable RequestCallback requestCallback,
        @Nullable ResponseExtractor<T> responseExtractor) throws RestClientException {

    Assert.notNull(url, "URI is required");
    Assert.notNull(method, "HttpMethod is required");
    ClientHttpResponse response = null;
    try {
        ClientHttpRequest request = createRequest(url, method);
        if (requestCallback != null) {
            requestCallback.doWithRequest(request);
        }
        response = request.execute();
        handleResponse(url, method, response);
        return (responseExtractor != null ? responseExtractor.extractData(response) : null);
    }
    catch (IOException ex) {
        String resource = url.toString();
        String query = url.getRawQuery();
        resource = (query != null ? resource.substring(0, resource.indexOf('?')) : resource);
        throw new ResourceAccessException("I/O error on " + method.name() +
                " request for \"" + resource + "\": " + ex.getMessage(), ex);
    }
    finally {
        if (response != null) {
            response.close();
        }
    }
}

메서드의 내용은 실제 request를 생성하고 외부시스템에 대한 통신을 실행하는 것입니다. 위의 메서드의 catch 부분을 보면 checked Exception인 IOException이 에 대한 처리가 있습니다. 이 처리는 request#execute()에서 발생할 수 있는 예외입니다. 해당 예외를 받아서 RunTimeException을 상속받은 unchecked Exception인 ResourceAccessException으로 치환하여 예외를 처리합니다. 따라서 해당 메서드를 호출하는 호출 메서드들은 IOException에 대한 처리를 별도로 해주지 않아도 됨을 뜻합니다.

마무리

오늘은 이렇게 java exception을 처리하는 3가지 방법인 예외복구, 예외회피, 예외전환에 대해서 알아보는 시간을 가졌습니다. 화련한 기술도 좋지만 단단하게 기본을 다지는 것 역시 중요함을 다시한번 느꼈습니다.

도움이 되셨다면 하트와 광고 클릭 부탁드려요.

감사합니다.

참조

http://www.nextree.co.kr/p3239/

https://docs.oracle.com/javase/8/docs/api/

https://docs.spring.io/spring-framework/docs/current/javadoc-api/

댓글