Skip to content

Item 83. 지연 초기화는 신중히 사용하라 #73

@jseok0917

Description

@jseok0917

Chapter : 11. 동시성

Item : 83. 지연 초기화는 신중히 사용하라

Assignee : jseok0917


🍑 서론

지연 초기화(Lazy initialization)

  • 필드의 초기화 시점을 그 값이 처음 필요할 때까지 늦추는 기법

일반적인 초기화 vs 지연 초기화

  • 일반적인 초기화
public class InitializationExample {
    //즉시 초기화, 이 값을 바로 메모리에 할당
    //하지만, 이 필드를 잘 사용하지 않는다면 메모리 낭비가 될 수 있다.
    private Resource resource = new Resource(); 

    public Resource getResource() {
        return resource;
    }
}
  • 지연 초기화
public class LazyInitializationExample {
    private Resource resource; //선언만 해두고

    //getter를 통해 실제로 해당 필드를 가져올 때 값이 null이면 할당해준다
    public Resource getResource() { 
        //다만 해당 필드를 가져올때마다 계속 if문을 통한 검사로직이 필요 => 추가비용 발생
        if (resource == null) {
            resource = new Resource();
        }
        return resource;
    }
}
  • 요약
    • 일반적인 초기화 : 초기화 비용 O, 검사비용 없음, 코드단순
    • 지연 초기화 : 초기화 비용 낮음, 검사비용 O, 코드복잡
    • 대부분의 상황에서는 일반적인 초기화가 지연 초기화보다 낫다
    • 지연 초기화를 사용할 때는 항상 성능테스트 해보고 도입해야한다.


🍑 본론

지연 초기화는 어떤 상황에 써야할까?

  1. 성능 최적화를 위해

    • 필드의 초기화 비용이 크고, 필드를 낮은 빈도로 사용할 때
  2. 초기화 순환성을 피하기 위해 (아래 번외 참고, 책 443페이지에 번역오류있음, [Effective Java] Item 83. 지연 초기화로 인한 초기화 순환성의 영향 JavaBookStudy/JavaBook#45 참고)


  • 멀티스레드 환경에서의 지연 초기화
    • 지연 초기화하는 필드를 둘 이상의 스레드가 공유한다면
    • 한번만 초기화해야하는 필드를 두 개의 스레드가 동시에 초기화할 수 있으므로,
    • 동시성 이슈에 대한 해결책이 필요하다.

어떻게 지연 초기화 코드를 짜야할까?

  1. 멀티스레드 환경을 고려
public class LazyInitializationExample {
    private Resource resource; //선언만 해두고

    //getter를 통해 실제로 해당 필드를 가져올 때 값이 null이면 할당해준다
    //동시성을 고려하여 Synchronized 키워드로 작성
    public synchronized Resource getResource() { 
        if (resource == null) {
            resource = new Resource();
        }
        return resource;
    }
}
  • 동시성 문제는 간단히 해결되지만 synchronized로 성능이 오히려 저하된다. 사실 초기화가 두번 이루어지는게 문제가 될 수 있는건데, 두번 초기화돼도 상관없는 상황이면 그냥 synchonized 떼고 써도됨(83-5 단일검사 관용구)
  • 혹은, 성능에 관계없이 초기화 순환성을 피하기 위해 지연초기화를 사용하는 것이라면 그냥 이렇게 synchronized 를 사용하는게 명료한 답

  1. 지연 초기화 홀더 클래스 관용구 사용
  • 자바에서 클래스는 클래스가 처음 쓰일 때 비로소 초기화된다는 점을 이용
public class LazyInitializationExample {

    //정적 내부 클래스 사용
    private static class ResourceHolder {
        static final Resource INSTANCE = new Resource();
    }

    public Resource getResource() {
        return ResourceHolder.INSTANCE;
    }
}
  • synchronized 사용할 필요 아예 없음 => 성능저하X
  • 클래스 초기화할 때는 VM이 알아서 필드접근에 대한 동기화 시켜줌

  1. 혹은 이중검사 관용구를 사용
public class LazyInitializationExample {
    private volatile Resource resource; //선언만 해두고, volatile로!

    //getter를 통해 실제로 해당 필드를 가져올 때 값이 null이면 할당해준다
    //Synchronized 키워드를 메서드 내부 검사로직에 넣어준다
    public Resource getResource() { 
        Resource result = this.resource;

        //값이 이미 할당돼있는 상태면 바로 결과 반환(lock 사용X)
        if (result != null) {
            return result;
        }

        //초기화가 안돼있을때만 lock 사용
        synchronized(this) {
            if (result == null) {
                result = new Resource();
            }
            return result;
        }
    }
}
  • 이미 초기화된 상태에서는 synchronized가 작동하지 않으므로 성능저하가 생기지 않음
  • 필드를 volatile로 선언해야함(아이템78 참고)


🍑 결론

  • 지연초기화는 항상 성능테스트 해보고 써야한다.
  • 성능 문제로 쓰는거면 지연 초기화 홀더 클래스 관용구 사용하면 좋음
  • 코드 ㅈㄴ복잡해짐 왜 왠만하면 일반적인 초기화가 낫다고 하는지 알겠죠?

번외

초기화 순환성이란?

  • 클래스나 객체를 초기화할 때, 둘 이상의 요소가 서로를 참조해야하는 경우 발생할 수 있음
  1. StackOverFlow
	public static class A {
	    private B b;

        // A 객체 생성 시 A 인스턴스를 새로 만들기 때문에 문제가 발생한다.
	    public A() {
	        b = new B(new A());  // 새로운 A 인스턴스를 생성하며, 이로 인해 무한 루프가 발생
	    }
	}

	public static class B {
	    private A a;

	    public B(A a) {
	        this.a = a;
	    }
	}

	public static void main(String[] args) {
	   A a = new A();  // 무한 루프에 빠짐
	}

  • 지연 초기화를 이용한 문제 해결
public static class A {
    private B b;

    public A() {
        // 지연 초기화를 위해 B 인스턴스 생성을 미룸
    }

    //두번 초기화되는 문제를 해결하기 위해 synchronized
    public synchronized B getB() {
        if (b == null) {
            b = new B(this); // 필요 시에만 B 인스턴스 생성
        }
        return b;
    }
}

public static class B {
    private A a;

    public B(A a) {
        this.a = a;
    }
}

public static void main(String[] args) {
    A a = new A();  // 무한 루프에 빠지지 않음
    B b = a.getB(); // B 인스턴스가 처음 사용될 때 생성
}

Referenced by

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions