싱글톤을 구현하는 방법은 다양하다
싱글톤 패턴에서 가장 간단한 형태의 구현 방법
싱클톤 클래스의 인스턴스를 클래스 로딩 단계에서 생성하는 방법이다
하지만 어플리케이션에서 해당 인스턴스를 사용하지 않더라도 인스턴스를 생성하기 때문에 자칫 낭비가 발생할 수도 있다.
public class Car {
private static final Car instance = new Car();
// private constructor to avoid client applications to use constructor
private Car() {}
public static Car getInstance() {
return instance;
}
}
위 방법을 사용할 땐, 싱글톤 클래스가 다소 적은 리소스를 다룰 때여야 한다.
File System, Database Connetion 등 큰 리소스들을 다루는 싱글톤을 구현할 때는 위 같은 방식을 이용하기보단 getInstance() 메서드가 호출될 때까지 싱글톤 인스턴스를 생성하지 않는 것이 더 좋다.
그리고 Eager Initialization은 Exception에 대한 Handling도 제공하지 않고있다.
Static Block Initialization은 Eager Initalization과 유사하지만 static block을 통해서 Exception Handling에 대한 옵션을 제공하고있다.
public class Car {
private static Car instance;
private Car(){}
//static block initialization for exception handling
static {
try {
instance = new Car();
} catch (exception e) {
throw new RuntimeException("Exception occured in creating car instance");
}
}
public static Car getInstance() {
return instance;
}
}
위 같이 구현할 경우 싱글톤 클래스의 인스턴스를 생성할 때 발생할 수 있는 예외에 대한 처리를 할 수 있지만, Eager Initialization과 마찬가지로 클래스 로딩 단계에서 인스턴스를 생성하기 때문에 여전히 큰 리소스를 다루는 경우에는 부적합하다.
이름에 걸맞게 위에 설명한 두 방식과는 다르게 나중에 초기화를 하는 방법이다.
이는 global access한 getInstance() 메서드를 호출할 때 인스턴스가 없다면 생성한다.
public class Car {
private static Car instance;
private Car(){}\
public static Car getInstance(){
if(instance == null) {
instance = new Car();
}
return instance;
}
}
Lazy는 앞선 두 방식이 안고있던 문제인 사용하지 않을 경우에 인스턴스가 낭비되는 것에 대한 어느 정도 해결책이 된다.
하지만 Lazy도 문제점이 있는데, multi-thread 환경에서의 동기화 문제이다.
만약 인스턴스가 생성되지 않은 시점에 여러 쓰레드가 동시에 getInstance()를 호출한다면 예상치 못한 결과를 얻을 수 있을뿐더러, 단 하나의 인스턴스를 생성한다는 싱글톤 패턴의 위반하는 문제점을 야기할 수 있다.
Lazy 방법을 구현해도 괜찮은 경우는 Single-thread 환경이 보장될 때 이다.
Thread safe Singleton은 Lazy의 문제를 해결하기 위한 방법으로, getInstance() 메서드에 synchronized를 걸어두는 방법인데, synchronized 키워드는 임계 영역(Critical Section)을 형성해 해당 영역에 오직 하나의 쓰레드만 접근 가능하게 해준다.
public class Car {
private static Car instance;
private Car(){}
public static synchronized Car getInstance() {
if(instance == null) {
instance = new Car();
}
return instance;
}
}
이 방식으로 구현한다면 getInstance() 메서드 내에 진입하는 쓰레드가 하나로 보장받기 때문에 멀티 쓰레드 환경에서도 정상 동작하게 된다, 하지만 synchronized 키워드 자체에 대한 비용이 크기때문에 너무 잦은 사용으로 남발하게되면 오히려 어플리케이션 성능이 떨어지게 된다.
그로인해 고안된 방식은 double checked locking 이다.
이는 getInstance() 메서드 수준에 lock 을 걸지 않고 instance가 null일 경우에만 synchronized가 동작하도록 한다.
public static Car getInstance() {
if(instance == null) {
synchronized (Car.class) {
if(instance == null) {
instance = new Car();
}
}
}
return instance;
}
Bill Pugh가 고안한 방식으로 inner static helper class 를 사용하는 방식
앞선 방식이 안고 있는 문제점들을 대부분 해결한 방식으로, 현재 가장 널리 쓰이는 싱글톤 구현 방법
public class Singleton {
private Singleton() {}
private static class SingletonHelper {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHelper.INSTANCE;
}
}
inner class로 인해 복잡해 보이지만 생각보다 간단하다
private inner static class 를 두어 싱글톤 인스턴스를 갖게 한다.
앞선 Eager 와 static block 방식과의 차이점이라면 SingletonHelper 클래스는 Singleton 클래스가 로드 될 때에도 로드 되지 않다가 getInstance()가 호출될 때 비로소 JVM 메모리에 로드되고 인스턴스를 생성하게 된다.
아울러 synchronized를 사용하지 않기 때문에 Thread safe의 문제였던 성능 저하 문제까지 해결되었다.
앞에서 설명한 모든 싱글톤 방식은 완전히 안전할 순 없다.
이유는 Java의 Reflection을 통해 싱글톤을 파괴할 수 있기 때문인데
Java의 거장 Joshua Bloch는 Enum으로 싱글톤을 구현하는 방법을 제안해냈다.
public enum EnumSingleton {
INSTANCE;
public static void doSomething() {
// do something
}
}
하지만 이 방법 또한 Lazy와 static block의 같이 사용하지 않을 경우의 메모리 문제를 해결하지 못한 것과 유연성이 떨어지는 면에 한계를 지니고 있다.
이 글의 핵심은 다양한 구현 방법이 있다는 것을 알게됬고 각각의 장단점을 지니고 있어
무엇이 옳고 그른지 판단할 수 없지만, inner static class 방식을 사용하는 것이 가장 최선의 방법이라고 생각이 든다.