Exception handling for CompletableFuture

CompletableFuture 에서 발생할 수 있는 예외를 처리하는 방법에 대해 이리저리 고민하다 글을 써 본다.

예외 처리

예외는 기본적으로 exceptionally() 에서 처리한다.

// kotlin
CompletableFuture
    .supplyAsync<Int> {
        throw NullPointerException("")
    }
    .exceptionally {
        return@exceptionally if (it.cuase is NullPointerException) 0 else -1
    }

exceptionally()Throwable 타입을 받아서 정상 값을 반환하는 메소드를 파라미터로 받는다.

보통 예외를 삼키는 방식으로 사용할 수 있다.

예외 통과 시키기

만약 특정 예외만 핸들링하고 나머지는 그대로 예외를 통과 시키고 싶은 경우에는 뭔가 귀찮은 작업을 해야 한다.

// kotlin
CompletableFuture
    .supplyAsync<Int> {
        throw NullPointerException("")
    }
    .exceptionally {
        if (it.cuase is NullPointerException) {
            return@exceptionally 0
        } else {
            throw it.cause
        }
    }

간단하다. 하지만 이 간단한 것이 자바에서는 안 된다.

// java
CompletableFuture.<Integer>supplyAsync(
    () -> {
        throw new NullPointerException("");
    })
    .exceptionally(t -> {
         if (t.getCause() instanceof NullPointerException) {
             return 0;
         } else {
             throw t.getCause(); // 컴파일 에러
         }
    });

자바에서 예외를 통과 시키려면 뭔가 복잡한 일을 해야 한다.

다음 예제와 같이 handle() 을 사용하여

값을 한 번 CompletableFuture로 감싼다. 이유는 정상 케이스와 예외 케이스를 모두 핸들링 하기 위해서다.

그 이후,CompletableFuture 를 unwrap 하여 원래 값으로 만든다.

// java
CompletableFuture.<Integer>supplyAsync(
    () -> {
        throw new NullPointerException("");
    })
    .handle((v, t) -> {
        if (v != null) return CompletableFuture.completedFuture(v);
        if (t.getCause() instanceof NullPointerException) {
            return CompletableFuture.completedFuture(0);
        } else {
            return CompletableFuture.failedFuture(t.getCause()); // java9
        }
    })
    .thenCompose(a -> a); // CompletableFuture<CompletableFuture<Integer>> -> CompletableFuture<Integer>

exceptionallyCompose

java12 에 exceptionallyCompose가 추가되었다. 이걸로 해결 될 듯.

결론 : 코틀린을 사용하자.

Written on December 10, 2019