한 번만 계산해 두고 계속 쓰는 값을 안전하게 만드는 기능. 홀더 관용구와 이중 검사 잠금을 대체한다.
// Java 26 (JEP 526)
private final LazyConstant<Config> cfg = LazyConstant.of(this::loadConfig);
Config cfg() { return cfg.get(); } // 최초 1회만 loadConfig 가 돈다
무엇을 대체하나
지금까지 스레드 안전한 지연 초기화는 손으로 짜야 했다.
- 홀더 관용구 — 정적 중첩 클래스를 만들어 클래스 로딩의 락에 얹는다. 안전하지만 인스턴스 필드에는 못 쓴다
- 이중 검사 잠금 —
volatile을 빠뜨리면 조용히 깨진다. 가장 유명한 동시성 버그의 무대
LazyConstant 는 둘의 제약을 다 없앤다. 인스턴스 필드로 쓸 수 있고, 동기화를 직접 다루지 않으며, final 이라 JIT 이 초기화 이후를 상수로 취급할 수 있다.
이름과 API 가 바뀌었다
| Java 25 (JEP 502) | Java 26 (JEP 526) | |
|---|---|---|
| 타입 | StableValue | LazyConstant |
| 만들기 | StableValue.of() — 빈 상자 | LazyConstant.of(계산함수) |
| 읽기 | orElseSet(계산함수) | get() |
| 저수준 | orElseSet·setOrThrow·trySet | 전부 제거 |
null | 허용 | 불허 |
설계 방향이 "빈 상자를 만들고 나중에 채운다" 에서 "계산 방법을 미리 주고 알아서 채우게 한다" 로 옮겼다. 값이 없는 중간 상태가 사라져 잘못 쓸 여지가 준다. null 을 막은 것도 같은 맥락이다 — 읽기 경로에서 분기 하나가 없어진다.
지연 컬렉션
List<Double> roots = List.ofLazy(100, Math::sqrt); // 꺼낼 때 계산
Map<Locale, ResourceBundle> b = Map.ofLazy(locales, this::loadBundle);
원소 100개를 미리 다 계산하지 않고 실제로 꺼내는 것만 계산한다. 로케일별 리소스 번들처럼 가짓수는 많은데 실제로 쓰는 건 몇 개 인 경우에 맞는다.
프리뷰라는 점을 잊지 않는다
25 에서 26 으로 오며 타입 이름과 주요 메서드가 통째로 바뀌었고, isInitialized()·orElse(T) 는 Java 27 에서 제거될 예정이다. 프리뷰 API 를 운영 코드에 굳히면 다음 버전에서 컴파일이 깨진다. 정식화될 때까지는 홀더 관용구가 여전히 안전한 선택지다.
면접 함정
- ❌ "Stable Values 라는 기능이 있다" → 25 의 이름이다. 26 에서 Lazy Constants 로 바뀌었다.
- ❌ "지연 초기화는 volatile + 이중 검사가 정석" → 그것은 제약이 많은 옛 방법이다. 홀더 관용구가 먼저고, 앞으로는 이 기능이 대체한다.
- ❌ "프리뷰 기능도 켜서 쓰면 된다" → API 가 릴리스마다 바뀐다. LTS 정식화 전에는 코드에 굳히지 않는다.