소프트웨어 관련/Design Pattern

[Design Pattern] Singleton Pattern

JJangGu 2022. 5. 11. 00:55

이번에는 굉장히 많이 듣게 되는 패턴인, 싱글턴 패턴입니다. 너무 많이 들었던 패턴이고, Spring 을 공부하다 보면 자연스레 듣게 되는 패턴입니다. 

 

Singleton Pattern

싱글턴 패턴은 해당 클래스의 인스턴스가 하나만 만들어지고, 어디서든지 그 인스턴스에 접근할 수 있도록 하기 위한 패턴

 

싱글턴 패턴 자체의 정의는 어렵지 않습니다. 다만 구현방법과 왜 필요한지에 대해서 고민을 해봐야 한다고 생각합니다. Spring 을 공부할때도 빈의 기본 스코프가 싱글턴이라는 것은 배우지만 정작 싱글턴이 무엇인지는 관심을 가지고 찾아보지 않으면 그냥 인스턴스가 하나만 만들어지는 구나 하고 넘어갈 수 있습니다. 

 

먼저, 인스턴스를 하나만 만든다는 정의를 기준으로 고전적인 구현법을 보겠습니다. 

 

public class Singleton {
    private static Singleton uniqueInstance;
 
    // 기타 인스턴스 변수
 
    private Singleton() {}
 
    public static Singleton getInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
 
    // 기타 메소드
}

 

위의 코드를 보면, 유일한 인스턴스를 저장할 정적변수가 있습니다. 그리고 생성자를 private 으로 선언해 외부에서 생성하는 것을 막았습니다. 아직 인스턴스가 만들어지지 않았다면 생성자를 통해서 새로운 인스턴스를 만들어 반환합니다. 이렇게 해석하면 문제가 없어보입니다.

 

하지만 위의 코드에는 문제가 있습니다. 간단하게 두개의 스레드가 있습니다. 첫번째 스레드와 두번째 스레드가 동시에 싱글턴 객체를 얻으려고 한다고 가정을 해보겠습니다. 인스턴스가 없는 상황에서 첫번째 스레드가 먼저 getInstance()를 호출했습니다. 그리고 uniqueInstance가 null 이라서 새로운 인스턴스를 생성하려고 하죠. 그런데 그 시점에 두번째 스레드도 getInstance()를 호출합니다. 이 시점에도 아직 인스턴스가 만들어지지 않아서 uniqueInstance 가 null 이면 두개의 스레드 모두에서 인스턴스를 만들게 됩니다. 그러면 반환되는 객체가 달라지면서 싱글턴의 정의가 깨지게 되죠.

 

그러면 위와 같은 문제를 해결해보겠습니다. 

 

public class Singleton {
    private static Singleton uniqueInstance;
 
    // 기타 인스턴스 변수
 
    private Singleton() {}
 
    public static synchronized Singleton getInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
 
    // 기타 메소드
}

 

이제 아까와 같은 문제는 사라졌습니다. synchronized 를 통해서 한 스레드가 사용을 끝내기 전에 다른 스레드를 기다리게 합니다. 이 방식을 Lazy initialization 이라고 합니다. 하지만 여기서도 문제는 있습니다. 저 uniqueInstance 에 인스턴스를 대입하고 나면 굳이 동기화를 사용할 필요가 없습니다. 이미 만들어졌으므로 그 인스턴스를 리턴해주면 되니까요. 이런 클래스가 많아지면 오히려 성능에 문제가 생길 것 입니다. 

 

그러면 또 이런 문제를 해결해보겠습니다. 

 

public class Singleton {
    private static Singleton uniqueInstance = new Singleton();

    private Singleton() { }

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

 

 

인스턴스를 정적 초기화부분에서 생성합니다. 이렇게 되면 클래스를 로딩하는 시점에서 유일한 인스턴스를 생성해낼 수 있습니다. 이런 방법을 Eager Initialization 이라고 합니다. 

 

또 다른 방법도 보겠습니다. 이번에는 DCL(Double - Checking Locking) 을 쓰는 방법입니다. 

 

public class Singleton {
    private volatile static Singleton uniqueInstance;

    private Singleton() { }

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

 

 

DCL 을 사용하면, 일단 인스턴스가 생성되어 있는지 확인한 다음, 생성되어 있지 않았을 때만 동기화를 합니다. 처음에만 동기화를 하고 이후에는 동기화 하지 않는 것이죠. 

 

* volatile 

값을 읽어올 때 캐시가 아닌 메모리에서 읽어온다. 캐시와 메모리간의 값의 불일치를 해결할 수 있다. 멀티 스레드 환경에서 하나의 스레드가 read & write 하고, 다른 스레드가 read 할 때 가장 최신의 값을 보장한다.

 

마지막으로 holder 에 의한 초기화 방법입니다. 

 

public class Singleton {

    private Singleton() { }

    private static class SingletonHolder {
        private static final Singleton instance = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }
}

 

이 방법은 클래스 안에 클래스(holder) 를 두어서 JVM의 클래스 로더 매커니즘과 클래스가 로드되는 시점을 이용한 방법입니다. Lazy initialization 방식을 가져가면서 동기화 문제를 해결할 수 있습니다. holder 클래스는 getInstance() 가 호출되기 전까지는 참조되지 않고, 최초로 호출됐을때 싱글턴 객체를 생성하여 리턴합니다. static 과 final 을 이용한 것 이죠. 

 

이렇게 싱글턴 패턴에 대한 소개를 마치겠습니다 👍