Item 83 - 지연 초기화는 신중히 사용하라
지연 초기화 (lazy initialization) 🐢
- 필드의 초기화 시점을 그 값이 처음 필요할 때까지 늦추는 기법
- 정적 필드와 인스턴스 필드에 모두 사용가능
- 주로 최적화 용도로 쓰이지만, 클래스와 인스턴스 초기화 때 발생하는 위험한 순환 문제를 해결하는 효과도 있다.
지연 초기화는 필요할 때까지는 하지 말라 🙅
- 클래스나 인스턴스 생성 시의 초기화 비용은 줄지만 그 대신 지연 초기화하는 필드에 접근하는 비용은 커진다.
- 지연 초기화하려는 필드들 중 초기화가 이루어지는 비율에 따라, 실제 초기화에 드는 비용에 따라, 초기화 된 각 필드를 얼마나 빈번히 호출하느냐에 따라 지연 초기화가 실제로는 성능을 느려지게 할 수도 있다. 😕
지연 초기화가 필요할 때
- 해당 클래스의 인스턴스 중 그 필드를 사용하는 인스턴스의 비율이 낮은 반면(↓), 그 필드를 초기화하는 비용이 크다면(↑) 지연 초기화가 제 역할을 해줄 것이다.
- 정말 그런지는 지연 초기화 적용 전 후 성능을 측정해봐야 한다.
- 대부분의 상황에서 일반적인 초기화가 지연 초기화보다 낫다.
인스턴스 필드 초기화 방법
(1) 일반적인 방법
1
2
// 인스턴스 필드를 초기화하는 일반적인 방법
private final FieldType field = computeFieldValue();
(2) 지연 초기화 - Synchronized 접근자 방식
지연 초기화로 초기화 순환성을 깨뜨리고 싶으면
synchronized
접근자를 사용하라.
1
2
3
4
5
6
7
private FieldType field;
private synchronized FieldType getField() {
if (field == null)
field = computeFieldValue();
return field;
}
- 지연 초기화하는 필드를 둘 이상의 스레드가 공유한다면 반드시 동기화해야 한다.
(3) 지연 초기화 - 홀더 클래스 관용구
성능 때문에 정적 필드를 지연 초기화해야 한다면 지연 초기화 홀더 클래스 관용구를 사용하라.
1
2
3
4
5
6
// 정적 필드용 지연 초기화 홀더 클래스 관용구
private static class Fieldholder {
static final FieldType field = computeFieldValue();
}
private static FieldType getField() { return FieldHolder.field; }
getField()
가 처음 호출되는 순간FieldHolder.field
가 처음 읽히면서Fieldholder
클래스 초기화를 촉발한다.getField()
가 필드에 접근하면서 동기화를 전혀 하지 않으므로 성능이 느려지지 않는다.
(4) 지연 초기화 -이중 검사 관용구
성능 때문에 인스턴스 필드를 지연 초기화해야 한다면 이중 검사 관용구를 사용하자.
이 관용구는 초기화된 필드에 접근할 때의 동기화 비용을 없애준다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 인스턴스 필드 지연 초기화용 이중검사 관용구
private volatile FieldType field;
private FieldType getField() {
FieldType result = field;
if (result != null) { // 첫 번째 검사 (락 사용 안 함)
return result;
synchronized(this) {
if (field == null) // 두 번째 검사 (락 사용)
field = computeFieldValue();
return field;
}
}
- 한 번은 동기화 없이 검사하고, 두 번째는 동기화하여 검사한다.
- 두 번째 검사에서도 필드가 초기화되지 않았을 때만 필드를 초기화한다.
- 필드가 초기화된 후로는 동기화하지 않으므로 해당 필드는
volatile
로 선언해야 한다. - 위 코드에서
result
지역 변수가 필요한 이유?- 필드가 이미 초기화된 상황에서 그 필드를 한번만 읽도록 보장하고 성능을 높여준다.
(5) 🕷️ 이중 검사 관용구의 변종 1 - 단일 검사 관용구
이중 검사 관용구에서 반복해서 초기화해도 상관없는 인스턴스 필드를 지연 초기화할 때 사용할 수 있다.
1
2
3
4
5
6
7
8
9
// 이중 검사 관용구의 변종 - 단일검사 관용구(초기화가 중복해서 일어날 수 있다!)
private volatile FieldType field;
private FieldType getField() {
FieldType result = field;
if (result == null)
field = result = computeFieldValue();
return result;
}
(6) 🕷️ 이중 검사 관용구의 변종 2 - 짜릿한 단일 검사 관용구
- 모든 스레드가 필드의 값을 다시 계산해도 상관없고 필드의 타입이
long
과double
을 제외한 다른 기본 타입이라면, 필드 선언에서volatile
한정자를 없애도 된다.- 이를 짜릿한 단일 검사(racy single-check) 관용구라 부른다.
1
2
3
4
5
6
7
8
9
// 이중검사 관용구의 변종 - 짜릿한 단일검사 관용구(field의 선언에 volatile 한정자를 제거했다.)
private FieldType field;
private FieldType getField() {
FieldType result = field;
if (result == null)
field = result = computeFieldValue();
return result;
}
- 이 관용구는 필드의 접근 속도를 높여주지만, 초기화가 스레드당 최대 한 번 더 이뤄질 수 있다.
- 아주 이례적인 기법으로, 거의 사용되지 않는다.
💡 핵심 정리
- 대부분의 필드는 지연시키지 말고 곧바로 초기화해야 한다.
- 성능이나 위험한 초기화 순환을 막기 위해 지연 초기화를 써야하면 올바르게 사용하자.
- 인스턴스 필드에는 이중 검사 관용구, 정적 필드에는 홀더 클래스 관용구를 사용하자.
- 반복해 초기화해도 괜찮은 인스턴스 필드에는 단일 검사 관용구도 고려 대상이다.
This post is licensed under CC BY 4.0 by the author.