지연 초기화는 신중히 사용하라
지연 초기화란 필드의 초기화 시점을 그 값이 처음 필요할 때까지 늦추는 기법이다. 값이 쓰이지 않는다면 초기화도 결코 일어나지 않는다.
지연 초기화는 정적 필드와 인스턴스 필드 모두에 사용할 수 있다.
보통 최적화 용도로 사용되며, 클래스와 인스턴스 초기화 때 발생하는 위험한 순환 문제를 해결하는 효과도 있다.
그러나 지연 초기화도 최적화라는 사실을 잊으면 안된다. 최적화는 필요한 경우에만 적용하라고 했다.
지연 초기화는 클래스 혹은 인스턴스 생성 시 초기화 비용을 줄여주지만 지연 초기화하는 필드에 접근하는 비용은 커진다.
따라서 지연 초기화를 적용한 필드들이 얼마나 초기화되어 사용되는지, 그 필드를 얼마나 빈번하게 호출하는지 등등의 영향을 받는다.
지연 초기화가 필요한 경우
- 해당 클래스의 인스턴스 중 지연 초기화 필드를 사용하는 인스턴스의 비율이 낮고, 그 필드를 초기화하는 비용이 큰 경우
그런데 이를 확인할 수 있는 방법은 지연 초기화 적용 전후의 성능을 측정해보는 것 뿐이다.
심지어 멀티 쓰레드 환경에서는 지연 초기화가 더 까다로워진다. 지연 초기화는 필드를 둘 이상의 쓰레드가 공유하면 어떤 형태로든 반드시 동기화해주어야 한다.
대부분의 상황에서 일반적인 초기화가 지연 초기화보다 낫다.
지연 초기화 방법
1
private final FieldType field = computFieldValue();
- 일반적인 초기화 방법이다. final 키워드를 사용했다.
인스턴스 필드 지연 초기화 방법
1
2
3
4
5
6
7
8
private FieldType field;
private synchronized FieldType getField() {
if (field == null) {
field = computeFieldValue();
}
return field;
}
- 인스턴스 필드를 지연 초기화 하는 방식이다.
- 지연 초기화가 초기화 순환성을 깨뜨릴 것 같다면 synchronized를 단 접근자를 사용해야 한다.
- 이 방법이 가장 간단하고 명확하다.
- 초기화 순환성이란 초기화 과정에서 서로가 서로를 초기화하려는 의존관계가 발생하는 상황을 뜻한다.
- 초기화가 완료되지 않은 객체나 클래스에 접근할 수 있다. (NPE 발생)
- 따라서 synchronized 접근자를 이용해 이 문제를 피하는 것이다.
정적 필드 지연 초기화 관용구
만약 성능 때문에 정적 필드를 지연 초기화해야 하는 경우 지연 초기화 홀더 클래스 관용구를 사용하면 된다.
1
2
3
4
5
6
7
private static class FieldHolder {
static final FieldType field = computFieldValue();
}
private static FieldType getField() {
return FieldHolder.field;
}
- 정적 필드를 지연 초기화 하는 홀더 클래스 관용구이다.
- getField()가 처음 호출되는 순간 FieldHolder.field가 처음 읽히면서 FieldHolder 클래스 초기화를 촉발한다.
- 심지어 getField() 메서드가 필드에 접근할 때 동기화를 전혀 하지 않아 성능이 느려지는 문제도 없다.
- 동기화를 하지 않아도 되는 이유는 일반적인 VM은 오직 클래스를 초기화할 때만 필드 접근을 동기화하고, 초기화가 끝난 후에는 VM이 동기화 코드를 제거해 아무런 검사나 동기화 없이 필드에 접근하기 때문이다.
인스턴스 필드 지연 초기화 관용구 (성능 이유일 때)
성능이 이유라면 인스턴스 필드를 지연 초기화 할 때 이중검사 관용구를 사용하면 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private volatile FieldType field;
private FieldType getField() {
FieldType result = field;
if (result != null) {
// 첫 번째 검사 (락 사용 X)
return result;
}
synchronized(this) {
if (field == null) {
//두 번째 검사 (락 사용)
field = computeFieldValue();
}
return field;
}
}
- 필드의 값을 두 번 검사하는 방식이다.
- 초기화된 필드에 접근할 때의 동기화 비용을 없애준다.
- 필드가 초기화된 이후로는 동기화하지 않으므로 해당 필드는 반드시 volatile로 선언해 가장 최근의 값을 읽도록 보장해주어야 한다.
- result 지역변수를 사용하는 이유는 성능을 높여주고 표준적으로 적용되는 우아한 방법이기 때문이다.
- 필드가 이미 초기화된 상황에서는 그 필드를 딱 한번만 읽도록 보장하는 역할을 한다.
정리
지연 초기화 기법을 알아보았다. 이 기법들은 기본 타입 필드, 객체 참조 필드 모두 적용할 수 있다.
그러나 지연 초기화는 진짜 필요한 경우가 아니라면 사용하지 말고 곧바로 초기화하는 것이 좋다.
만약 성능 혹은 위험한 초기화 순환 문제를 막기 위해 지연 초기화를 써야한다면 알맞은 지연 초기화 관용구를 찾아 사용하면 된다.