자바에서 싱글톤 패턴을 구현하는 여러가지 방식에 대해 알아보려고 한다.

이전에 정리한 싱글톤 패턴에 관한 글이 궁금하다면 참고하면 좋겠다.

1. Eager initialization(이른 초기화 방식)

Singleton의 가장 기본적인 Eager initialization 방식이다. 먼저, 클래스 내에 전역 변수로 instance 변수를 생성하고 private static을 사용하여 인스턴스화에 상관없이 접근이 가능하면서 동시에 private 접근 제어 키워드를 사용해 EageInitialization.instance로 바로 접근 할 수 없도록 한다.

또, 생성자에도 pirvate 접근 제어 키워드를 붙여 다른 클래스에서 new EageInitialization(); 방식으로 새로운 인스턴스를 생성하는 것을 방지한다. 오로지 정적 메소드인 getInstance() 메소드를 이용해서 인스턴스를 접근하도록 하여 유일무이한 동일 인스턴스를 사용하는 기본 싱글톤 원칙을 지키게 한다.

이른 초기화 방식은 싱글톤 객체를 미리 생성해 놓는 방식이다. 항상 싱글톤 객체가 필요하거나 객체 생성 비용이 크게 들어가지 않는 경우에 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
public class EagerInitialization{
// private static으로 선언(전역 변수)
private static EagerInitialization instance = new EagerInitialization();

// private한 생성자
private EagerInitialization(){

}

public static EagerInitialization getInstance(){
return instance;
}
}

장점

  • static으로 생성된 변수에 싱글톤 객체를 선언했기 때문에 클래스 로더에 의해 클래스가 로딩될 때 싱글톤 객체가 생성된다. 또 클래스 로더에 의해 클래스가 최초 로딩될 때 객체가 생성됨으로 Thread-safe하다.

단점

  • 싱글톤 객체 사용 유무와 관계 없이 클래스가 로딩되는 시점에 항상 싱글톤 객체가 생성되고, 메모리를 잡고 있기 때문에 비효율적이다.

2. Lazy initialization(늦은 초기화 방식)

Eager initialization(이른 초기화 방식)과 정반대로 클래스가 로딩되는 시점이 아닌 클래스의 인스턴스가 사용되는 시점에서 싱글톤 인스턴스를 생성한다. 즉, 사용 시점까지 싱글톤 객체 생성을 미루기 때문에 사용하기 전까지 메모리를 점유하지 않는다. getInstacne() 메소드 안에서 instance가 null인 경우에만 new LazyInitialization()으로 싱글톤 객체를 할당한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class LazyInitialization{

private static LazyInitialization instance;

// 생성자
private LazyInitialization(){

}

public static LazyInitialization getInstance(){
if(instance == null){
intance = new LazyInitialization();
}
return instance;
}
}

[장점]

  • 싱글톤 객체가 필요할 때 인스턴스를 얻을 수 있다. Eager initialization 방식의 단점을 보완한 수 있다.(메모리 누수)

[단점]

  • 만약 multi-thread 환경에서 여러 곳에서 동시에 getInstance() 메소드를 호출할 경우 인스턴스가 두 번 생성될 여지가 있다. 즉, multi-thread 환경에서는 싱글톤의 철학이 깨질 수 있는 위험이 있다.

3. Thread safe Lazy initializtion

Lazy initializtion 방식에서는 multi-thread 환경에서 thread-safe 하지 않다는 단점을 보완하기 위해서 멀티 스레드에서 스레드들이 동시 접근하는 동시성 문제를 synchronized 키워드를 이용해 해결한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public ThreadSafeLazyInitialization{
private static ThreadSafeLazyInitialization intance;
// 생성자
private ThreadSafeLazyInitialization(){

}

public static synchronized ThreadSafeLazyInitialization getInstance(){
if(instance == null){
intance = new ThreadSafeLazyInitialization();
}
return instance;
}
}

[장점]

  • 위에서 언급했듯이 Lazy initialization 방식에서 thread-safe 하지 않은 점을 보완한다.

[단점]

  • synchronized 키워드를 사용할 경우 자바 내부적으로 해당 영역이나 메소드를 lock, unlock 처리하기 때문에 많은 비용이 발생한다. 대략 100배 정도 비효율적이라고 한다.
  • 따라서 많은 thread들이 getInstance()를 호출하게 되면 프로그램 전반적인 성능 저하가 발생한다.

3-1. Thread safe Lazy initialization + Double-checked locking 기법

위에서 Thread safe Lazy initialization을 보았다. 많은 스레드들이 동시에 synchronized 처리된 메소드를 접근하면 성능저하가 발생된다고 했다. 이를 좀 더 완화하기 위해서 Double-checked locking 기법을 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ThreadSafeLazyInitialization{
private static ThreadSafeLazyInitialization instance;
// 생성자
private ThreadSafeLazyInitialization(){

}

public static ThreadSafeLazyInitialization getInstance(){
// Double-checked locking
if(instance == null){
synchronized (ThreadSafeLazyInitialization.class){
if(instance == null){
instance = new ThreadSafeLazyInitialization();
}
}
}
return instance;
}
}

첫 번째 if문에서 instance가 null인 경우 synchronized 블록에 접근하고 한 번 더 if문으로 instance가 null인지 유무를 체크한다. 2번 모두 다 instance가 null인 경우에 new를 통해 인스턴스화 시킨다.
이후에는 instance가 null이 아니기 때문에 synchronized 블록을 타지 않는다. 이런 Double-checked locking(DCL) 기법을 통해 성능 저하를 보완할 수 있다.

4. Initialization on demand holder idiom(holder에 의한 초기화)

이 방법은 클래스 안에 클래스(Holder)를 두어 JVM의 Class Loader 매커니즘과 Class가 로드되는 시점을 이용한 방법이다. Lazy initialization 방식을 가져가면서 Thread간 동기화 문제를 동시에 해결할 수 있다.

로직은 InitializationOnDemandHolderIdiom 클래스에는 SingleTonHolder 클래스의 변수가 없기 때문에 InitializationOnDemandHolderIdiom 로딩시 SingleTonHolder 클래스를 초기화하지 않는다.

static으로 선언된 중첩 클래스 SingleTonHolder는 getInstance 메소드가 호출되기 전에는 참조되지 않으며, 최초로 getInstance() 메소드가 호출될 때 SingleTonHolder.instance를 참조하는 순간 클래스 로더에 의해 Class가 로딩되고 초기화되며, 싱글톤 객체를 생성하여 리턴한다.

또한, SingleTonHolder 안에 선언된 INSTANCE가 static 변수이기 때문에 클래스 로딩 시점에 한 번만 호출된다. 또한, final을 사용하기 때문에 다시 값이 할당되지 않도록 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class InitializationOnDemandHolderIdiom{
private InitializationOnDemandHolderIdiom instante;
// 생성자
private InitializationOnDemandHolderIdiom(){

}

public static class SingleTonHolder{
public static final InitializationOnDemandHolderIdiom INSTANCE = new InitializationOnDemandHolderIdiom();
}

public static InitializationOnDemandHolderIdiom getInstance(){
return SingleTonHolder.INSTANCE;
}
}

위의 방법은 현재까지 가장 많이 사용되는 것으로 알려져있다. 그만큼 지금까지 나온 방법 중 가장 효율적인 방법이라고 한다.

참고