GoF::싱글톤 패턴(Singleton Pattern)
강의/코딩으로 학습하는 GoF의 디자인 패턴

GoF::싱글톤 패턴(Singleton Pattern)

이 글은 '백기선의 코딩으로 학습하는 GoF의 디자인 패턴' 강좌를 듣고 해당 내용을 공부하며 정리한 글입니다.

싱글톤 패턴(Singleton Pattern)

인스턴스를 오직 한 개만 만들어서 제공하는 클래스가 필요한 경우에 사용하는 패턴

즉, 클래스가 최초 한번만 메로리를 할당받고 그 메모리에 인스턴스를 만들어 사용하는 디자인 패턴으로 생성자가 여러 차례 호출되더라도 실제 생성되는 인스턴스는 기존에 생성된 인스턴스이다.

 

정의(Definition)

소프트웨어 디자인 패턴에서 싱글턴 패턴(Singleton pattern)을 따르는 클래스는, 생성자가 여러 차례 호출되더라도 실제로 생성되는 객체는 하나이고 최초 생성 이후에 호출된 생성자는 최초의 생성자가 생성한 객체를 리턴한다. 이와 같은 디자인 유형을 싱글턴 패턴이라고 한다. 주로 공통된 객체를 여러 개 생성해서 사용하는 DBCP(DataBase Connection Pool)와 같은 상황에서 많이 사용된다고 한다.

 

사용 이유

시스템 런타임, 환경 세팅 관련 정보 등 인스턴스가 여러 개일 때 문제가 발생하는 경우 등이 있는데 싱글톤 패턴을 사용함으로써 가져갈 수 있는 이점은 다음과 같을 것이다.

  1. 메모리, 속도 측면 : 객체의 인스턴스를 재사용하기 때문(고정된 메모리 영역을 사용)
  2. 데이터 공유가 쉬움 : 기존 인스턴스가 전역으로 사용되기 때문
  3. 인스턴스가한 개만 존재하는 것을 보증하고 싶은 경우

보통 이런 이점 중 3번째가 개인적으로 가장 중요하다고 생각된다. 여러 다른 이유들은 다른 요소(결합도 등)들과 고려했을 때 문제가 될 수도 있지만 만약 인스턴스가 한 개만 존재하는 것을 보증하고 싶은 경우에는 싱글톤을 써야 한다고 생각하기 때문입니다.

 

싱글톤 패턴의 사용

싱글톤 패턴의 공통된 특징은 private constructor를 갖는다는 점, static method를 사용한다는 점이다.

싱글톤 패턴에서는 생성자를 클래스 자체에서만 접근할 수 있어야 하기에 private와 같은 접근 제어자를 걸어주는 것이다. 만약, public constructor라면 다른 부분에서 인스턴스와 시킬 수 있기 때문이다.

 

추가로 인스턴스는 생성 이후 수정이 되지 않도록 막아주어야 한다. 그렇지 않다면 나중에 해당 클래스의 인스턴스를 NULL로 초기화해버릴 수도 있기 때문이다.

 

이른 초기화(Eager Initialization)

static 키워드를 통해 클래스 로더가 초기화하는 시점에 정적 바인딩(static binding)을 통해 해당 인스턴스를 메모리에 등록하기 때문에 Thread-safe 하다. 다만, 실제 해당 인스턴스를 사용하지 않더라도 생성을 하기에 효율성이 떨어지게 됩니다. 

public class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton() {}
    
    public static Singleton getInstance() {
        return instance;
    }
}

 

늦은 초기화(Lazy Initialization)

인스턴스를 실제 사용하는 시점에서 생성하는 방법으로 실제 사용하지 않을 시에는 생성하지 않기 때문에 이른 초기화의 방법보다는 좀 더 효율적일 수 있으나 여기서의 getInstance는 멀티 스레드 환경에서는 안전하지 않습니다.(Thread Safe하지 않음)

public class Singleton {

    private static Singleton instance;
    private Singleton() { }

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

해당 방법이 멀티 스레드에서 안전하지 않는 이유는 만약 두 스레드가 동시에 즉, 먼저 접근한 인스턴스가 생성되기 전에 해당 인스턴스에  다른 쓰레드가 접근 시에 인스턴스가 생성되어 있지 않는 것으로 보고 중복으로 새로운 인스턴스를 생성할 수 있기 때문이다.(동일하지 않은 인스턴스)

 

늦은 초기화, 동기화 처리(Lazy Initialization with synchronized)

Lazy Initialization의 멀티 스레드 환경에서의 문제는 Synchronized 키워드를 사용하여 동기화 처리를 통해 해결할 수 있습니다.

다만, getInstance를 호출 시에 해당 인스턴스가 생성 여부를 따지지 않고 동기화 처리과정을 거쳐야 한다는 점이다.

 

기본적으로 동기화라는 과정이 락(Lock)을 거는 메커니즘을 사용하기 때문에 속도 측면에서의 효율이 많이 떨어지는 것이다.

public class Singleton {
    private static Singleton instance;
    private Singleton() { }

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

 

늦은 초기화, DCL(Lazy Initialization. Double Checked Locking)

Synchronized의 비용이 부담될 때 사용하는 방법으로 먼저 인스턴스의 생성 여부를 확인하는 방법이 있다.

이 경우에는 인스턴스만 생성되지 않았을 때에만 동기화 처리를 하기 때문에 효율적으로 동기화 블록을 만들 수 있습니다.

public class Singleton {
    private volatile static Singleton instance;
    private Singleton() { }

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

이 경우에는 volatile 키워드를 사용해야만 DCL기법이 제대로 사용될 수 있습니다.

여기서 volatile 키워드가 필요한 이유는 따로 정리하도록 하겠다. 다만, volatile 키워드를 사용함으로 해당 변수는 Main Memory에 값을 읽어오기 때문에 변수의 값 불일치 문제가 생기지 않게 됩니다.(CPU cache 사용 X)

 

늦은 초기화, Static Inner class사용

클래스 안에 클래스(Holder)를 두어 JVM의 Class loader 메커니즘과 Class가 로드되는 시점을 이용한 방법이다.

public class Singleton {
    private Singleton() { }

    private static class SingletonHolder {
        public static final Singleton INSTANCE = new Singleton();
    }
    
    public static Singlton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

여기서 getInstance가 호출될 때 SingletonHolder 클래스가 호출이 되고 그때 해당 인스턴스가 만들어지기 때문에 기존 방법과 같은 복잡한 개념을 몰라도 사용할 수가 있습니다. 

 

다만, 이경우에도 해당 싱글톤 패턴을 깨트릴 수 있는데 다음과 같은 방법이 있습니다.


  1. 리플렉션의 사용
  2. 직렬화 그리고 역직렬화의 사용

리플렉션의 사용을 먼저 살펴보면 다음과 같습니다.p

public class Application {
    public static void main(String[] args) throws NoSuchMethodExceotion, 
                                                  InvocationTargetException,
                                                  InstantiationExcetpion {
        Singleton singleton = Singleton.getInstance();
    
        Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton singleton2 = constructor.newInstance();
    
        System.out.println(singleton == singletons2);    //false
    }
}

의도한 바와 다르게 인스턴스를 생성하게 되면 새로운 인스턴스가 생성될 수 있습니다.

 

또 다른 방법으로는 직렬화와 역직렬화를 사용하는 방법이 있습니다.

public class Application {
    public static void main(String[] args) throws IOException {
        Singleton singleton = Singleton.getInstance();
        Singleton singleton2 = null;
        
        try (ObjectOutput out = new ObjectOutputStream(new FileOutputStream("singleton.obj"))) {
            out.writeObject(singleton);
        }
        
        try (ObjectInput in = new ObjectInputStream(new FileInputStream("singleton.obj"))) {
            singleton2 = (singleton) in.readObject();
        }
        
        System.out.println(singleton == singleton2);    //false
    }
}

역직렬화를 하는 과정에서 새로 생성자가 실행되기 때문에 다른 결과가 나올 수 있습니다.

다만 역직렬화의 경우에는 readResolve를 생성해주면 해당 케이스에 대하여 대응할 수 있습니다.

public class Singleton implements Serializable {
    private Singleton() { }

    private static class SingletonHolder {
        public static final Singleton INSTANCE = new Singleton();
    }
    
    public static Singlton getInstance() {
        return SingletonHolder.INSTANCE;
    }
    
    protected Object readResolve() {
        return getInstance();
    }
}

하지만 리플렉션의 경우 딱히 대응이 힘들기 때문에 새로운 싱글톤 패턴의 구현 방법이 필요하다.

 

늦은 초기화, Enum 사용

Enum 인스턴스의 생성은 기본적으로 Thread-safe 하기 때문에 스레드 관련 코드를 사용하지 않아도 되기 때문에 간편해진다.

public enum Singleton {
    INSTANCE;
}

Enum을 사용하는 방식의 장점은 위에서 언급한 리플랙션, 직렬화와 역직렬화의 상황을 방지할 수 있다는 것입니다. 왜냐하면, 바이트코드상에 리플렉션이 방지되어 있을 뿐 아니라 이미 Serializable이 구현되어 있기 때문이다.

public class App {
	public static void main(String[] args] throws NoSuchMethodException,
    											InvocationTargetException,
                                                InstantiationException) {
   		
        Settings settings = Settings.INSTANCE;
        Settings settings1 =- null;
        
        Constructor<?>[] declaredConstructors = Settings.class.getDeclaredConstructors();
        
        for (Constructor<?> constructor : declaredConstructors) {
        	settins1 = (Settings) constructor.newInstance("INSTANCE");
        }
        
        Systeom.out.println(settings == settings1);
    }
}

 

 

다만, 이 경우에는 상속을 사용할 수 없습니다. 또한, Context의존성이 있는 환경에서는 싱글턴의 초기화 과정에 Context라는 의존성이 끼어들 가능성이 있는 단점이 있습니다.

 

싱글톤은 어떻게 사용될까?

그렇다면 실무에서의 싱글톤 패턴은 어떻게 사용될까? 

우선 다른 디자인 패턴 구현체의 일부로 사용될 수 있으며, 다음과 같은 상황에서 사용된다.

 

java.lang.Runtime

Runtime이라는 자바가 제공하고 있는 라이브러리를 사용하는 경우

Runtime runtime = Runtime.getRuntime();

new 생성자를 통해 생성할 수 없습니다.

 

스프링에서의 싱글톤 스코프

특정 정의된 빈을 가지고 ApplicationContext를 만들면 항상 같은 type의 빈이 나오게 된다.

ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Singleton.class);

이경우 싱글톤 스코프라고 말하는데 엄밀히 말해서는 싱글톤 패턴과는 다르다고 한다. ApplicationContext내부에서 유일한 인스턴스로서 관리가 되는 것일 뿐이기 때문이다.


 

코딩으로 학습하는 GoF의 디자인 패턴 - 인프런 | 강의

디자인 패턴을 알고 있다면 스프링 뿐 아니라 여러 다양한 기술 및 프로그래밍 언어도 보다 쉽게 학습할 수 있습니다. 또한, 보다 유연하고 재사용성이 뛰어난 객체 지향 소프트웨어를 개발할

www.inflearn.com

 

싱글턴 패턴(Singleton Pattern)

자바와 스프링의 싱글턴 패턴(Singleton Pattern)과 차이점

webdevtechblog.com

 

[Design Pattern] 싱글톤 패턴(Singleton Pattern)에 대하여

싱글톤 패턴(Singleton Pattern)이란? 싱글톤 패턴은 객체를 딱 하나만 생성하여 생성된 객체를 프로그램 어디에서나 접근하여 사용할 수 있도록 하는 패턴을 말합니다. 개발을 하다 보면 전역적으로

coding-factory.tistory.com

 

싱글톤 패턴(Singleton pattern)을 쓰는 이유와 문제점

싱글톤 패턴(Singleton Pattern) 싱글톤 패턴 애플리케이션이 시작될 때 어떤 클래스가 최초 한번만 메모리를 할당하고(Static) 그 메모리에 인스턴스를 만들어 사용하는 디자인패턴. 생성자가 여러

jeong-pro.tistory.com