83. 지연 초기화는 신중히 사용하라
지연 초기화는 필드의 초기화 시점을 그 값이 처음 필요할 때까지 늦추는 기법이다.
즉, 객체가 생성될 때 바로 초기화하지 않고, 해당 값이 필요한 시점에 초기화한다.
이는 정적 필드와 인스턴스 필드 모두에 사용할 수 있다.
// 일반적인 초기화
private final Member member = createMember();
// 지연 초기화
private Member member;
private synchronized Member getMember() {
if (member == null) {
member = createMember();
}
return member;
}
지연 초기화는 양날의 검이다
지연 초기화를 사용하면 장점과 단점이 함께 있다.
장점:
객체 생성 시의 초기화 비용을 줄일 수 있다
사용하지 않는 필드는 초기화되지 않는다
단점:
필드에 처음 접근할 때 초기화 비용이 발생한다
코드가 복잡해진다
멀티스레드 환경에서는 추가적인 동기화가 필요하다
일반 초기화와 지연 초기화 비교
초기화 시점
객체 생성 시
필드 첫 사용 시
초기 로딩 속도
느림 (모든 필드 초기화)
빠름 (필요한 필드만 초기화)
첫 사용 시 속도
빠름 (이미 초기화됨)
느림 (초기화 필요)
메모리 사용
더 많음 (사용하지 않는 필드도 초기화)
더 적음 (필요한 필드만 초기화)
코드 복잡성
간단함
복잡함 (동기화 필요)
스레드 안전성
기본적으로 안전
추가 작업 필요 (volatile, synchronized)
지연 초기화가 필요한 경우
클래스 중 일부 인스턴스만 해당 필드를 사용할 때
모든 인스턴스가 사용한다면 일반 초기화가 낫다
필드 초기화 비용이 크고 사용 빈도가 낮을 때
초기화 비용이 작다면 일반 초기화가 낫다
초기화 순환 문제를 해결해야 할 때
두 객체가 서로를 참조하는 경우
지연 초기화 기법들
1. 일반적인 초기화 (권장)
// 인스턴스 필드 초기화의 일반적인 방법
private final FieldType field = computeFieldValue();
대부분의 상황에서는 이 방식이 가장 간단하고 안전하다.
2. synchronized 접근자 방식
// 인스턴스 필드의 지연 초기화 - synchronized 접근자 방식
private FieldType field;
private synchronized FieldType getField() {
if (field == null)
field = computeFieldValue();
return field;
}
이 방식은 초기화 순환성 문제가 있을 경우 사용하자. 다만 동기화로 인한 성능 저하가 있다.
3. 홀더 클래스 관용구 (정적 필드용)
// 정적 필드용 지연 초기화 홀더 클래스 관용구
private static class FieldHolder {
static final FieldType field = computeFieldValue();
}
private static FieldType getField() {
return FieldHolder.field;
}
정적 필드를 지연 초기화할 때 가장 좋은 방법이다.
클래스는 처음 사용될 때만 초기화되는 자바의 특성을 활용한다.
4. 이중검사 관용구 (인스턴스 필드용)
// 인스턴스 필드 지연 초기화용 이중검사 관용구
private volatile FieldType field;
private FieldType getField() {
FieldType result = field;
if (result == null) { // 첫 번째 검사 (락 없이)
synchronized(this) {
if (field == null) // 두 번째 검사 (락 사용)
field = computeFieldValue();
return field;
}
}
return result;
}
volatile
키워드는 필드의 변경이 모든 스레드에 즉시 보이도록 보장한다.
지역 변수 result
는 이미 초기화된 경우 필드를 한 번만 읽도록 최적화한다.
인스턴스 필드를 지연 초기화하면서도 동기화 비용을 최소화하고 싶을 때 사용한다.
volatile 키워드 간략 설명
지연 초기화 패턴(특히 이중검사와 단일검사 관용구)에서 volatile
키워드는 중요한 역할을 한다
메모리 가시성 보장
한 스레드가
volatile
변수를 수정하면 다른 모든 스레드가 즉시 그 변경을 볼 수 있다.이것은 여러 스레드가 동시에 변수에 접근할 때 매우 중요하다
객체 초기화 안전성
volatile
없이는 객체가 완전히 초기화되기 전에 다른 스레드가 참조를 볼 수 있다.이로 인해 반쯤 초기화된 객체가 사용될 위험이 있다
사용 시점
멀티스레드 환경에서 지연 초기화를 사용할 때는 필드를
volatile
로 선언해야 한다단, 홀더 클래스 관용구는 예외로, JVM의 클래스 로딩 메커니즘이 동기화를 보장하므로
volatile
이 필요하지 않다.
5. 단일검사 관용구
// 단일검사 관용구 - 초기화가 중복해서 일어날 수 있다!
private volatile FieldType field;
private FieldType getField() {
FieldType result = field;
if (result == null)
field = result = computeFieldValue();
return result;
}
초기화가 여러 번 일어나도 상관없는 경우(예: 계산 비용은 크지만 항상 같은 결과를 반환하는 경우)에 사용한다.
각 기법의 사용 상황 비교
일반 초기화
대부분의 경우
간단하고 안전함
불필요한 초기화 가능성
synchronized 접근자
순환 참조 문제 시
안전함
성능 저하
홀더 클래스
정적 필드
성능 좋음
정적 필드만 가능
이중검사
인스턴스 필드
성능과 안전성 균형
구현 복잡
단일검사
중복 초기화 허용 시
구현 간단
중복 초기화 발생
멀티스레드 환경에서의 주의점
멀티스레드 환경에서 지연 초기화를 사용할 때는 반드시 적절한 동기화를 해야 한다.
그렇지 않으면 다음과 같은 문제가 발생할 수 있다
가시성 문제: 한 스레드의 변경이 다른 스레드에 보이지 않을 수 있다
경쟁 상태: 여러 스레드가 동시에 초기화를 시도할 수 있다
부분적 초기화: 객체가 완전히 초기화되기 전에 다른 스레드가 접근할 수 있다
이러한 문제를 방지하기 위해 volatile
키워드와 적절한 동기화(synchronized)를 사용해야 한다.
정리
대부분의 필드는 지연시키지 말고 곧바로 초기화하자.
성능 때문에 혹은 초기화 순환을 막기 위해 지연 초기화가 필요하다면
인스턴스 필드에는 이중검사 관용구
정적 필드에는 홀더 클래스 관용구
중복 초기화가 허용된다면 단일검사 관용구
🧩 어려웠던 점
지연 초기화에 대해 공부하면서 가장 어려웠던 점은 멀티스레드 환경에서의 안전성을 이해하는 것이었다.
성능 측면에서 지연 초기화가 실제로 도움이 되는 상황을 판단하는 것도 어려웠다.
실제로는 측정을 통해서만 확인할 수 있다는 점이 현실적인 어려움으로 다가왔다.
💡 느낀 점
코드의 성능과 안전성, 가독성 사이의 균형을 맞추는 것이 중요하다는 점을 다시 한번 깨달았다.
특히 멀티스레드 환경에서는 성능 향상을 위한 기법들이 오히려 더 큰 문제를 일으킬 수 있기 때문에 신중하게 접근해야 한다.
Last updated