디자인 패턴 4번째 시간입니다~ 이번에 알아볼 패턴은 Singleton Pattern입니다. 저에겐 이 패턴을 대학 강의전에 유니티 게임 엔진을 자습하면서 먼저 접했었습니다. 다른 디자인 패턴들과 비교해서 사용처가 확실하고 패턴 자체가 직관적이어서 여러분들이 보시기에도 굉장히 이해가 쉬운 패턴이라고 생각됩니다. 그럼, 이제부터 열심히 서술해보도록 하겠습니다!
클래스가 한 개의 인스턴스만을 만들 수 있도록 하고, 어디서나 생성된 인스턴스에 접근할 수 있도록 만듦
# 고전적 싱글톤 패턴 구현법
public class Singleton
{
private static Singleton _instance = null;
private Singleton() {};
public static Singleton getInstacne()
{
if (_instance == null)
{
_instance = new Singleton();
}
return _instance;
}
}
public class Main
{
public static void main(String[] args)
{
Singleton singleton1 = Singleton.getInstacne();
Singleton singleton2 = Singleton.getInstacne();
System.out.println(System.identityHashCode(singleton1));
System.out.println(System.identityHashCode(singleton2));
}
}
실행결과

위 코드를 보시면, 인스턴스 생성자는 private으로 다른 객체에서 생성할 수 없게 만듭니다. 그리고 정적 멤버 변수에 자기 자신을 저장해두고, 다른 클래스에서 이 싱글톤 객체에 접근하려고 할때 getter 함수(위에서 getInstance()를 얘기한다.)를 통해 메모리 값을 가져갈 수 있게 만듭니다. getter내에선 해당 인스턴스가 생성이 되어 있는지 체크하고 나서 return 합니다.
싱글톤 패턴으로 구현되는 클래스들은 다른 여러 객체들이 해당 클래스를 동시에 참조해야 할때 사용된다. 만약 싱글톤 패턴을 구현하지 않고 객체들이 해당 클래스에 접근하려할때 마다 인스턴스를 생성해 전달하기 시작하면, 불필요하게 자원들(메모리, 인스턴스를 생성하거나 제거할때 발생하는 오버헤드 등)이 낭비가 발생하는데 싱글톤 패턴으로 이를 방지할 수 있습니다. 또한, static 키워드와 함께 인스턴스를 생성하면 다른 객채에서 getter함수를 통해 접근하는 과정도 용이해진다는 이점들을 가져갈 수 있습니다.
다만, 싱글톤 패턴을 사용할때 주의할 점이 있습니다. 전체 프로세스 중 단 한개의 인스턴스만 생성된 상태이므로, 여러 객체들이 동시에 해당 인스턴스를 참조하면서 생기는 동기화 문제를 고려해야만 합니다. 이런 현상이 어떻게 발생하는지 java.lang 패키지 안에 Runnable 인터페이스를 응용한 멀티 쓰레드 시스템을 만들어 보여드리겠습니다.
public class ThreadExample implements Runnable
{
private int num;
private int count;
public ThreadExample(int num, int count)
{
this.num = num;
this.count = count;
}
@Override
public void run()
{
for (int i = 0; i < count; i++)
{
System.out.printf("Thread num = %d\n", num);
System.out.println(Singleton.getInstance());
}
}
}
import java.lang.*;
public class Main {
public static void main(String[] args) {
Thread thread1 = new Thread(new ThreadExample(1, 5));
Thread thread2 = new Thread(new ThreadExample(2, 5));
Thread thread3 = new Thread(new ThreadExample(3, 5));
thread1.start();
thread2.start();
thread3.start();
}
}
실행결과

실행결과를 보시면 맨 처음에 나온 싱글톤 인스턴스의 주소값(...@589e025)과 이후에 출력되는 주소값(...@4bf41f61)이 다른 것을 확인할 수가 있습니다. 이 문제는 이 줄에서 발생합니다.
public static Singleton getInstacne()
{
if (_instance == null)
{
_instance = new Singleton();
}
return _instance;
}
저는 쓰레드를 총 3개 생성해서 오버라이드된 run() 안에 각각 싱글톤 인스턴스를 점유하도록 만들었습니다. thread들이 start 되면 위의 getter 함수를 실행할텐데, 동시에 if (_instance == null) 문을 통과하는 쓰레드가 2개 이상이면 _instance = new Singleton(); 도 2번 이상 실행되며, 본래 한 개만 존재해야할 인스턴스가 2개 이상 생성되기 때문에 저련 현상이 발생합니다. 물론 실행결과에서 보시면 그 이후로, 인스턴스의 주소값은 일정합니다. 맨 처음에 생성된 싱글톤 인스턴스는 더 이상 참조되지 않으니, 자바의 가비지 컬렉션이 삭제시키겠지만, 본래 싱글톤 패턴의 설계에 위반되기 때문에 이를 반드시 해결해야합니다.
바로 싱글톤 패턴에 대해 thread-safe 를 보장하도록 설계하는 것입니다. 여기서 thread-safe란 표현이 생소하신 분들도 계실텐데, 간단하게 설명하자면 멀티쓰레드 프로그래밍에서 쓰이는 용어로 2개 이상의 쓰레드가 동일한 자원에 접근할 때에도 문제없이 전체 프로세스가 작동할 수 있도록 안정성이 보장될 때 thread safe하다라고 말할 수 있습니다. 자세한건 차후 멀티쓰레드 프로그래밍에 대해 포스팅 글을 작성할 때 서술하겠습니다.
public static synchronized Singleton getInstance()
{
if (_instance == null)
{
_instance = new Singleton();
}
return _instance;
}
java에선 쓰레드들이 사용하는 매서드를 작성할때 synchronized 를 붙여 간단히 해결할 수 있습니다. 저 synchronized 키워드가 붙여진 함수를 다수의 객체에서 동시에 접근하려 할때, 한번에 하나씩 실행시킬 수 있도록 만들어 주는 선언입니다. 이미 선점하여 사용하고 있는 프로세스가 있다면, 다른 프로세스의 접근을 거부합니다. 따라서 synchroized 키워드 하나만으로 간단히 thread-safe를 보장하는 것이 가능합니다. 다만, 호출할때마다 동기화를 시키기 때문에 각각의 쓰레드가 독립적으로 접근할때에는 오버헤드를 발생시키기 때문에 이 순간은 비효율적입니다. 만약 getInstance() 속도가 크게 영향을 미칠 정도가 아니라면, 무시를 해도 괜찮지만 그렇지 않을 경우, 인스턴스를 프로세스가 시작할때 미리 생성해두고 synchronized를 사용하지 않는 방법으로 오버헤드를 줄일 수 있습니다.
DCL(Double Checkin Locking)을 사용해서 getInstance() 함수에서 동기화되는 부분을 줄일 수도 있습니다.
public class Singleton {
private static volatile Singleton _instance;
private Singleton() {};
public static Singleton getInstance()
{
if (_instance == null)
{
synchronized (Singleton.class)
{
if (_instance == null)
{
_instance = new Singleton();
}
}
}
return _instance;
}
}
지금 싱글톤 클래스의 멤버 변수에 volatile 이라고 새로운 키워드가 하는 역할은 변수를 CPU cache에 저장하지 않고 메모리에서 읽고 저장합니다. 또한, 쓰레드를 사용할 때 다른 프로세서에 있는 cache에 변수값이 저장되어 서로 다른 값을 사용하는 것을 방지합니다. 또한, getInstance에서 처음 if (_instance == null) 통과한 쓰레드들끼리만, Singleton 인스턴스를 동기화시킨다음 다시 if (_instance == null) 로 필터링합니다. 이게 DCL의 방법입니다. 참고로 이 방법은 jVM 버전이 1.5이상, java 버전이 5 이상에서만 동작한다고 합니다. 버전에 따라서 thread-safe가 보장되지 않을 수도 있다는 말이니, 이 방법을 사용할땐 주의해주시길 바랍니다.
public class Singleton {
private static class InnerSingleton
{
static final Singleton _instance = new Singleton();
}
private Singleton() {};
public static Singleton getInstance()
{
return InnerSingleton._instance;
}
}
위에서 보여드린 Singleton 클래스에는 멤버 변수가 없어, JVM에서 이 클래스를 메모리에 적재(load)할 때 인스턴스를 생성하지 않습니다. 따라서 getInstance()가 호출될 때에만 정적 내부 클래스를 통해 인스턴스를 생성하고 return 합니다. 따라서, 인스턴스는 필요할 때에만 생성되고, thread-safe를 유지 함으로써, 가장 권장되는 방법이기도 합니다.
마지막으론 Enum으로 싱글톤 클래스를 구현하는 방법입니다. 왜냐하면 java에선 모든 enum은 프로그램에서 한 번만 인스턴스화 되도록 보장하기 때문입니다. 다만, Enum 외의 클래스는 상속 불가능한 문제점이 있습니다.
public enum Singleton {
SINGLETON_OBJECT
}
--> 대충 만든 것처럼 보이지만 진짜로 이게 끝이다.