Posts [이펙티브자바] 아이템83-지연 초기화는 신중히 사용하라
Post
Cancel

[이펙티브자바] 아이템83-지연 초기화는 신중히 사용하라

핵심 정리: 대부분의 필드는 지연시키지 말고 곧바로 초기화해야 한다. 성능 때문에 혹은 위험한 초기화 순환을 막기 위해 꼭 지연 초기화를 써야 한다면 올바른 지연 초기화 기법을 사용하자. 인스턴스 필드에는 이중검사 관용구를, 정적 필드에는 지연 초기화 홀더 클래스 관용구를 사용하자. 반복해 초기화해도 괜찮은 인스턴스 필드에는 단일검사 관용구도 고려 대상이다.

지연 초기화

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

지연 초기화는 꼭 최적화 용도가 아니라면 사용하지마라.

  • 지연 초기화는 양날의 검이다.
  • 클래스 혹인 인스턴스 생성 시의 초기화 비용은 줄지만 그 대신 지연 초기화하는 필드에 접근하는 비용은 커진다.
  • 지연 초기화하려는 필드들 중 결국 초기화가 이뤄지는 비율에 따라, 실제 초기화에 드는 비용에 따라, 초기화된 각 필드를 얼마나 빈번히 호출하느냐에 따라 지연초기화가 실제론 성능을 더 느리게 할 수도 있다.

지연 초기화가 꼭 필요할 때

  • 해당 클래스의 인스턴스 중 그 필드를 사용하는 인스턴스의 비율이 낮은 반면, 그 필드를 초기화하는 비용이 크다면 지연 초기화가 제 역할을 해줄 것
    • 적용 전후의 성능도 꼭 측정해보자.

멀티스레드 환경에선 지연 초기화를 하기 까다롭다.

  • 지연 초기화하는 피드를 둘 이상의 스레드가 공유한다면 어떤 형태로든 반드시 동기화해야 한다.
    • 그렇지 않으면 심각한 버그로 이어질 것이다.(아이템78)
    • 참고로 이번 아이템에서 다루는 모든 초기화기법은 스레드 안전하다.

여러 가지 지연 초기화 기법

  • 대부분의 상황에선 일반적인 초기화가 지연 초기화보다 낫다.
  • 다음은 인스턴스 필드의 일반적인 초기화 형태다. final 한정자를 사용했음에 주목하자(아이템17)
1
private final FieldType field1 = computeFieldValue();

지연 초기화가 초기화 순환성(initialization circularity)을 깨뜨릴 것 같으면 synchronized를 단 접근자를 사용하자. 이 방법이 가장 간단하고 명확한 대안이다.

1
2
3
4
5
[ initialization circularity ]
Class A in its constructor creates instance of class B, class B creates instance of class C, and class C creates instance of class A.

출처: https://javabom.tistory.com/92
=> 클래스 A의 생성자에서 B를 참조하고 B가 다시 A를 참조하는 순환 문제를 뜻함
1
2
3
4
5
6
7
8
// 코드 83-2 인스턴스 필드의 지연 초기화 - synchronized 접근자 방식
private FieldType field;

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

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

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

  • 성능 때문에 정적 필드를 지연 초기화해야 한다면 지연 초기화 홀더 클래스(lazy initialization holder class) 관용구를 사용하자.
  • 클래스는 클래스가 처음 쓰일 때 비로소 초기화된다는 특성을 이용한 관용구다.
1
2
3
4
5
6
// 코드 83-3 정적 필드용 지연 초기화 홀더 클래스 관용구 (443쪽)
private static class FieldHolder {
  static final FieldType field = computeFieldValue();
}

private static FieldType getField() { return FieldHolder.field; }
  • 위 관용구의 동작 방식은 다음과 같다. getField 가 처음 호출되는 순간 FieldHolder.field 가 처음 읽히면서, 비로소 FieldHolder 클래스 초기화를 촉발한다.
  • 이 관용구의 멋짐은 getField 메서드가 필드에 접근하면서 동기화를 전혀 하지 않으니 성능이 느려질 거리가 전혀 없다는 거싱다.
  • 일반적인 VM은 오직 클래스를 초기화할 때만 필드 접근을 동기화할 것이다.
  • 클래스 초기화가 끝난 후에는 VM이 동기화 코드를 제거하여, 그다음 부턴 아무런 검사나 동기화 없이 필드에 접근하게 한다.

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

  • 성능 때문에 인스턴스 필드를 지연 초기화해야 한다면 이중검사(double-check) 관용구를 사용하라
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 코드 83-4 인스턴스 필드 지연 초기화용 이중검사 관용구
private volatile FieldType field;

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

  synchronized(this) {
    if (field == null) // 두 번째 검사 (락 사용)
      field = computeFieldValue();
    return field;
  }
}
  • 초기화된 필드에 접근할 때의 동기화 비용을 없애준다.
    • 필드의 값을 두 번 검사하는 방식으로, 한 번은 동기화 없이 검사하고, 두번째는 동기화하여 검사한다.
    • 두번째 검사에서도 필드가 초기화되지 않았을 때만 필드를 초기화한다.
    • 필드가 초기화 된 후로는 동기화 하지 않으므로 해당 필드는 반드시 volatile로 선언한다.
  • result 지역변수: 필드가 이미 초기화된 상황에서는 이 필드를 딱 한번만 읽도록 보장하는 역할을 한다.
    • 반드시 필요하진 않지만 성능을 높여주고, 저수준 동시성 프로그래밍에 표준적으로 적용되는 더 우아한 방법이라 한다.
    • 이 부분과 관련해서 다소 이해하기 어려워 좀 더 깊은 고민이 필요할 것 같다.
  • 이중검사를 정적 필드에도 적용할 수 있지만 굳이 그럴 이유는 없다. 이보단 지연 초기화 홀더 클래스 방식이 더 낫다.
  • 이따금 반복해서 초기화해도 상관없는 인스턴스 필드를 지연 초기화해야 할 때가 있는데, 이런 경우라면 이중검사에서 두번째 검사를 생략할 수 있다.
    • 이는 단일검사 관용구가 된다.
1
2
3
4
5
6
7
8
9
// 코드 83-5 단일검사 관용구 - 초기화가 중복해서 일어날 수 있다.
private volatile FieldType field;

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

Note: 위에서 언급한 초기화 기법들은 기본 타입 필드와 객체 참조 필드 모두에 적용할 수 있다. 이중검사와 단일 검사 관용구를 수치 기본 타입필드에 적용한다면 필드의 값을 null대신 0과 비교하며 된다.

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

  • 이 기법은 아주 이례적인 기법으로, 보통 거의 쓰지 않는다.
  • 모든 스레드가 필드의 값을 다시 계산해도 상관없고 필드의 타입이 long과 double을 제외한 다른 기본 타입이라면, 단일검사의 필드 선언에서 volatile 한정자를 없애도 된다.
  • 이 변종을 짜릿한 단일검사 관용구(racy single-check)라 부른다.
  • 이 관용구는 어떤 환경에선 필드 접근 속도를 높여주지만, 초기화가 스레드당 최대 한 번 더 이뤄질 수 있다.
This post is licensed under CC BY 4.0 by the author.

[이펙티브자바] 아이템80-스레드보다는 실행자, 태스크, 스트림을 애용하라

[Architecture] 헥사고날아키텍처