홈 › 고급 › 09 / 10

CompletableFuture 심화와 가상 스레드 실전

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

4. 응용 변형 예제

변형 1: MDC 를 비동기 단계로 옮기기

ThreadLocal 은 따라가지 않으므로 호출 시점에 값을 잡아 두고 단계 안에서 다시 넣습니다.

java
String reqId = MDC.get("reqId");                       // 호출 스레드에서 캡처
CompletableFuture.supplyAsync(() -> {
    MDC.put("reqId", reqId);
    try { return service.call(); } finally { MDC.clear(); }
}, executor);

Spring 에서는 TaskDecorator 로 이 코드를 executor 에 한 번만 등록합니다. 매 호출마다 손으로 쓰는 것보다 실수가 적습니다.

변형 2: 부분 실패를 값으로 모으기

여러 API 를 동시에 불러 일부가 실패해도 나머지를 쓰려면 각 future 를 handle 로 감싼 뒤 allOf 합니다.

java
List<CompletableFuture<Optional<String>>> fs = names.stream()
        .map(n -> CompletableFuture.supplyAsync(() -> api.slow(n, 100), ex)
                .orTimeout(500, TimeUnit.MILLISECONDS)
                .handle((v, e) -> Optional.ofNullable(v)))
        .toList();
CompletableFuture.allOf(fs.toArray(CompletableFuture[]::new)).join();
List<String> ok = fs.stream().map(CompletableFuture::join).flatMap(Optional::stream).toList();

allOf 는 하나라도 실패하면 예외로 끝나므로, 그 전에 각 future 에서 예외를 값으로 바꿔 두는 것이 요령입니다.

변형 3: 가상 스레드에 이름 붙이기

기본 가상 스레드는 이름이 없어 로그에서 구분이 어렵습니다. 팩토리로 접두어와 번호를 붙입니다.

java
ThreadFactory tf = Thread.ofVirtual().name("api-", 0).factory();
ExecutorService ex = Executors.newThreadPerTaskExecutor(tf);   // api-0, api-1, ...

변형 4: 순차 의존 호출은 thenCompose

주문 조회 뒤 그 결과로 회원을 조회하는 식의 의존 관계는 thenApply 로 쓰면 CompletableFuture<CompletableFuture<User>> 가 됩니다. thenCompose 가 한 겹을 벗깁니다.

java
CompletableFuture<User> user = findOrder(id)                    // CF<Order>
        .thenCompose(order -> findUser(order.userId()));        // CF<User>