/ #JAVA#디자인패턴

싱글톤 패턴(Singleton Pattern)

GOF 디자인 패턴

싱글톤 패턴(Singleton Pattern)

싱글톤 패턴은 특정 클래스의 인스턴스가 오직 하나임을 보장하고, 이 인스턴스에 접근할 수 있는 방법을 제공합니다. 즉, 특정 클래스의 객체는 하나만 생성되도록 하여 동일 인스턴스를 재사용 하는 패턴입니다.

구조

singleton

참여 객체

Singleton

생성자의 접근자를 private로 설정하여 새로운 인스턴스를 생성하지 못하도록 하고, 인스턴스(Instance)의 접근을 위한 오퍼레이션(Operation)을 static으로 정의하여 해당 메소드에 의해서만 인스턴스의 접근이 가능하도록 합니다.

인스턴스는 싱글턴 객체의 클래스 변수로 설정되어 클래스가 메모리의 데이터영역에 저장될 때 같이 저장됨으로써 오직 한번만 인스턴스를 생성할 수 있습니다. 이로써 싱글턴 객체는 유일한 인스턴스의 생성에 대한 책임을 지게 됩니다.

종류

이른 초기화 방식 (Eager Initialization)

이른 초기화 방식은 싱글톤 패턴의 가장 기본적인 유형입니다.

전역변수로 인스턴스를 생성하고 접근자를 private로 설정함으로써 싱글톤 객체의 오퍼레이션 메소드에 의해서만 접근이 가능하도록 합니다. 또한 생성자의 접근자를 private로 절정함으로써 외부에서 인스턴스를 생성할 수 없도록 만듭니다.

EagerSingleton.java
package design_pattern.creational.singleton;

public class EagerSingleton {
    
    private static final EagerSingleton INSTANCE = new EagerSingleton();

    private EagerSingleton() {}

    public static EagerSingleton getInstance() {
        return INSTANCE;
    }
}

자바에서는 static으로 선언한 클래스 변수가 곧 전역변수입니다. 클래스가 메모리의 데이터 영역에 저장될 때 클래스 변수인 INSTANCE는 싱글톤 인스턴스를 한번만 생성하여 참조함으로써 동일한 인스턴스를 재사용하는 싱글톤 원칙을 지킬 수 있게 됩니다.

이른 초기화 방식의 장점은 전역변수로 싱글톤 인스턴스를 만들었기때문에 클래스 로더에 의하여 클래스가 로딩될 때 오직 한번만 인스턴스가 생성되므로 Thread-safe하게 싱글톤 객체를 생성할 수 있습니다.

단점은 객체의 사용여부와 상관없이 클래스 로딩 시점에 인스턴스가 생성되어 프로그램이 종료될때까지 메모리를 점유하기때문에 자주 사용하지 않는 객체라면 비효율적일 수 있습니다.

늦은 초기화 방식 (Lazy Initialization)

늦은 초기화 방식은 이른 초기화 방식과 반대로 클래스의 오퍼레이션 메소드가 호출될 때 인스턴스를 생성합니다. 즉, 클래스 메소드가 호출될 때까지 메모리를 점유하지 않습니다.

LazySingleton.java
package design_pattern.creational.singleton;

public class LazySingleton {

    private static LazySingleton INSTANCE = null;

    private LazySingleton() {}

    public static LazySingleton getInstance() {
        if(INSTANCE == null){
            INSTANCE = new LazySingleton();
        }
        return INSTANCE;
    }
}

늦은 초기화 방식의 장점은 필요할때 객체를 생성할 수 있다는 것입니다.

단점은 멀티 쓰레드(Thread) 환경에서 동시에 호출되어 싱글톤 원칙을 보장할 수 없다는 것입니다.

쓰레드 안전 이른 초기화 방식 (Thread safe Lazy Initialization)

늦은 초기화 방식을 보완하여 synchronized 키워드를 메소드에 선언하여 쓰레드 안전(Thread safe) 상태로 싱글톤을 생성할 수 있는 방식입니다. synchronized로 선언된 메소드는 하나의 쓰레드가 메소드를 사용하면 lock을 걸어 다른 쓰레드를 대기상태로 만듭니다.

ThreadSafeLazySingleton.java
package design_pattern.creational.singleton;

public class ThreadSafeLazySingleton {

    private static ThreadSafeLazySingleton INSTANCE = null;

    private ThreadSafeLazySingleton() {}

    public static synchronized ThreadSafeLazySingleton getInstance() {
        if(INSTANCE == null){
            INSTANCE = new ThreadSafeLazySingleton();
        }
        return INSTANCE;
    }
}

이 방식의 장점은 Thread-safe 환경이라는 것입니다.

단점은 syncronized가 선언된 메소드가 호출이 많으면 성능이 나빠질 수 있습니다.

DCL 방식 (Double-Checked locking)

DCL 방식은 syncronized를 메소드에 선언하는 것이 아니라 메소드 내부에 syncronized 블락을 만드는 방식입니다.

DCLSingleton.java
package design_pattern.creational.singleton;

public class DCLSingleton {

    private volatile static DCLSingleton INSTANCE = null;

    private DCLSingleton() {}

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

위에 코드와 같이 인스턴스가 없을때 synchronized 키워드를 통해 블락을 만들어 하나의 쓰레드만 작업할 수 있도록 하고 블락 내부에서 다시한번 인스턴스 존재 여부를 체크하게 됩니다. 2번 체크하기때문에 DCL(Double Checking Locking) 방식이라 불립니다.

private 접근자로 설정한 INSTANCE 클래스를 보면 volatile이라는 키워드를 사용했습니다. 하나의 프로세서(CPU)는 최적화를 위해 변수를 메인 메모리에 바로 저장하지 않고 캐쉬메모리에 저장하여 성능을 향상시킵니다. 그러나 volatile로 선언된 변수는 변수를 캐쉬메모리에 저장하지 않고 곧바로 메인메모리에 반영하도록 합니다.

volatile 키워드를 사용하지 않고 멀티코어 환경에서 작업한다면 인스턴스를 캐쉬메모리에 저장하여 메인메모리에 인스턴스를 생성하기전에 다른 코어의 쓰레드가 비어있는 메모리를 참조하기때문에 문제가 발생할 수 있습니다.

Holder에 의한 초기화 방식 (Initialization on demand holder idiom)

이 방식은 클래스안에 클래스 홀더(Holder)를 두어 JVM 클래스 로더 매커니즘과 클래스가 로드되는 시점을 이용한 방식입니다. 현재까지 가장 많이 이용되고 있는 방식입니다.

StaticHolderSingleton.java
package design_pattern.creational.singleton;

public class StaticHolderSingleton {

    private StaticHolderSingleton(){}
	
	private static class SingleTonHolder {
		private static final StaticHolderSingleton INSTANCE = new StaticHolderSingleton();
	}
	
	public static StaticHolderSingleton getInstance(){
		return SingleTonHolder.INSTANCE;
	}
}

위에 코드에서 보는 바와 같이 클래스 안에 private 접근자로 중첩클래스를 선언하고 중첩클래스 내부에 private 접근자로 클래스 변수를 선언했습니다. 중첩 클래스는 오퍼레이션 메소드(Operation Method)에 의하여 호출되기 전까지 참조되지 않으며, 오퍼레이션 메소드에 의하여 호출될때 클래스 로더에 의하여 싱글톤 인스턴스를 생성하게 됩니다.

중첩클래스 내부에 선언된 클래스 변수는 static으로 선언되었기 때문에 중첩클래스가 로드되는 시점에 한번만 싱글톤 인스턴스를 생성할 수 있게 됩니다. 또한 final 키워드를 통해 다시 값이 할당되지 않도록 하여 싱글톤 원칙을 지킬 수 있게 됩니다.