지연 초기화
- 필드의 초기화 시점을 해당 값이 처음 필요할 때까지 늦추는 기법
- 값이 전혀 쓰이지 않을 경우 초기화가 일어나지 않음
- 정적 필드와 인스턴스 필드 모두 적용 가능
- 주로 최적화 용도로 쓰이지만, 클래스와 인스턴스 초기화 시 발생하는 `위험한 순환` 문제를 해결하는 효과도 있음
- 위험한 순환(Perilous Cycle) 문제는 초기화하는 동안 객체가 자기 자신을 참조하는 경우에 발생할 수 있는 문제
- ex) 클래스나 객체의 생성자에서 다른 객체를 생성하고, 이 생성된 객체가 다시 자신을 참조하는 경우 위험한 순환 문제가 발생 (무한 루프)
1. 지연 초기화는 필요할 때까지는 하지 말자
- 지연 초기화는 양날의 검
- 클래스 혹은 인스턴스 생성 시 발생하는 초기화 비용은 줄지만 지연 초기화하는 필드에 접근하는 비용이 커짐
- 지연 초기화하려는 필드들 중 다음과 같은 상황에 따라 오히려 지연 초기화로 인해 성능 저하가 발생할 수 있음
- 지연 초기화가 이루어지는 비율
- 실제 초기화에 드는 비용
- 초기화된 각 필드 호출 빈도
- 대부분의 상황에서 일반적인 초기화가 지연 초기화보다 나음
2. 지연 초기화가 필요한 케이스
- `클래스의 인스턴스 중 해당 필드를 사용하는 인스턴스의 비율이 낮은 반면 해당 필드를 초기화하는 비용이 클 경우` 지연 초기화가 제 역할을 할 확률이 높음
- 지연 초기화 적용 전후 성능 비교 반드시 진행할 것
3. 멀티 쓰레드 환경에서의 지연 초기화
- 지연 초기화하는 필드를 둘 이상의 쓰레드가 공유할 경우 어떤 형태로든 반드시 동기화 필요
- 동기화하지 않을 경우 심각한 버그를 야기할 수 있음
인스턴스 필드 초기화하는 방법
1. 일반적인 방법
코드 부연 설명
- final 한정자를 사용했고
- 인스턴스를 생성함과 동시에 초기화
2. Synchronized 접근자 방식
코드 부연 설명
- 지연 초기화가 초기화 순환성을 깨뜨릴 수 있다면 synchronized 접근자를 사용
- `초기화 순환성`은 서로가 서로를 초기화해야 하는 경우
- 정적 필드와 인스턴스 필드 모두 적용 가능
초기화 순환성 예시
3. 지연 초기화 홀더 클래스 관용구(Lazy Initialization Holder Class)
코드 부연 설명
- 성능 때문에 정적 필드를 지연 초기화해야 할 경우 지연 초기화 홀더 클래스 관용구를 사용
- `클래스는 클래스가 처음 쓰일 때 비로소 초기화`된다는 특성을 이용한 관용구
- getField() 메서드가 처음 호출되는 순간 FieldHolder.field가 처음 읽히면서 비로소 FieldHodler 클래스 초기화를 진행
- 지연 초기화 홀더 클래스 관용구의 장점은 getField() 메서드가 필드에 접근할 때 동기화를 전혀 사용하지 않으므로 성능 저하가 없다는 점
- 일반적인 VM은 오직 클래스를 초기화할 때만 필드 접근을 동기화
- 클래스 초기화가 끝나면 VM이 동기화 코드를 제거하여 그다음부터는 아무런 검사나 동기화 없이 필드에 접근
4. 이중 검사 관용구 (Double-Check)
코드 부연 설명
- 성능 떄문에 인스턴스 필드를 지연 초기화해야 할 경우 이중 검사 관용구를 사용
- 해당 관용구는 초기화된 필드에 접근할 때의 동기화 비용을 제거
- 필드의 값을 두 번 검사하는 방식으로 한 번은 동기화 없이 검사하고 필드가 아직 초기화되지 않았을 경우 두 번째는 동기화하여 검사
- 두 번째 검사에서도 필드가 초기화되지 않았을 때만 필드를 초기화
- 필드가 초기화된 후로는 동기화하지 않으므로 해당 필드는 반드시 volatile로 선언하여 캐시가 아닌 메모리에서 읽어오도록 처리 필요
- result 변수는 필드가 이미 초기화된 상황에서는 해당 필드를 딱 한 번만 읽도록 보장하는 역할
- 반드시 필요하지 않지만 성능을 높여주고 저수준 동시성 프로그래밍에 표준적으로 적용되는 방법
- 정적 필드에도 이중 검사 관용구를 적용할 수 있지만 앞서 설명한 정적 필드에는 지연 초기화 홀더 클래스 관용구를 사용하는 것을 권장
5. 단일 검사 관용구 (Single-Check)
코드 부연 설명
- 반복해서 초기화해도 상관없는 인스턴스 필드를 지연 초기화해야 할 경우가 존재하는데 이런 케이스라면 이중검사 관용구에서 두 번째 검사를 생략 가능
- 하지만 필드는 여전히 volatile로 선언하여 동기화 비용을 제거
1번부터 5번 초기화 기법 간단 정리
- 인스턴스 필드 초기화하는 1번부터 5번까지의 방법은 기본 타입 필드와 객체 참조 필드 모두에 적용 가능
- 이중 검사와 단일 검사 관용구를 수치 기본 타입 필드에 적용할 경우 필드의 값을 null 대신 숫자 기본 타입 변수의 기본값인 0과 비교하면 됨
6. 짜릿한 단일검사 관용구(Racy Single-Check)
코드 부연 설명
- 모든 쓰레드가 필드의 값을 다시 계산해도 상관없고 필드의 타입이 long과 double을 제외한 다른 기본 타입일 경우 단일 검사의 필드 선언에서 volatile 한정자를 없애도 됨
- 단일 검사 관용구에서 volatile 한정자를 없앤 관용구를 짜릿한 단일 검사 관용구라고 지칭
- 필드 값을 업데이트하는 메서드인 updateValue()는 단일 연산으로 이루어지므로, 여러 쓰레드가 동시에 이 메서드를 호출해도 안전하게 작동
- 아주 이례적인 기법으로 보통은 쓰이지 않음
정리
- 대부분의 필드는 지연시키지 말고 바로 초기화하는 것을 권장
- 성능 때문에 혹은 위험한 초기화 순환을 막기 위해서 지연초기화를 사용한다면 올바르게 사용해야 하며 1번부터 5번 방법 중 하나를 적용하는 것을 권장
- 인스턴스 필드에는 이중 검사 관용구를 정적 필드에는 지연 초기화 홀더 클래스 관용구를 사용하는 것을 권장
- 반복해서 초기화해도 괜찮은 인스턴스에는 단일 검사 관용구도 고려해 보는 것을 권장
참고
이펙티브 자바
반응형
'JAVA > Effective Java' 카테고리의 다른 글
[아이템 85] 자바 직렬화의 대안을 찾으라 (1) | 2024.05.12 |
---|---|
[아이템 84] 프로그램의 동작을 쓰레드 스케줄러에 기대지 말라 (0) | 2024.04.22 |
[아이템 82] 쓰레드 안전성 수준을 문서화하라 (0) | 2024.04.07 |
[아이템 81] wait와 notify보다는 동시성 유틸리티를 애용하라 (0) | 2024.04.07 |
[아이템 80] 쓰레드보다는 실행자, 태스크, 스트림을 애용하라 (0) | 2024.04.07 |