Home [Effective Java] Item83. 지연 초기화는 신중히 사용하라!
Post
Cancel

[Effective Java] Item83. 지연 초기화는 신중히 사용하라!

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 한정자를 제거 가능.
  • 이 관용구는 어떤 환경에서는 필드 접근 속도를 높여주나, 초기화가 스레드당 최대 한 번 더 일어날 수 있다.
  • 거의 사용하지 않는 기법이다.

핵심 정리

  • 대부분의 필드는 지연시키지 말고 곧바로 초기화해야 한다.
  • 성능 혹은 위험한 초기화 순환을 막기 위해 지연 초기화를 사용해야만 한다면, 올바른 지연초기화 기법을 사용하자.
  • 인스턴스 필드에는 이중검사 관용구를 사용하자.
  • 정적 필드에는 지연초기화 홀더 클래스 관용구를 사용하자.
  • 반복해서 초기화해도 괜찮은 인스턴스 필드에는 단일검사 관용구 사용도 고려할 수 있다.
This post is licensed under younghwani by the author.

[Effective Java] Item82. 스레드 안전성 수준을 문서화하라!

[Effective Java] Item84. 프로그램의 동작을 스레드 스케줄러에 기대지 말라!

Comments powered by Disqus.