DEV ℧ Developer Diary

[EffectiveJava] item83 - 지연 초기화는 신중히 사용하라

지연초기화(lazy initialization)
필드의 초기화 시점을 그 값이 처음 필요할때까지 늦추는 기법. 정적 필드와 인스턴스 필드 모두에 사용할 수 있다.

그래서 지연초기화를 하는 값이 전혀 쓰이지 않으면 초기화도 결코 일어나지 않는다. 지연초기화는 주로 최적화 용도로 쓰이지만, 클래스와 인스턴스 초기화 때 발생하는 위험한 순환 문제를 해결하는 효과도 있다.

지연초기화

주의할 점

지연초기화는 필요할 때까지는 하지 않는 걸 추천한다. 클래스 혹은 인스턴스 생성 시의 초기화 비용은 줄지만 대신 지연초기화하는 필드에 접근하느 ㄴ비용은 커진다.

지연초기화 하여는 필드 중 초기화의 비율, 실제 초기화에 드는 비용, 호출 비율에 따라 실제로는 성능을 느려지게 할 수 있다.

지연초기화 필요한 경우

해당 클래스의 인스턴스 중 그 필드를 사용하는 인스턴스의 비율이 낮은 반면, 그 필드를 초기화하는 비용이 크다면 지연초기화가 제 역할을 해줄 것이다.

하지만 정말 그런지 아는 유일한 방법은 지연 초기화 적용 전후의 성능을 측정해보는 것이다.

멀티 스레드 환경의 지연초기화

멀티스레드 환경에서는 지연 초기화를 하기가 까다롭다. 지연 초기화하는 필드를 둘 이상의 스레드가 공유한다면 어떤 형태로든 반드시 동기화해야 한다.

대부분의 상황에서 일반적인 초기화가 지연 초기화보다 낫다.

인스턴스 필드의 초기화

인스턴스 필드를 초기화하는 일반적인 방법

private final FieldType field = computeFieldValue();

인스턴스 필드의 지연 초기화

지연 초기화가 초기화 순환성(initialization circularity)을 깨뜨릴 것 같으면 synchronized를 단 접근자를 사용하자.

인스턴스 필드의 지연 초기화 - synchronized 접근자 방식

private FieldType field;
private synchronized FieldType getField() {
    if (field == null)
        field = computeFieldValue();
    return field;
}

이상의 두 관용구는 정적 필드에도 똑같이 적용된다. 물론 필드와 접근자 메서드 선언에 static 한정자를 추가해야 한다.

정적 필드용 지연 초기화

성능 때문에 정적 필드를 지연 초기화해야 한다면 지연 초기화 홀더 클래스(lazy initialization holder class) 관용구를 사용하자.
클래스는 클래스가 처음 쓰일 때 비로소 초기화된다는 특성을 이용한 관용구다.

정적 필드용 지연 초기화 홀더 클래스 관용구

private static class FieldHolder {
    static final FieldType field = computeFieldValue();
}

private static FieldType getField() { return FieldHolder.field; }

getField가 처음 호출되는 순간 FieldHolder.field가 처음 읽히면서, FieldHolder 클래스 초기화를 촉발한다.

getField 메서드가 필드에 접근하면서 동기화를 전혀 하지 않으니 성능이 느려질 거리가 없다.

이중검사(double-check) 관용구

성능 때문에 인스턴스 필드를 지연 초기화해야 한다면 이중검사(double-check) 관용구를 사용하라.

이 관용구는 초기화된 필드에 접근할 때의 동기화 비용을 없애준다.

첫번째 검사는 동기화 없이 검사하고, 두 번째 검사는 동기화 하여 필드가 초기화되지 않았을 때만 필드를 초기화한다.

인스턴스 필드 지연 초기화용 이중검사 관용구

private volatile FieldType field;

private FieldType getField() {
    FieldType result = field;
    if (result != null) { /** 첫 번째 검사 (락 사용 안 함) */
        return result;
    }

    synchronized(this) {
        if (field == null) /** 두 번째 검사 (락 사용) */
            field = computeFieldValue();
        return field;
    }
}

result 라는 지역변수가 필요한 이유는 필드가 이미 초기화된 상황에서는 그 필드를 딱 한 번만 읽도록 보장하는 역할을 한다.

이중검사를 정적 필드에도 적용할 수 있지만 지연 초기화 홀더 클래스 방식이 더 낫다.

단일검사(single-check) 관용구

이따금 반복해서 초기화 해도 상관없는 인스턴스 필드를 지연초기화 할 경우 이중검사에서 두 번째 검사를 생략할 수 있다.

단일검사 관용구 - 초기화가 중복해서 일어날 수 있다.

private volatile FieldType field;
private FieldType getField() {
    FieldType result = field;
    if (result == null)
        field = result = computeFieldValue();
    return result;
}

이번 Item에서 이야기한 모든 초기화 기업은 기본 타입 필드와 객체 참조 필드 모두에 적용할 수 있다.

이중검사와 단일검사 관용구를 수치 기본 타입 필드에 적용한다면 필드의 값을 null 대신 (숫자 기본 타입 변수의 기본값인) 0과 비교하면 된다.

짜릿한 단일검사(racy single-check) 관용구

모든 스레드가 필드의 값을 다시 계산해도 상관없고 필드의 타입이 long과 double을 제외한 다른 기본 타입이라면, 단일검사의 필드 선언에서 volatile 한정자를 없애도 된다.

이 변종은 짜릿한 단일검사(racy single-check) 관용구라 불린다.

이 관용구는 어떤 환경에서는 필드 접근 속도를 높여주지만, 초기화가 스레드당 최대 한 번 더 이뤄질 수 있다.

이례적인 기법으로 거의 쓰지 않는다.