Effective Java : 아이템 83. 지연 초기화는 신중히 사용하라

    아이템 83. 지연 초기화는 신중히 사용하라

    지연초기화는 성능 최적화 기법중 하나다. 따라서 안하는게 일단은 최선임. 

     

     


    지연 초기화란?

    Object lazy;
    // 수많은 코드 이후.
    lazy = new Object();

    지연 초기화는 필드가 필요한 시점까지 초기화하는 기법이다. 위의 코드가 간단한 예시이다. 지연 초기화는 '성능 최적화'를 위해 사용되는 기법 중 하나다. 따라서 안하는게 더 좋을 가능성이 있으며, 가급적이면 하지 않는 것이 좋다. 

     


    지연 초기화가 도움이 되는 경우

    지연 초기화는 실제 필드가 필요한 시점까지 인스턴스를 생성하지 않는다. 이것이 도움이 되는 경우는 다음 조건을 모두 만족할 때다. 물론 만족한다고 해서 성능 개선을 항상 보장하지는 않는다.

    • 초기화 비용이 크다. 
    • 이 필드가 잘 호출되지 않는다. 

    이 경우 지연초기화를 했을 때, 지연 초기화의 비용은 '필드를 호출할 때'로 넘어가게 된다. 이 말은 '초기화 하는데 드는 큰 비용'을 '호출할 때' 지불해야한다는 것이다. 

     


    멀티 쓰레드 환경에서 지연 초기화

    멀티 쓰레드 환경에서 특정 필드를 지연 초기화 해야 한다면 일반적으로는 반드시 동기화 한다. 만약 동기화 되지 않는다면 심각한 버그로 연결될 가능성이 크다. 지연 초기화를 가장 낮은 부분부터 점진적으로 이해해보자.

     

    인스턴스 필드의 초기화.

    private final FieldType field = computedFieldValue();

    위 코드는 일반적으로 인스턴스 필드를 초기화 할 때 사용하는 방법이다. 

     

    인스턴스 필드의 지연 초기화 @ 멀티 쓰레드 → 단일 검사

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

    멀티 쓰레드에 의해서 지연 초기화가 오염될 것으로 예상된다면, 지연 초기화 하는 부분을 synchronized 키워드를 이용해서 동기화를 해줘야한다. 위에서는 getField()를 통해 지연 초기화가 이루어지는데, synchronized를 이용해 딱 한번의 지연 초기화만 이루어지도록 했다. 

     

    정적 필드의 지연 초기화 @ 멀티 쓰레드.

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

    정적 필드를 지연 초기화 할 때는, Java의 클래스 로딩 메커니즘을 활용하면 동기화 (Synchronzied) 구문 없이도 쓰레드 안전하게 지연 초기화를 해낼 수 있다. 

    • Java의 클래스가 처음 참조될 때, JVM은 해당 클래스를 로드하고 초기화한다.
    • 이 초기화 과정은 쓰레드 안전하게 이루어진다. 

    이런 특성을 이용한 것인데, 위 코드에 대입해서 이해해보자. getField() 메서드가 호출될 때, FieldHolder 클래스가 참조된다. 이 때, FieldHolder 클래스의 field는 초기화 되기 시작한다. 이 방법은 '동기화 구문'을 사용하지 않기 때문에 성능이 느려질 것이 없다는 점이다.

     

    인스턴스 필드의 지연 초기화 @ 멀티 쓰레드 → 이중검사

    만약 성능 때문에 인스턴스 필드를 지연 초기화 해야한다면, 이중검사 관용구를 사용하는 것이 좋다. 단일 검사와 비교하면 다음과 같다.

    1. 주로 동기화 없이 검사하기 때문에 동기화에 블록되는 부분을 최소화 시킴. 
    2. 필드가 초기화 된 이후로는 동기화하지 않음. 따라서 반드시 volatile 키워드로 필드를 선언해야한다. 

    먼저 아래 코드를 살펴보자.

    private volatile FieldType field;
    
    private FieldType getField() { 
    	FieldType result = field;
        if (result != null) // 첫번째 검사 (락 사용 안함)
        	retun result;
            
        synchronized(this) {
        	if (field == null) // 두번째 검사 (락 사용)
            	field = computeFieldValue();
            return field;
        }
    }

    먼저 동기화 하지 않은 영역에서 필드가 초기화 되었는지를 확인하고, 안되어 있을 경우 동기화 블록 내에서 초기화를 진행한다. 이렇게 동작하기 위해 volatile 키워드가 필수적이다. 그 이유는 다음과 같다

    synchronized 블록 내에서 일어난 변경 사항은 메모리에 바로 반영된다. 하지만, 다른 쓰레드가 인스턴스의 field에 접근하게 되면, 코어 캐시에서 값을 읽어올 수도 있다. 이 때 코어 캐시의 값과 메모리의 값이 다르다면, 초기화가 완료되었으나 쓰레드마다 '초기화 되지 않은 것'으로 볼 수 있는 상황이 존재하기 때문이다. 

    volatile 키워드와 초기화 되었는지를 동기화 없이 확인하는 부분을 이용해서 동기화 하는 영역을 최소화 할 수 있으며, 이를 통해 약간의 성능 개선을 기대해 볼 수도 있다. 

     

    인스턴스 필드의 지연 초기화 @ 멀티 쓰레드 → 멱등성있으면 단일 검사. 

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

    만약 여러 번 초기화 되어도 문제가 없는 필드(멱등성)라고 한다면, 이중검사에서 두번째 검사를 제외해도 무방하다. 그러므로 동기화 비용도 줄일 수 있다는 장점이 있다. 

     

    참조 타입이 아니라 값타입의 지연 초기화인 경우?

    private volatile int field;
    
    private int getField() { 
    	int result = field;
        if (result != 0) // 
        	field = result = computeFieldValue();
        return result;
        }
    }

    값타입의 지연 초기화가 필요하다면, null 체크 대신 값타입 변수가 기본값을 가지는지를 살펴보는 방식으로 처리할 수 있다. 

    댓글

    Designed by JB FACTORY