핵심 요약
- 일반적으로 필드는 지연시키지 말고 곧바로 초기화해야 한다.
- 성능 혹은 초기화 순환을 막기 위해 꼭 지연 초기화를 써야 한다면 제대로 사용하라.
✨ 적절한 초기화 방법 선택 가이드
더보기
- 일반적인 초기화: 대부분의 상황에서 적절한 방법
- synchronized 접근자: 인스턴스 필드에 적절하나 이중 검사 관용구가 더 좋은 방법
- 홀드 클래스 관용구: 정적 필드에 적절한 방법
- 이중 검사 관용구: 인스턴스 필드에 적절한 방법
- 단일 검사 관용구: 여러 번 초기화해도 문제가 없는 인스턴스 필드에 고려할 수 있는 방법
- 짜릿한 단일 검사 관용구: 대부분의 상황에서 권장되지 않는 방법
지연 초기화(lazy initialization)
- 객체나 데이터를 실제로 필요한 순간까지 초기화하지 않는 기법이다.
- 장점
- 메모리와 같은 자원의 효율적인 사용을 위해 사용되며, 특정 조건에서 성능 최적화의 효과도 기대할 수 있다.
- 초기화 순서의 의존성 문제를 피할 수 있으므로 초기화 순환성 문제를 해결할 수 있다.
- 하지만 대부분의 초기화 순환성 문제는 잘못된 설계 때문이다 ..
- 단점 및 주의할 점
- 초기화 확률, 초기화 비용, 사용 빈도 등 여러 요소를 고려하여 결정해야 하는 최적화 전략이다.
- 프로파일링 도구를 사용하여 지연 초기화 적용 전후의 성능을 측정하는 것이 유일한 방법이다.
- 특히, 멀티스레드 환경에서 지연 초기화를 사용할 때는 주의가 필요하다.
- 여러 스레드가 지연 초기화 필드에 동시에 접근하면 동기화 문제가 발생할 수 있다.
- 초기화 확률, 초기화 비용, 사용 빈도 등 여러 요소를 고려하여 결정해야 하는 최적화 전략이다.
✍ 필요할 때까지 하지 말라 (item 67. 최적화는 신중히 하라)
✍ 대부분의 상황에서 일반적인 초기화가 지연 초기화보다 낫다.
❔ 초기화 순환성(initialization circularity)
더보기
- 클래스나 객체의 초기화 과정에서 발생할 수 있는 문제로서, 클래스나 객체가 초기화되는 동안 그와 관련된 다른 클래스나 객체도 초기화돼야 하는 상황이 생길 때 나타난다.
- 이런 순환적인 의존성이 발생하면, 어떤 클래스나 객체를 먼저 초기화해야 할지 명확하지 않아 문제가 발생할 수 있다.
- ex. 클래스 A가 초기화되기 위해 클래스 B의 초기화가 필요하고, 반대로 클래스 B가 초기화되기 위해 클래스 A의 초기화가 필요한 경우
- 단일 책임 원칙과 의존 역전 원칙을 따름으로써 이런 문제를 피할 수 있다.
지연 초기화 방법
🐥 정적 필드와 인스턴스의 지연 초기화
더보기
- 정적 필드의 지연 초기화
- 정적 필드는 클래스가 로드되는 시점에 초기화된다.
- 필드를 사용하지 않을 가능성이 크거나, 초기화에 큰 비용이 들어가는 경우 효율적일 수 있다.
- 인스턴스 필드의 지연 초기화
- 인스턴스는 객체 생성 시점에 초기화된다.
- 필드에 대한 연산이 복잡하거나 리소스를 많이 사용하는 경우 효율적일 수 있다.
아래 모든 예시 코드는 스레드 안전하다.
또한, 수치 기본 타입 필드에 적용한다면 필드의 값을 null 대신 기본값인 0과 비교하면 된다.
1. 일반적인 초기화
: 대부분의 상황에서 일반적인 초기화가 지연 초기화보다 낫다.
변경 가능성을 최소화하고자 final 한정자를 사용하였다.(item 17. 변경 가능성을 최소화하라)
// 인스턴스 필드를 초기화하는 일반적인 방법
public class InstanceFieldExample {
private final FieldType instanceField;
public InstanceFieldExample() {
this.instanceField = computeFieldValue();
}
private FieldType computeFieldValue() {
return new FieldType("Instance Field Initialized");
}
public FieldType getInstanceField() {
return instanceField;
}
public static void main(String[] args) {
InstanceFieldExample instanceFieldExample = new InstanceFieldExample();
System.out.println(instanceFieldExample.getInstanceField());
}
}
// 정적 필드를 초기화하는 일반적인 방법
public class StaticFieldExample {
private static final FieldType staticField;
static {
staticField = computeFieldValue();
}
private static FieldType computeFieldValue() {
return new FieldType("Static Field Initialized");
}
public static FieldType getStaticField() {
return staticField;
}
public static void main(String[] args) {
System.out.println(StaticFieldExample.getStaticField());
}
}
2. synchronized 접근자로 지연 초기화
: 지연 초기화가 초기화 순환성을 깨뜨릴 것 같으면 synchronized를 단 접근자를 사용하자.
: 인스턴스 필드 지연 초기화에 적절하다.
지연 초기화로 초기화 순환성 문제를 방지하고, synchronized 접근자를 사용하여 스레드 안전성을 보장하는 방법이다.
public class InstanceFieldExample {
private FieldType field;
private FieldType computeFieldValue() {
return new FieldType("Instance Field Initialized");
}
// 필드에 대한 지연 초기화와 동기화
public synchronized FieldType getField() {
if (field == null) {
field = computeFieldValue();
}
return field;
}
public static void main(String[] args) {
InstanceFieldExample instance = new InstanceFieldExample();
System.out.println(instance.getField());
}
}
public class StaticFieldExample {
private static FieldType field;
private static final Object lock = new Object();
private static FieldType computeFieldValue() {
return new FieldType("Static Field Initialized");
}
// 필드에 대한 지연 초기화와 동기화
public static FieldType getField() {
synchronized (lock) {
if (field == null) {
field = computeFieldValue();
}
return field;
}
}
public static void main(String[] args) {
System.out.println(StaticFieldExample.getField());
}
}
3. 홀드 클래스 관용구로 지연 초기화
: 성능 때문에 정적 필드를 지연 초기화해야 한다면 지연 초기화 홀더 클래스 관용구를 사용하자.
클래스가 처음으로 사용될 때 클래스 초기화가 발생한다는 자바의 특성을 이용하는 패턴이다.
public class HolderClassIdiom {
// 내부 정적 홀더 클래스
private static class FieldHolder {
// 클래스 로딩 시점에 한 번만 실행된다.
static final FieldType field = computeFieldValue();
}
private static FieldType computeFieldValue() {
return new FieldType("Initialized by Lazy Initialization Holder");
}
public static FieldType getField() {
return FieldHolder.field;
}
public static void main(String[] args) {
System.out.println(HolderClassIdiom.getField());
}
}
더보기
- FieldHolder 클래스는 getField 메소드가 호출될 때까지 로드되지 않는다.
- 따라서 FieldHolder.field는 getField 메소드를 호출할 때까지 초기화되지 않는다.
- 자바의 일반적인 VM은 클래스 초기화 과정이 스레드에 안전하다는 보장이 있다.
- 따라서 동기화 없이도 멀티스레드 환경에서 안전하게 필드를 초기화할 수 있다.
- 동기화의 오버헤드(synchronized 키워드나 별도의 잠금 객체) 없이 정적 필드를 안전하게 지연 초기화할 수 있다.
- 클래스 초기화 완료된 후에는 VM이 동기화 코드를 제거하게 되므로, 필드에 동기화 없이 바로 접근할 수 있다.
4. 이중검사 관용구로 지연 초기화
: 성능 때문에 인스턴스 필드를 지연 초기화해야 한다면 이중검사 관용구를 사용하라.
: 정적 필드의 경우 홀더 클래스를 사용하라 (이중검사 관용구는 volatile의 오버헤드 존재)
초기화된 후에는 동기화 오버헤드를 제거하여 성능을 향상시키는 기법이다.(item 79. 과도한 동기화는 피하라)
public class DoubleCheck {
private volatile FieldType field;
private FieldType getField() {
FieldType result = field;
if (result != null) // 첫 번째 검사 (락 사용 안 함)
return result;
synchronized (this) {
if (field == null) // 두 번째 검사 (락 사용)
field = computeFieldValue();
return field;
}
}
private FieldType computeFieldValue() {
return new FieldType("Initialized by Double Check");
}
public static void main(String[] args) {
DoubleCheck instance = new DoubleCheck();
System.out.println(instance.getField());
}
}
더보기
- 동작 방식
- 동기화 블록을 사용하지 않고 result가 null이 아닌지 확인한다. null이 아닐 경우 동기화 없이 필드 값을 반환한다. (첫 번째 검사)
- 첫 번째 검사가 실패하면 동기화 블록에 진입한다. 필드가 아직 초기화되지 않았을 경우에만 발생한다.
- 동기과 블록 내에서 필드가 여전히 null인지 확인한다. 만약 다른 스레드가 필드를 초기화했다면 필드 값을 반환한다. 여전히 null일 경우 필드를 초기화한다. (두 번째 검사)
- volatile로 선언하여 명령어 재정렬과 가시성 문제를 방지해야 한다.(item 78. 공유 중인 가변 데이터는 동기화해 사용하라)
- 명령어 재정렬은 최적화 기법으로 프로그램 코드의 실행 순서를 변경하는 방법이다.
- 이로 인해 아직 초기화되지 않은 객체를 볼 가능성이 있다.
- volatile 키워드는 이러한 명령어 재정렬을 방지한다.
- result 지역 변수는 성능 최적화를 위해 사용되며, 특히 volatile 필드의 성능 오버헤드를 최소화하기 위한 목적으로 사용된다.
- volatile은 항상 메인 메모리에서 최신 값을 읽거나 쓰므로 오버헤드가 발생한다.
- field 변수에 두 번 접근하는 대신 result 지역 변수를 사용하여 메인 메모리 접근을 최소화한다.
- 지역 변수는 스택 메모리에 저장돼 접근이 빠르다.
- 멀티 스레드 환경에서는 이러한 사소한 최적화도 꽤나 큰 차이를 보인다고 한다.
5. 단일 검사 관용구로 지연 초기화
: 여러 번 초기화해도 상관없는 인스턴스 필드를 지연 초기화할 경우 사용할 수 있다.
이중 검사 관용구는 성능 최적화를 위해 동기화 블록 내에서 필드가 초기화되었는지 다시 확인한다.
여러 번 초기화해도 문제가 되지 않는다면, 이중 검사의 두 번째 검사를 생략하고 단일 검사 관용구를 사용할 수 있다.
public class SingleCheck {
private volatile FieldType field;
private FieldType getField() {
FieldType result = field;
if (result == null)
field = result = computeFieldValue();
return result;
}
private static FieldType computeFieldValue() {
return new FieldType("Initialized Value");
}
public static void main(String[] args) {
SingleCheck instance = new SingleCheck();
System.out.println(instance.getField());
}
}
더보기
- 이중 검사 관용구와 마찬가지로 volatile과 result 지연 변수를 사용한다.
- 필드의 반복 초기화가 문제를 일으키지 않기 때문에 동기화를 생략할 수 있다.
- 초기화가 중복으로 일어날 수 있으므로, 초기화 작업 비용이 클 경우 적합하지 않다.
6. 짜릿한 단일 검사 관용구
: 여러 번 초기화해도 상관없는 기본 필드(long과 double 제외)를 지연 초기화할 경우 사용할 수 있다.
: 대부분의 상황에서는 권장되지 않는다.
여러 번 초기화해도 되고 long과 double을 제외한 기본 필드라면 단일 검사 관용구에서 volatile을 생략할 수 있다.
public class ThrillingSingleCheck {
private FieldType field;
private FieldType getField() {
FieldType result = field;
if (result == null)
field = result = computeFieldValue();
return result;
}
private static FieldType computeFieldValue() {
return new FieldType("Initialized Value");
}
public static void main(String[] args) {
ThrillingSingleCheck instance = new ThrillingSingleCheck();
System.out.println(instance.getField());
}
}
더보기
- volatile 한정자를 생략하여 필드에 대한 접근이 더 빨라질 수 있다.
- 그러나 메모리 가시성 문제로 단일 검사 관용구보다 더 빈번하게 초기화 메소드가 여러 번 호출될 수 있다.
- 초기화가 중복으로 일어날 수 있으므로, 초기화 작업 비용이 클 경우 적합하지 않다.
'Language > Java' 카테고리의 다른 글
[모던 자바 인 액션] Ch 1 자바 8, 9, 10, 11 : 무슨 일이 일어나고 있는가? (0) | 2024.04.10 |
---|---|
[effective java] 아이템 84. 프로그램의 동작을 스레드 스케줄러에 기대지 말라. (0) | 2023.08.13 |
[effective java] 아이템 82. 스레드 안전성 수준을 문서화하라. (0) | 2023.08.13 |
[effective java] 아이템 81. wait와 notify보다는 동시성 유틸리티를 애용하라. (0) | 2023.08.13 |
[effective java] 아이템 80. 스레드보다는 실행자, 태스크, 스트림을 애용하라. (0) | 2023.08.13 |