Contents
see List개요
Java 23에서 정식 도입된 Structured Concurrency와 Scoped Values는 동시성 프로그래밍의 복잡성을 획기적으로 줄여주는 혁신적인 기능입니다. 기존의 ExecutorService나 ThreadLocal이 가진 한계를 극복하고, 더 안전하고 효율적인 멀티스레드 프로그래밍을 가능하게 합니다.
이 글에서는 Structured Concurrency의 핵심 개념과 Scoped Values의 활용법을 실전 예제와 함께 상세히 알아보겠습니다.
Structured Concurrency 핵심 개념
Structured Concurrency는 비동기 작업의 생명주기를 명확하게 관리하는 패러다임입니다. 전통적인 ExecutorService는 작업을 제출한 후 완료를 추적하기 어렵고, 예외 처리가 복잡했습니다.
StructuredTaskScope는 부모 스레드가 자식 스레드를 완전히 제어하며, 부모가 종료되면 모든 자식도 자동으로 종료됩니다. 이는 리소스 누수를 원천적으로 방지합니다.
기본 사용 예제
import java.util.concurrent.StructuredTaskScope;
public class WeatherService {
record Weather(String temperature, String humidity) {}
public Weather fetchWeather(String city) throws Exception {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
var tempTask = scope.fork(() -> fetchTemperature(city));
var humidityTask = scope.fork(() -> fetchHumidity(city));
scope.join(); // 모든 작업 완료 대기
scope.throwIfFailed(); // 실패 시 예외 전파
return new Weather(tempTask.get(), humidityTask.get());
}
}
private String fetchTemperature(String city) {
// 외부 API 호출 시뮬레이션
return "25°C";
}
private String fetchHumidity(String city) {
return "60%";
}
}
ShutdownOnSuccess 전략
여러 작업 중 하나만 성공하면 되는 경우 ShutdownOnSuccess를 사용합니다. 첫 번째 성공 시 나머지 작업을 즉시 취소하여 리소스를 절약합니다.
public class FastestResponseService {
public String getQuote(String symbol) throws Exception {
try (var scope = new StructuredTaskScope.ShutdownOnSuccess()) {
scope.fork(() -> fetchFromExchange1(symbol));
scope.fork(() -> fetchFromExchange2(symbol));
scope.fork(() -> fetchFromExchange3(symbol));
scope.join();
return scope.result(); // 가장 먼저 성공한 결과 반환
}
}
private String fetchFromExchange1(String symbol) throws Exception {
Thread.sleep(100);
return "Exchange1: $150";
}
private String fetchFromExchange2(String symbol) throws Exception {
Thread.sleep(50);
return "Exchange2: $151";
}
private String fetchFromExchange3(String symbol) throws Exception {
Thread.sleep(200);
return "Exchange3: $149";
}
}
Scoped Values 실전 활용
Scoped Values는 ThreadLocal의 현대적 대안으로, 불변성과 명확한 스코프를 보장합니다. ThreadLocal은 상태를 변경할 수 있어 예측 불가능한 버그를 유발했지만, Scoped Values는 한 번 바인딩되면 스코프 내에서 불변입니다.
기본 선언과 사용
import java.lang.ScopedValue;
public class UserContext {
// Scoped Value 선언 (불변)
public static final ScopedValue USER_ID = ScopedValue.newInstance();
public static final ScopedValue REQUEST_ID = ScopedValue.newInstance();
public void handleRequest(String userId, String requestId) {
// where() 메서드로 값 바인딩
ScopedValue.where(USER_ID, userId)
.where(REQUEST_ID, requestId)
.run(() -> processRequest());
}
private void processRequest() {
// 어디서든 현재 컨텍스트 접근 가능
String currentUser = USER_ID.get();
String currentRequest = REQUEST_ID.get();
System.out.println("Processing request " + currentRequest +
" for user " + currentUser);
// 중첩된 메서드 호출에서도 동일한 값 유지
saveAuditLog();
}
private void saveAuditLog() {
String user = USER_ID.get();
String request = REQUEST_ID.get();
System.out.println("Audit: User=" + user + ", Request=" + request);
}
}
Structured Concurrency와 통합
Scoped Values는 Structured Concurrency와 완벽하게 통합되어, 부모 스레드의 컨텍스트가 자식 스레드로 자동 전파됩니다.
public class OrderService {
public static final ScopedValue TENANT_ID = ScopedValue.newInstance();
public void processOrder(String tenantId, String orderId) throws Exception {
ScopedValue.where(TENANT_ID, tenantId).run(() -> {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
// 모든 fork된 작업에서 TENANT_ID 접근 가능
scope.fork(() -> validateInventory(orderId));
scope.fork(() -> processPayment(orderId));
scope.fork(() -> updateShipping(orderId));
scope.join();
scope.throwIfFailed();
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
private Void validateInventory(String orderId) {
String tenant = TENANT_ID.get(); // 부모의 컨텍스트 자동 전파
System.out.println("Validating inventory for tenant: " + tenant);
return null;
}
private Void processPayment(String orderId) {
String tenant = TENANT_ID.get();
System.out.println("Processing payment for tenant: " + tenant);
return null;
}
private Void updateShipping(String orderId) {
String tenant = TENANT_ID.get();
System.out.println("Updating shipping for tenant: " + tenant);
return null;
}
}
활용 팁
- 리소스 관리 자동화: try-with-resources를 통해 StructuredTaskScope가 자동으로 종료되며, 모든 자식 스레드도 정리됩니다.
- 예외 전파 전략: ShutdownOnFailure는 첫 실패 시 즉시 중단, ShutdownOnSuccess는 첫 성공 시 중단합니다.
- 성능 최적화: Scoped Values는 ThreadLocal보다 메모리 효율적이며, 가상 스레드와 궁합이 좋습니다.
- 컨텍스트 전파: 보안 토큰, 트랜잭션 ID, 테넌트 정보 등을 Scoped Values로 관리하면 코드가 깔끔해집니다.
- 디버깅 개선: 구조화된 동시성은 스택 트레이스가 명확하여 디버깅이 용이합니다.
- 가상 스레드와 조합: Java 21의 가상 스레드와 함께 사용하면 수만 개의 동시 작업도 효율적으로 처리할 수 있습니다.
마무리
Java 23의 Structured Concurrency와 Scoped Values는 동시성 프로그래밍의 패러다임을 바꾸는 혁신입니다. ExecutorService의 복잡한 생명주기 관리와 ThreadLocal의 예측 불가능한 상태 공유 문제를 모두 해결합니다.
특히 마이크로서비스 아키텍처에서 요청 컨텍스트를 전파하거나, 병렬 데이터 처리 파이프라인을 구축할 때 그 진가를 발휘합니다. 가상 스레드와 함께 사용하면 고성능 동시성 시스템을 놀랍도록 간결한 코드로 구현할 수 있습니다.
지금 바로 Java 23으로 업그레이드하여 이 강력한 기능들을 프로젝트에 적용해보시기 바랍니다.