4. 응용 변형 예제
변형 1: MDC 를 비동기 단계로 옮기기
ThreadLocal 은 따라가지 않으므로 호출 시점에 값을 잡아 두고 단계 안에서 다시 넣습니다.
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 합니다.
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: 가상 스레드에 이름 붙이기
기본 가상 스레드는 이름이 없어 로그에서 구분이 어렵습니다. 팩토리로 접두어와 번호를 붙입니다.
ThreadFactory tf = Thread.ofVirtual().name("api-", 0).factory();
ExecutorService ex = Executors.newThreadPerTaskExecutor(tf); // api-0, api-1, ...변형 4: 순차 의존 호출은 thenCompose
주문 조회 뒤 그 결과로 회원을 조회하는 식의 의존 관계는 thenApply 로 쓰면 CompletableFuture<CompletableFuture<User>> 가 됩니다. thenCompose 가 한 겹을 벗깁니다.
CompletableFuture<User> user = findOrder(id) // CF<Order>
.thenCompose(order -> findUser(order.userId())); // CF<User>