Item83. 지연 초기화는 신중히 사용하라!
Intro
- 지연 초기화(lazy initialization)는 필드의 초기화 시점을 그 값이 처음 필요할 때까지 늦추는 기법이다.
- 값이 전혀 쓰이지 않으면 초기화도 결코 일어나지 않는다.
- 이 기법은 정적 필드, 인스턴스 필드 모두에 사용할 수 있다.
- 주로 최적화 용도로 사용된다. 또 클래스와 인스턴스 초기화 때 발생하는 위험한 순환 문제를 해결하는 효과도 있다.
지연 초기화
- “필요할 때까지는 하지 말라”
- 지연 초기화는 양날의 검이다. 클래스 혹은 인스턴스 생성 시의 초기화 비용은 줄어드나, 그 대신 지연 초기화가 이뤄지는 비율에 따라, 실제 초기화에 드는 비용에 따라, 초기화된 각 필드를 얼마나 빈번히 호출하느냐에 따라 지연 초기화가 실제 성능을 떨어뜨릴 수 있다.
- 지연 초기화가 필요한 경우
- 해당 클래스의 인스턴스 중 그 필드를 사용하는 인스턴스의 비율이 낮은 반면, 그 필드를 초기화하는 비용이 높은 경우
- 지연 초기화가 실제 성능을 개선하는지는 적용 전후의 성능 비교로 확인할 수밖에 없다.
- 멀티스레드 환경에서는 지연 초기화를 하기가 까다롭다.
- 지연 초기화하는 필드를 둘 이상의 스레드가 공유한다면 어떤 형태로든 반드시 동기화해야 한다. 그렇지 않으면 심각한 버그를 유발한다.
인스턴스 필드를 초기화하는 방법
- 대부분의 상황에거 일반적인 초기화가 지연 초기화보다 낫다는 것을 기억하자.
일반적인 초기화
1
private final FieldType field = computeFieldValue();
- final 한정자를 사용함.
지연 초기화
synchronized 접근자 방식
1
2
3
4
5
6
7
private FieldType field;
private synchronized FieldType getField() {
if (field == null)
field = computeFieldValue();
return field;
}
- 지연 초기화가 초기화 순환성(initialization circularity)을 깨뜨릴 것 같으면 synchronized를 단 접근자를 사용하자.
- 일반적인 초기화와 synchronized 접근자를 사용한 지연 초기화는 정적 필드에도 동일하게 적용된다. 물론 필드와 접근자 메서드에 static은 붙인다.
홀더 클래스 관용구
1
2
3
4
5
private static class Fieldholder {
static final FieldType field = computeFieldValue();
}
private static FieldType getField() { return FieldHolder.field; }
- 성능 때문에 정적 필드를 지연 초기화해야 한다면 지연 초기화 홀더 클래스(lazy initialization holder class) 관용구를 사용하자.
- 클래스는 클래스가 처음 쓰이는 시점에 비로소 초기화된다는 특성을 이용한 관용구다.
- getField가 처음 호출되는 순간 FieldHolder.field가 처음 읽히면서, 비로소 Fieldholder 클래스 초기화를 촉발한다.
- 이 관용구의 멋진 점은? getField 메서드가 필드에 접근하면서 동기화를 전혀 안 하니 성능이 느려질 거리가 전혀 없다.
- 일반적인 VM은 오직 클래스를 초기화할 때만 필드 접근을 동기화할 것이다. 초기화 후 VM이 동기화 코드를 제거히고, 그 다음부터 아무런 검사나 동기화 없이 필드 접근이 가능해진다.
이중검사 관용구
1
2
3
4
5
6
7
8
9
10
11
12
13
private volatile FieldType field;
private FieldType getField() {
FieldType result = field;
if (result != null) { // 첫 번째 검사 (락 사용 안 함)
return result;
synchronized(this) {
if (field == null) // 두 번째 검사 (락 사용)
field = computeFieldValue();
return field;
}
}
- 성능 때문에 인스턴스 필드를 지연 초기화해야 한다면 이중검사(double check) 관용구를 사용하라.
- 이 관용구는 초기화된 필드에 접근할 때의 동기화 비용을 없애준다.
- 필드의 값을 두 번 검사하는 방식인데, 한 번은 동기화 없이 검사하고, 필드가 아직 초기화되지 않았다면 두 번째는 동기화햐여 검사한다. 두 번째 검사에서 필드가 아직 초기화되지 않은 상태일 때만 필드 초기화를 진행한다.
- 필드가 초기화된 후로는 동기화하지 않으니 해당 필드는 반드시 volatile로 선언해야 한다.
- result 지역변수는 필드가 이미 초기화된 상황에서 그 필드를 딱 한 번만 읽도록 보장하는 역할을 한다.
- 반드시 필요하지는 않지만 성능을 높여준다. 또 저수준 동시성 프로그래밍에 표준적으로 적용되는 더 우아한 방법이다.
- 이중 검사를 정적 필드에도 적용 가능하지만 그럴 이유는 없다. 정적 필드는 그냥 지연초기화 홀더 클래스 관용구를 사용하도록 하자.
단일검사 관용구
1
2
3
4
5
6
7
8
private volatile FieldType field;
private FieldType getField() {
FieldType result = field;
if (result == null)
field = result = computeFieldValue();
return result;
}
- 반복해서 초기화해도 상관없는 인스턴스 필드를 지연초기화해야 할 때가 있는데, 이런 경우라면 이중검사에서 두 번째 검사를 생략할 수 있다.
- 이러한 변종을 단일검사(single check) 관용구라 부른다.
- 필드는 여전히 volatile로 선언한다.
- 이번 아이템에서 이야기한 모든 초기화 기법은 기본 타입 필드, 객체 참조 필드에 적용할 수 있고, 그래야 한다.
- 이중검사 및 단일 검사 관용구를 수치 기본 타입에 적용한다면 필드의 값을 null 대신 0과 비교하자.
짜릿한 단일검사 관용구
- 모든 스레드가 필드의 값을 다시 계산해도 상관없고, 필드의 타입이 long, double을 제외한 기본 타입인 경우, 단일검사의 필드 선언에서 volatile 한정자를 제거 가능.
- 이 관용구는 어떤 환경에서는 필드 접근 속도를 높여주나, 초기화가 스레드당 최대 한 번 더 일어날 수 있다.
- 거의 사용하지 않는 기법이다.
핵심 정리
- 대부분의 필드는 지연시키지 말고 곧바로 초기화해야 한다.
- 성능 혹은 위험한 초기화 순환을 막기 위해 지연 초기화를 사용해야만 한다면, 올바른 지연초기화 기법을 사용하자.
- 인스턴스 필드에는 이중검사 관용구를 사용하자.
- 정적 필드에는 지연초기화 홀더 클래스 관용구를 사용하자.
- 반복해서 초기화해도 괜찮은 인스턴스 필드에는 단일검사 관용구 사용도 고려할 수 있다.
Comments powered by Disqus.