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

지연 초기화

  • 지연 초기화 (lazy initalization) 는 필드의 초기화 시점을 그 값이 처음 필요할 때까지 늦추는 방법이다.

  • 값이 쓰이지 않으면 당연히 초기화도 일어나지 않으며, 지연 초기화는 정적 필드 (static) 와 인스턴스 필드에 모두 사용할 수 있다.

  • 지연 초기화를 함부러 사용하면 안되는게, 객체 생성시 초기화 비용은 최적화가 될 수 있으나 지연 초기화하는 필드에 추가로 접근해야 하는 비용이 생긴다.

1
2
3
4
String lateinitVar;
if(lateinitVar == null){
lateInitVar = "lazy initilization";
}

지연 초기화를 사용할 것으로 권장되는 상황은 다음과 같다.

  • 해당 클래스의 객체 중 그 필드를 사용하는 경우보다 필드를 초기화하는 비용이 더 큰 경우

만약 멀티 쓰레드 환경이라면 지연 초기화하려는 필드가 여러 쓰레드에 의해 공유되는 상황에서는 초기화 작업이 반드시 동기화하고 초기화해야 한다.

위 상황을 제외하고는 대부분의 상황에서는 일반적인 초기화가 지연초기화보다 낫다.

1
private final FieldType field = computeFieldValue(); //일반적인 초기화 
1
2
3
4
5
6
7
8
private FieldType field; //지연초기화 방식

private synchronized FieldType getField(){
if(field == null){
field = computeFieldValue();
}
return field;
}
  • 만약 성능떄문에 정적필드를 지연 초기화해야 한다면 lazy initialization holder class 관용구를 사용하자
1
2
3
4
private static class FieldHolder {
static final FieldType field = computeFieldValue();
}
private static FieldType getField() {return FieldHolder.field;}

위처럼 전용 Holder class를 만들어두면 getField()method가 호출될떄 FieldHolder 클래스 초기화가 된다. 이렇게 사용하게 되면 클래스 초기화가 최초에 끝난 뒤에는 아무런 검사나 동기화 없이 필드에 접근하게 된다.

1
2
3
if(field == null){
field = computeFieldValue();
}

위와 같이 매번 필드에 접근해서 null여부를 확인하지 않아도 된다는 말이다.

  • 성능 떄문에 멤버필드를 지연 초기화해야 한다면 double check 관용구를 사용하자

이 방식은 필드의 값을 두 번 검사하는 방식으로 , 최초는 동기화 없이 검사하고 두 번쨰는 동기화하여 검사한다. 두번째 검사에서도 필드가 초기화되지 않았을 때만 필드를 초기화한다. 필드가 초기화된 이후에는 동기화하지 않으므로 해당 필드를 반드시 volatile(메인메모리로부터 저장하고 읽어옴) 로 선언해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

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 지역변수를 사용한 이유는 필드가 이미 초기화된 상황에서 필드를 한번만 읽도록 보장하기 위함이다.

Double Check 관용구 - 추가사항

  • 가끔 반복해서 초기화해도 상관없는 멤버필드를 지연 초기화할 때가 있는데, 이럴떄에는 이중검사에서 단일검사로 (Single Check) 관용구로 변경해도 된다.
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){
field = result = computeFieldValue(); // 초기화 중복해서 일어나도 괜찮음
}

}

위에서 정리한 내용은 모두 primitive type 과 reference data type에 모두 적용가능하며 단 default 값만 신경써서 지연 초기화를 수행하면 된다(ex) int - 0 , Integer - null )

정리

저자는 지연 초기화는 왠만하면 사용하지 말되, 정말 초기화비용이 비싼경우에 사용하는데 사용할때에는 멀티 쓰레드 환경에서 여러개의 쓰레드가 해당 필드를 동시에 접근해서 초기화를 이상하게 수행할 수 있으니 동기화하여 수행하라고 권고하고 있다.

  • 멤버필드 지연 초기화 : Double Check 관용구
  • 정적필드 지연 초기화 : Lazy Initialization Holder Class 관용구

Comments