홈 › 고급 › 09 / 10

CompletableFuture 심화와 가상 스레드 실전

어느 스레드에서, 어떤 풀로, 실패하면 어떻게
섹션 7진행 0 / 10

6. 연습 문제

문제 1

예제 2 의 measure 를 CPU 작업으로 바꿔 보세요. api.slow 대신 1억 번 덧셈 루프를 돌리고 세 executor 의 시간을 비교하면 어떤 순서가 나오는지, 그 이유를 설명하세요.

정답 보기

commonPool 과 고정 풀 20 은 비슷하고, 가상 스레드는 같거나 약간 느립니다. 캐리어를 2개로 제한한 이 예제에서는 가상 스레드가 가장 느립니다.

CPU 작업은 기다리는 구간이 없어 캐리어에서 내려올 기회가 없습니다. 코어 8개면 어떤 방식이든 8개 이상 동시에 계산할 수 없고, 가상 스레드는 스케줄링 비용만 더해집니다. 가상 스레드의 이득은 I/O 대기에서만 나옵니다.

문제 2

Retry.withBackoff 에 "재시도할 예외 종류" 를 지정하는 인자를 추가하세요. IllegalStateException 만 재시도하고 IllegalArgumentException 은 즉시 실패해야 합니다.

정답 보기
java
public static <T> CompletableFuture<T> withBackoff(Supplier<T> task, int maxAttempts, long firstDelayMs,
                                                   Executor executor, Class<? extends Throwable> retryOn) {
    return attempt(task, 1, maxAttempts, firstDelayMs, executor, retryOn);
}
// attempt 의 handle 안에서
Throwable cause = ex instanceof CompletionException ? ex.getCause() : ex;
if (!retryOn.isInstance(cause) || n >= max) return CompletableFuture.<T>failedFuture(cause);

잘못된 입력처럼 다시 해도 같은 결과인 예외는 재시도하면 안 됩니다. Spring Retry 의 @Retryable(retryFor = ...) 이 같은 구분입니다.

문제 3

예제 7 에서 세마포어 획득에 100ms 타임아웃을 걸어, 기다리다 못 얻은 작업은 "포기" 로 세어 보세요. 200개 중 몇 개가 포기되는지 예측하고 실행으로 확인하세요.

정답 보기
java
AtomicInteger giveUp = new AtomicInteger();
if (!connections.tryAcquire(100, TimeUnit.MILLISECONDS)) { giveUp.incrementAndGet(); return; }
try { FakeApi.sleep(20); } finally { connections.release(); }

100ms 동안 10개씩 20ms 마다 처리되므로 약 50~60개가 통과하고 나머지 140개 안팎이 포기됩니다. 이것이 HikariCP connection-timeout 에 걸려 예외가 쏟아지는 상황의 축소판입니다. 스레드를 늘려서 해결되지 않고, 커넥션을 늘리거나 유입을 줄여야 합니다.