diff --git "a/11\354\236\245/83_\354\247\200\354\227\260 \354\264\210\352\270\260\355\231\224\353\212\224 \354\213\240\354\244\221\355\236\210 \354\202\254\354\232\251\355\225\230\353\235\274_\352\271\200\354\247\200\354\225\240.md" "b/11\354\236\245/83_\354\247\200\354\227\260 \354\264\210\352\270\260\355\231\224\353\212\224 \354\213\240\354\244\221\355\236\210 \354\202\254\354\232\251\355\225\230\353\235\274_\352\271\200\354\247\200\354\225\240.md" new file mode 100644 index 0000000..b598c99 --- /dev/null +++ "b/11\354\236\245/83_\354\247\200\354\227\260 \354\264\210\352\270\260\355\231\224\353\212\224 \354\213\240\354\244\221\355\236\210 \354\202\254\354\232\251\355\225\230\353\235\274_\352\271\200\354\247\200\354\225\240.md" @@ -0,0 +1,121 @@ +# [Effective Java] item 83. 지연 초기화는 신중히 사용하라 + +### 지연 초기화 (lazy initialization) +- `필드의 초기화 시점을 그 값이 처음 필요할때까지 늦추는 기법` +- 그 값이 전혀 쓰이지 않으면 초기화도 결코 일어나지 않는다. +- 정적 필드와 인스턴스 필드 모두에 사용할 수 있다. +- 주로 최적화 용도로 쓰이지만, 클래스와 인스턴스 초기화 때 발생하는 위험한 순환 문제를 해결하는 효과도 있다. + +### 지연 초기화는 필요할 때까지는 하지 마라! +- 지연 초기화는 양날의 검이다. `클래스 혹은 인스턴스 생성 시의 초기화 비용은 줄지만 그 대신 지연 초기화하는 필드에 접근하는 비용은 커진다.` +- 지연 초기화하려는 필드들 중 결코 초기화가 이루어지는 비율에 따라, 실제 초기화에 드는 비용에 따라, 초기화 된 각 필드를 얼마나 빈번히 호출하느냐에 따라 지연 초기화가 (다른 수많은 최적화와 마찬가지로) 실제로는 성능이 느려지게 할 수도 있다. + +### 지연 초기화가 필요할 때 +- `해당 클래스의 인스턴스 중 그 필드를 사용하는 인스턴스의 비율이 낮은 반면, 그 필드를 초기화하는 비용이 크다면` 지연 초기화가 제 역할을 해줄 것이다. +- 하지만 안타깝게도 정말 그런지를 알 수 있는 유일한 방법은 지연 초기화 적용 전 후의 성능을 측정해보는 것이다. + +### 멀티스레드 환경에서의 지연 초기화 +- `지연 초기화하는 필드를 둘 이상의 스레드가 공유한다면 어떤 형태로든 반드시 동기화해야 한다.` 그렇지 않으면 심각한 버그로 이어질 것이다. +- `대부분의 상황에서 일반적인 초기화가 지연 초기화보다 낫다.` + +다음은 인스턴스 필드를 선언할 때 수행하는 일반적인 초기화의 모습이다. final 한정자를 사용했음에 주목하자(아이템 17). + +### 인스턴스 필드를 초기화하는 방법 +#### 1. 일반적인 방법 +##### 인스턴스 필드를 초기화하는 일반적인 방법 +```java +private final FieldType field = computeFieldValue(); +``` + +#### 2. 지연 초기화 +`지연 초기화가 초기화 순환성(initialization circularity)을 깨뜨릴 것 같으면 synchronized를 단 접근자를 사용하자.` 이 방법이 가장 간단하고 명확한 대안이다. +#### 2.1 Synchronized 접근자 방식 +##### 인스턴스 필드의 지연 초기화 - synchronized 접근자 방식 +```java +private FieldType field; + +private synchronized FieldType getField() { + if (field == null) + field = computeFieldValue(); + return field; +} +``` + +이상의 두 관용구(보통의 초기화와 synchronized 접근자를 사용한 지연 초기화)는 정적 필드에도 똑같이 적용된다. 물론 필드와 접근자 메서드 선언에 static 한정자를 추가해야 한다. + +#### 2.2 Holder 클래스 관용구 +`성능 때문에 정적 필드를 지연 초기화해야 한다면 지연 초기화 홀더 클래스(lazy initialization holder class) 관용구를 사용하자.` 클래스는 클래스가 처음 쓰일 때 비로소 초기화된다는 특성을 이용한 관용구다. + +##### 정적 필드용 지연 초기화 홀더 클래스 관용구 +```java +private static class Fieldholder { + static final FieldType field = computeFieldValue(); +} + +private static FieldType getField() { return FieldHolder.field;} +``` + +getField가 처음 호출되는 순간 FieldHolder.field가 처음 읽히면서, 비로소 FieldHolder 클래스 초기화를 촉발한다. 이 관용구의 멋진 점은 fetField 메서드가 필드에 접근하면서 동기화를 전혀 사용하지 않으니 성능이 느려질 거리가 전혀 없다는 것이다. + +일반적인 VM은 오직 클래스를 초기화할 때만 필드 접근을 동기화할 것이다. 클래스 초기화가 끝난 후에는 VM이 동기화 코드를 제거하여, 그 다음부터는 아무런 검사나 동기화 없이 필드에 접근하게 된다. + +#### 2.3 이중검사(Double-Check) 관용구 +- `성능 때문에 인스턴스 필드를 지연 초기화해야 한다면 이중검사(double-check) 관용구를 사용하자.` +- 이 관용구는 초기화된 필드에 접근할 때의 동기화 비용을 없애준다. +- 필드의 값을 두 번 검사하는 방식으로, 한 번은 동기화 없이 검사하고, (필드가 아직 초기화되지 않았다면) 두 번째는 동기화하여 검사한다. +- 두 번째 검사에서도 필드가 초기화되지 않았을 때만 필드를 초기화한다. +- 필드가 초기화된 후로는 동기화하지 않으므로 해당 필드는 반드시 volatile로 선언해야 한다. + +##### 인스턴스 필드 지연 초기화용 이중검사 관용구 +```java +private volatile FieldType field; + +private FieldType getField() { + FieldType result = field; + if (result != null) { // 첫 번째 검사 (락 사용 안 함) + return result; + + synchronized(this) { + if (field == null) // 두 번째 검사 (락 사용) + field = computeFieldValue(); + return field; + } +} +``` + +코드가 다소 난해할 것이다. 특히 reuslt라는 지역변수가 필요한 이유는 뭘까? 이 변수는 필드가 이미 초기화된 상황(일반적인 상황이다)에서는 그 필드를 딱 한번만 읽도록 보장하는 역할을 한다. 반드시 필요하지는 않지만 성능을 높여주고, 저수준 동시성 프로그래밍에 표준적으로 적용되는 더 우아한 방법이다. + +내 컴퓨터에서는 지역변수를 사용하지 않을 때보다 1.4배 빠르게 동작한다. + +`이중검사를 정적 필드에도 적용할 수 있지만 굳이 그럴 이유는 없다. 이보다는 지연 초기화 홀더 클래스 방식이 더 낫다.` + +#### 2.5 단일검사(single-check) 관용구 +- 반복해서 초기화해도 상관없는 인스턴스 필드를 지연초기화해야 할 때가 있는데, 이런 경우라면 이중검사에서 두 번째 검사를 생략할 수 있다. +- 필드는 여전히 volatile로 선언한다. + +##### 단일검사 관용구 - 초기화가 중복해서 일어날 수 있다! +```java +private volatile FieldType field; + +private FieldType getField() { + FieldType result = field; + if (result == null) + field = result = computeFieldValue(); + return result; +} +``` + +이번 아이템에서 이야기한 모든 초기화 기법은 기본 타입 필드와 객체 참조 필드 모두에 적용할 수 있다. 이중검사와 단일검사 관용구를 수치 기본 타입 필드에 적용한다면 필드의 값을 null 대신 (숫자 기본 타입 변수의 기본값인) 0과 비교하면 된다. + +#### 2.4 짜릿한 단일검사(Racy Single-Check) 관용구 +- 모든 스레드가 필드의 값을 다시 계산해도 상관없고 필드의 타입이 long과 double을 제외한 다른 기본 타입이라면, 단일검사의 필드 선언에서 volatile 한정자를 없애도 된다. +- 이 관용구는 어떤 환경에서는 필드 접근 속도를 높여주지만, 초기화가 스레드당 최대 한 번 더 이뤄질 수 있다. 아주 이례적인 기법으로, 보통은 거의 쓰지 않는다. + +## 핵심 정리 +- 대부분의 필드는 지연시키지 말고 곧바로 초기화해야 한다. +- 성능 때문에 혹은 위험한 초기화 순환을 막기 위해 꼭 지연 초기화를 써애 한다면 올바른 지연 초기화 기법을 사용하자. +- 인스턴스 필드에는 이중검사 관용구를, 정적 필드에는 홀더 클래스 관용구를 사용하자. +- 반복해 초기화해도 괜찮은 인스턴스 필드에는 단일검사 관용구도 고려 대상이다. + +### 참고 자료 +- Effective Java 3/E \ No newline at end of file