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

신명철·2022년 6월 1일
0

Effective Java

목록 보기
78/80

지연 초기화

지연 초기화란, 필드의 초기화 시점을 그 값이 처음 필요할 때까지 늦추는 기법이다. 그래서 값이 전혀 쓰이지 않는다면 초기화도 일어나지 않는다. 이 기법은 정적 필드, 인스턴스 필드 모두에 사용할 수 있다. 지연 초기화는 주로 최적화 용도로 쓰지만, 클래스와 인스턴스 초기화 때 발생하는 위험한 순환 문제를 해결하는 효과도 있다.

지연 초기화 사용에 대해 해줄 조언은 "필요할 때까지는 하지 말라"다. 클래스 혹은 인스턴스 생성 시 초기화 비용은 줄지만 그 대신 지연 초기화하는 필드에 접근하는 비용은 커진다. 지연 초기화하는 필드들 중 결국 초기화되는 비율에 따라, 실제 초기화에 드는 비용에 따라, 초기화된 각 필드를 얼마나 빈번히 호출하느냐에 따라서 지연 초기화가 실제로는 성능을 느려지게 할 수도 있다.

대부분의 상황에서 일반적인 초기화가 지연 초기화보다 낫다. 다음은 인스턴스 필드를 선언할 때 수행하는 일반적인 초기화의 모습이다. final 한정자를 사용했음에 주목하자.

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

prviate final FieldType field = computeFieldValue();

지연 초기화가 초기화 순환성을 깨뜨릴 것 같다면 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가 처음 호출되는 순간 초기화된다. 이 관용구의 멋진 점은 getField 메서드가 필드에 접근할 때 동기화를 전혀 하지 않으니 성능이 느려질 거리가 전혀 없다는 것이다. 일반적은 VM은 오직 클래스를 초기화할 때만 필드 접근을 동기화할 것이다. 클래스 초기화가 끝난 후에는 VM이 동기화 코드를 제거하여 그 다음부터는 아무런 검사나 동기화 없이 필드에 접근하게 된다.

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

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 라는 지역변수는 필드가 이미 초기화된 상황에서 그 필드를 딱 한번만 읽도록 보장하는 역할을 한다. 이중검사를 정적 필드에도 적용할 수 있지만 이보다는 지연 초기화 홀더 클래스를 사용하는게 더 낫다.

이따금 반복해서 초기화해도 상관없는 인스턴스 필드를 지연 초기화해야 할 때가 있는데, 이런 경우라면 이중검사에서 두 번째 검사를 생략할 수도 있다. 이 경우 자연히 이름은 단일 검사 (single-check) 관용구가 된다. 필드는 여전히 volatile로 선언했음을 주목하자.

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

private volatile FieldType field;

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

모든 스레드가 필드의 값을 다시 계산해도 상관없고, 필드의 타입이 long, double을 제외한 다른 기본 타입이라면, 단일 검사의 필드 선언에서 volatile을 없애도 된다. 하지만 이 기법은 거의 사용하지 않는다.

profile
내 머릿속 지우개

0개의 댓글