DesignPattern

Singleton

Singleton pattern(싱글톤 패턴)이란 애플리케이션에서 인스턴스를 하나만 만들어 사용하기 위한 패턴이다.

 

Connection Pool(커넥션 풀), Thread Pool(스레드 풀), 디바이스 설정 객체 등의 경우, 인스턴스를 여러 개 만들게 되면 자원을 낭비하게 되거나 버그를 발생시킬 수 있으므로 오직 하나의 인스턴스만 생성하여 사용하도록 하는 것이 싱글톤 패턴의 목적이다.

 

하나의 인스턴스를 메모리에 등록해서 여러 스레드가 동시에 해당 인스턴스를 공유하여 사용할 수 있으므로, 요청이 많은 곳에서 사용하면 효율을 높일 수 있다.

주의! 해야할 점은 싱글톤을 만들때 동시성(Concurrency)문제를 고려하여 설계해야한다. (Thread-Safe)

구현

하나의 인스턴스만을 유지하기 위해 인스턴스 생성에 특별한 제약을 걸어두어야 한다.
  1. new를 실행할 수 없도록 생성자에 private 접근 제어자를 지정하고
  2. 유일한 단일 객체를 반환할 수 있도록 정적 메소드를 지원해야한다.
  3. 또한 유일한 단일 객체를 참조할 정적 참조변수가 필요하다.

 

예제 v1.

public class Singleton {
    private static Singleton singletonObj;

    private Singleton() {}

    public static Singleton getInstance() {
        if (singletonObj == null) {
            singletonObj = new Singleton();
        }
        return singletonObj;
    }
}

위 코드는 멀티스레드 환경에서 적용하다보면 문제가 발생할 수 있다.
동시에 접근하다가 하나만 생성되어야 하는 인스턴스가 두개 생성될 수 있는 것이다.
그렇기 때문에 getInstance()메소드를 동기화시켜야 멀티스레드 환경에서의 문제가 해결된다.

 

예제 v2. synchronized(동기화 블럭, Thread-safe)

public class Singleton {
    private static Singleton singletonObj;

    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if (singletonObj == null) {
            singletonObj = new Singleton();
        }
        return singletonObj;
    }
}

synchronized키워드를 사용하게 되면 성능상에 문제점이 존재한다. (100배 정도 느려진다고함)

 

좀 더 효율적으로 제어할 수는 없을까?

 

예제 v3. Double Checking Locking(DCL, Thread-safe)

public class Singleton {
    private static volatile Singleton singletonObj;

    private Singleton() {}

    public static Singleton getInstance() {
        if (singletonObj == null) {
            synchronized (Singleton.class) {
                if (singletonObj == null) {
                    singletonObj = new Singleton();
                }
            }
        }
        return singletonObj;
    }
}

DCL(Double Checking Locking)을 써서 getInstance()에서 동기화 되는 영역을 줄일 수 있다.
초기에 객체를 생성하지 않으면서도 동기화하는 부분을 작게 만들었다.

그러나 이 코드는 멀티코어 환경에서 동작할 때, 하나의 CPU를 제외하고는 다른 CPU가 lock이 걸리게 된다.

 

그렇기 때문에 다른 방법이 필요하다.

예제 v4.

public class Singleton {
    private static volatile Singleton singletonObj = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return singletonObj;
    }
}

volatile : 컴파일러가 특정 변수에 대해 옵티마이저가 캐싱을 적용하지 못하도록 하는 키워드이다.

클래스가 로딩되는 시점에 미리 객체를 생성해두고 그 객체를 반환한다.

 

예제 v5. LazyHolder(게으른 홀더) : Thread-safe

LazyHoder 방식은 가장 많이 사용되는 싱글턴 구현 방식이다.

volatile 이나 synchronized 키워드 없이도 동시성 문제를 해결하기 때문에 성능이 뛰어나다.

public class Singleton {
    private Singleton() {}

    /**
     * static member class
     * 내부클래스에서 static변수를 선언해야하는 경우 static 내부 클래스를 선언해야만 한다.
     * static 멤버, 특히 static 메서드에서 사용될 목적으로 선언
     */
    private static class InnerInstanceClazz {
        //클래스 로딩 시점에서 생성
        private static final Singleton singletonObj = new Singleton();
    }

    public static Singleton getInstance() {
        return InnerInstanceClazz.singletonObj;
    }
}

싱글톤 클래스에는 InnerInstanceClazz 클래스에 변수가 없기 때문에, static 맴버 클래스더라도
클래스 로더가 초기화 과정을 진행할때 InnerInstanceClazz 메소드를 초기화하지 않고 getInstance() 메소드를 호출할 때 초기화된다.

즉, 동적바인딩(Dynamic Binding)(런타임시에 성격이 결정)의 특징을 이용하여 Thread-safe 하면서 뛰어난 성능을 가진다.

InnerInstanceClazz 내부 인스턴스는 static 이기 때문에 클래스 로딩 시점에 한번만 호출된다는 점을 이용한것이며, final을 써서 다시 값이 할당되지 않도록 한다.

 

 

Reference