Java 26: Lazy Constants란 무엇이며 왜 double-checked locking을 대체하는가 (JEP 526)
Source: Dev.to
Java 26의 JEP 526이란?
JEP 526은 java.lang.LazyConstant 클래스를 도입합니다: 값이 불변이며 최대 한 번만 초기화되는 컨테이너입니다.
| 버전 | 기능 이름 | 비고 |
|---|---|---|
| Java 25 | Stable Values (JEP 502) | API가 더 장황하고 아직 프리뷰 단계 |
| Java 26 | Lazy Constants (JEP 526) | 간소화된 API, 두 번째 프리뷰 |
그가 해결하는 문제는 오래되었습니다:
final은 불변성을 보장하지만 즉시 초기화를 요구합니다.final이 아닌 필드는 늦은 초기화를 허용하지만 동시성 보장을 잃습니다.
LazyConstant는 두 세계를 결합합니다: 원하는 시점에 초기화하지만 한 번만 초기화되며 final과 동일한 안전성을 제공합니다.
왜 double‑checked locking이 존재하고 어떤 문제가 있는가?
모든 Java 개발자는 이런 코드를 작성해 본 적이 있다:
public class MetricRegistry {
private static volatile MetricRegistry instance;
public static MetricRegistry getInstance() {
if (instance == null) {
synchronized (MetricRegistry.class) {
if (instance == null) {
instance = new MetricRegistry();
}
}
}
return instance;
}
}작동하지만, 다음이 필요하다:
volatile- 두 번의
null검사 synchronized블록 하나
코드를 처음 보는 사람은 보통 혼란스러워한다. 정적 내부 클래스를 이용한 대안이 더 안전하지만, 여전히 work‑around이다. 이 옵션들 중 어느 것도 실제 문제에 대한 일급 추상화가 아니다: “필요할 때 한 번만 안전하게 초기화한다”.
LazyConstant가 실제로 어떻게 작동하나요?
Imagine um serviço de pagamento com um cliente HTTP caro de criar, que só faz sentido existir se a rota for chamada:
public class PaymentService {
private final LazyConstant httpClient =
LazyConstant.of(() -> HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build());
public String charge(String orderId, BigDecimal amount) {
HttpClient client = httpClient.get(); // primeira chamada cria o cliente
// usa o client para chamar o gateway
…
}
}HttpClient는httpClient.get()첫 번째 호출 시에 생성됩니다.- 이후
get()호출에서는 JVM이 동일한 값을 반환합니다 (심지어 constant‑folding을 적용할 수도 있습니다). - 구성상 스레드‑안전하며,
synchronized나volatile을 사용할 필요가 없습니다.
Java 25에서 Java 26으로 JEP 526에서 바뀐 점은?
- Java 25 – 저수준 메서드(
setOrThrow,trySet,orElseSet)를 제공하는 API로, 리소스를 동기화 원시 타입으로 변환합니다. - Java 26 –
LazyConstant.of(supplier)만 남깁니다. 초기화 함수를 제공하면 JVM이 나머지를 처리합니다.
컬렉션용 “lazy” 팩토리
lazy 리스트와 맵을 위한 팩토리 메서드가 위치를 옮겨 StableValue가 아니라 List와 Map에서 직접 제공됩니다:
// 각각 독립적으로 초기화되는 10개의 슬롯을 가진 lazy 리스트
List processors = List.ofLazy(10, _ -> new OrderProcessor());
// 정의된 키와 필요 시 초기화되는 값을 갖는 lazy 맵
Map limiters = Map.ofLazy(
Set.of("payments", "refunds", "reports"),
key -> RateLimiter.create(requestsPerSecond(key))
);리스트의 각 슬롯과 맵의 각 엔트리는 각자의 초기화 주기를 가집니다. payments 슬롯은 refunds가 생성되지 않아도 존재할 수 있습니다.
LazyConstant와 일반 final 필드의 차이점은 무엇인가요?
| 특징 | final | LazyConstant |
|---|---|---|
| 초기화 시점 | 생성자에서 (즉시) | .get() 첫 호출 시 |
| 동기화 필요 여부 | 아니오 (이미 스레드‑안전함) | 내부적으로 스레드‑안전 |
| 생성 비용 | 사용되지 않더라도 항상 지불 | 사용될 경우에만 지불 |
| 유연성 | 값은 생성 시점에 준비되어 있어야 함 | 아직 존재하지 않는 리소스에 의존할 수 있음 |
// final obriga inicialização imediata
private final ExpensiveClient client = new ExpensiveClient(); // roda no construtor
// LazyConstant inicializa na primeira chamada a .get()
private final LazyConstant client =
LazyConstant.of(ExpensiveClient::new);초기화된 후, JVM은 LazyConstant를 상수처럼 취급하고 정적 final과 동일한 최적화를 적용할 수 있습니다.
Java 26에서 Lazy Constants를 사용하기 전에 알아야 할 사항은?
- 아직 프리뷰 단계 – 컴파일하고 실행하려면 프리뷰를 활성화하세요:
javac --enable-preview --release 26 PaymentService.java
java --enable-preview PaymentServiceLazyConstantSerializable이 아닙니다. 객체를 직렬화해야 하는 경우 작동하지 않습니다.- 저장된 값은 불변이지만 내부 객체는 가변일 수 있습니다.
LazyConstant는 참조가 한 번만 할당되도록 보장합니다; 이후 객체를 어떻게 다루는지는 여러분의 책임입니다.
LazyConstant를 언제 사용하고 언제 사용하지 않을까?
사용할 때:
- 객체 생성 비용이 높고 항상 필요하지 않을 때.
- 수동으로 동기화를 관리하지 않고도 thread‑safe가 필요할 때.
- 예시: 데이터베이스 연결, HTTP 클라이언트, 설정 캐시, 메트릭 레지스트리.
사용하지 말아야 할 때:
- 간단한
final로 해결되는 경우. - 직렬화가 필요할 때.
- 초기화 시점을 정확히 제어하고 싶을 때(예: 애플리케이션 초기화의 특정 지점 이전).
LazyConstant는 초기화가 첫 번째 .get() 호출 이전에 이루어짐을 보장하지만, 사용자가 선택한 임의의 시점에서는 초기화되지 않습니다.
왜 지금, 아직 프리뷰 단계인 것이 중요한가?
double‑checked locking은 수천 개의 Java 시스템에서 운영 중이다. 누군가 복잡성을 좋아해서가 아니라, 더 나은 대안이 없었기 때문이다. JEP 526은 OpenJDK가 이 문제에 대한 답이다.
이 기능은 아직 최종화되기 전에 변경될 수 있으며, 두 번째 프리뷰는 바로 피드백을 수집하기 위한 것이다. 그러나 방향은 명확하다: 플랫폼은 초기화 “lazy”를 일급 원시 타입으로 다루고 싶어 하며, 보일러플레이트와 오류가 발생하기 쉬운 패턴의 필요성을 없애고자 한다.
LazyConstant – 일급 추상화
특정 기능을 일급 추상화로 사용하고, 컴파일러와 JVM의 지원을 받아 각 개발자가 자신만의 방식으로 구현하는 “코드 레시피” 대신 사용하세요. --enable-preview 로 지금 실험해 보는 것이 기능이 정식으로 안정화되기 전에 코드에 미치는 영향을 가장 직접적으로 이해하는 방법입니다.
LazyConstant와 JEP 526에 대한 자주 묻는 질문 (Java 26)
LazyConstant는 thread‑safe인가요?
예. 단일 초기화가 멀티스레드 환경에서도 synchronized나 volatile 없이 보장됩니다.
LazyConstant를 Serializable과 함께 사용할 수 있나요?
아니요. 객체를 직렬화해야 한다면 final이나 다른 메커니즘을 사용하세요.
LazyConstant가 정적 내부 클래스를 이용한 Singleton 패턴을 대체하나요?
대부분의 경우 그렇습니다. 결과는 동일하지만 코드가 더 간결해지고 의도가 타입에 명시됩니다.
이 기능을 사용하려면 Java 26이 필요합니까?
예, --enable-preview 옵션이 필요합니다. JEP 526은 second preview 단계이며 아직 안정화되지 않았습니다.
초기화 함수가 예외를 발생시키면 어떻게 되나요?
예외는 .get()을 호출한 쪽으로 전파되며 LazyConstant는 초기화된 것으로 표시되지 않습니다. 다음 호출 시 다시 시도합니다.
저자 소개
저는 Luis De Llamas이며, act digital에서 Developer Advocate, Oracle ACE, IBM Champion입니다.
Quarkus, Java 및 생태계에 대한 더 많은 콘텐츠를 팔로우하고 싶다면, 여기서 저를 찾을 수 있습니다: