Backend/Java

[Effective Java : Item 83] 지연 초기화는 신중히 사용하라

김세진 2024. 4. 9. 20:37
반응형

 

 

 

지연 초기화(lazy initialization)

 

지연 초기화란?

- 필드의 값이 처음 필요할 때까지 초기화를 늦추는 기법

- 정적 필드와 인스턴스 필드 모두 사용 가능

- 주로 최적화 용도로 사용, 초기화 시 발생하는 위험한 순환 문제를 해결하는 효과도 존재

 

지연 초기화의 양면성

지연 초기화는 무조건 좋은 것이 아니라 경우에 따라 다른 양면성이 존재한다.

 

- 클래스 혹은 인스턴스 생성 시의 초기화 비용 감소

- 반면 지연 초기화하는 필드에 접근하는 비용은 증가

- 필드를 초기화하는 비용, 횟수 등에 따라 지연 초기화 시 오히려 성능이 감소하는 경우도 있다. 즉, 인스턴스가 해당 필드를 잘 사용하지 않지만, 초기화 비용이 큰 경우에는 지연 초기화 도입을 고려할 필요성이 있다.

 

또한 멀티스레드 환경에서는 지연 초기화가 까다롭다고 한다. 지연 초기화하는 필드를 둘 이상의 스레드가 공유한다면 반드시 동기화해야 심각한 버그로 이어지지 않기 때문이라고 한다.

 

위같은 이유로, 저자는 대부분의 상황에서 일반적인 초기화가 지연 초기화보다 나으니 정말 필요할 때까지는 지연 초기화를 사용하지 말라고 전한다.

 

초기화 예시

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

private final FieldType field = computeFieldValue();

 

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

private FieldType field;

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

 

지연 초기화 시 synchronized 접근자를 사용할 경우 멀티스레드 환경에서 데이터 일관성을 유지할 수 있도록 동기화해준다. 따라서 여러 스레드가 동시에 객체를 초기화하려는 것을 방지해준다.

 

위의 두 예제 관용구는 정적 필드에도 동일하게 적용된다(단, 필드와 접근자 메서드 선언에 static 한정자 추가).

 

 


성능 향상을 위한 지연 초기화

 

지연 초기화 홀더 클래스(lazy initialization holder class)

만약, 성능을 위해 정적 필드를 지연 초기화하려는 경우, 지연 초기화 홀더 클래스 관용구를 사용하도록 하자.

 

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

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

 

이같은 방식의 장점은 다음과 같다.

- 내부 클래스가 클래스 로딩 시점에 초기화되며, 클래스 로딩은 JVM에 의해 스레드 안전하게 수행

- 따라서 synchronized를 사용한 getField 메서드처럼 필드에 접근하면서 동기화를 하지 않으므로 성능상 이점을 취할 수 있음

 

이중검사(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라는 지역변수는 field가 이미 초기화된 상황에서  딱 한 번만 읽도록 보장하는 역할을 한다고 한다. field는 volatile 한정자가 붙어 있으므로 필드에 접근할 때 메인 메모리에서 그 값을 조회하게 되는데, result 변수 없이 field를 그대로 사용하게 된다면 if 절에서 비교 때 한 번, return 할 때 한 번 총 두 번을 읽어와야 되기 때문이다.

 

만약, 인스턴스 필드가 반복해서 초기화되어도 상관없다면 위 관용구 대신 단일검사(single-check) 관용구를 사용해도 된다.

private volatile FieldType field;

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

 

 


정리

 

1. 반드시 필요하다고 생각되지 않으면 지연 초기화 대신 일반적인 초기화를 사용하자.

2. 성능, 혹은 초기화 순환을 막기 위해 지연 초기화를 사용해야 한다면 올바른 지연 초기화 기법을 사용하자.

3. 정적 필드에는 지연 초기화 홀더 클래스 관용구를, 인스턴스 필드에는 이중검사 관용구 사용하자.

4. 반복해서 초기화해도 괜찮은 인스턴스 필드는 단일검사 관용구를 사용하는 것도 고려해보자.

 

 

 

 

 

반응형