Singleton Pattern(싱글톤 패턴)

들어가기

이번 포스팅에서는 디자인 패턴에서 유명한 싱글톤 패턴(Singleton Pattern) 에 대해서 알아보겠습니다.

본론

싱글톤 패턴(Singleton Pattern) 은 특정 클래스에 대해 객체 인스턴스가 하나만 만들어질 수 있도록 해주는 패턴입니다.

간단하게 말하면, 싱글톤 패턴(Singleton Pattern) 은 어떤 상황에서든 해당 객체의 인스턴스는 하나만 존재해야하고 이를 사용하기 위해서는 새롭게 인스턴스를 생성하는 것이 아닌 이미 생성된 인스턴스를 사용해야합니다.

그렇다면 이런 싱글톤 패턴(Singleton Pattern) 의 객체를 어떻게 만들수 있을까요?

1. 고전적인 방법

public class Singleton {
    private static Singleton s;

    private Singleton() {}

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

위에 보인 예시는 아주 고전적인 방법의 예시입니다. 객체를 생성할 수 있는 생성자를 private로 캡슐화했습니다.

즉, 싱글톤 (Singleton Pattern) 객체를 만들기 위해서는 직접 생성자를 호출하는 방식이 아닌 class 내부의 getInstance 메서드를 활용해야만 합니다.

그리고 getInstance 메서드는 이미 인스턴스가 존재하는지 확인한 후 존재한다면 기존의 인스턴스를 반환하고 아니면 인스턴스를 생성한뒤 반환합니다.

인스턴스를 프로그램 시작과 동시에 생성하는 것이 아니라 상황에 따라 필요할때 getInstance 메서드를 호출하고 생성해 사용하고 있으며 이와 같은 방식을 Lazy Loading(지연 로딩) 이라고 부릅니다.

하지만 고전적인 방법 multi-threaded(다중 스레드) 환경에서 문제를 야기합니다.

다음과 같은 상황을 한번 생각해봅시다.

  1. Singleton 인스턴스가 아직 생성되지 않은 초기 상황에서 스레드 1이 getInstance를 호출해 if문을 실행
  2. 만약 스레드 1이 생성자를 호출해 인스턴스를 만들기 전에 거의 비슷한 시점에 스레드 2도 if문을 실행
  3. 인스턴스가 2개가 생성

2. synchrozied 사용

multi-threaded(다중 스레드)환경에서 싱글톤 패턴(Singleton Pattern)를 활용하기 위해서는 앞서 예시에서 봤던 것처럼 동기화가 필수입니다. 그리고 자바를 공부해보신 여러분이라면 synchronized라는 동기화 처리 방법을 알고 계실겁니다.

public class Singleton {
    private static Singleton s;

    private Singleton() {}

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

synchronized를 사용한다면 getInstance 메서드는 스레드간에 동기화가 처리되어 동시에 접근할 수 없게 됩니다. 즉, 두 스레드가 동시에 getInstance에 접근하여 인스턴스를 동시 생성할 수 없게 됩니다.

이렇게해서 프로그램 상에서 하나의 유일 인스턴스를 생성했습니다. 문제없이 잘 생성됩니다. 하지만 synchronized를 사용하면서 또 다른 문제가 발생합니다.

바로 여러분들고 알고 계시는 속도 문제입니다. synchronized를 사용하면서 getInstance를 실행할때 마다 속도가 급격하게 감소합니다.

3. synchrozied 사용 - 정적 초기화

synchronized의 사용으로 급격하게 떨어진 속도를 조금이나마 올리기 위해 인스턴스 자체를 getInstance로 생성하는 것이 아닌 클래스가 로딩될때 jvm에서 인스턴스로 생성하는 방식입니다.

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

    private Singleton() {}

    public static synchronized Singleton getInstance() {
        return s;
    }
}

보이는 것과 같이 getIntance에서는 인스턴스를 생성하지 않습니다. 하지만 이 방법도 문제점을 가지고 있습니다.

만약 getInstance 자체가 또 다른 자원을 많이 필요로 하면 속도가 느려지는 것을 막을수 없습니다. 다른 문제점은 인스턴스 자체가 필요할때 메모리에 올려 사용하지 않고 프로그램 시작서부터 메모리를 사용한다는 문제가 있습니다.

4. synchrozied 사용 - DCL(Double-Checking Locking)

DCL(Double-Checking Locking) 을 사용하면 Lazy Loading을 할 수 있는 동시에 synchronized 역시 효율적으로 사용할 수 있습니다.

public class Singleton {
    private volatile static Singleton s;

    private Singleton() {}

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

인스턴스가 생성되지 않았다면 생성될 때 최초 한 번만 동기화를 진행한 뒤 인스턴스를 생성합니다.

다만 DCL은 Multi-processor가 shared-memory를 사용하면서 문제가 발생하기 쉽다고 합니다. 따라서 안정성이 불안하다면 이 방법은 사용하지 않는 것이 좋습니다. 또, 멀티코어 환경에서 하나의 CPU를 제외하고는 다른 CPU가 lock이 걸리게 됩니다.

5. Demand Holder Idiom

public class Singleton {
    private Singleton() {}

    private static class LazyHolder {
        static final Singleton s = new Singleton();
    }

    public static Singleton getInstance() {
        return LazyHolder.s;
    }
}

이 방법은 중첩 클래스를 이용한 Holder를 사용하는 방법입니다. getInstance 메서드가 호출되기 전까지는 인스턴스가 생성되지 않습니다.

Lazy Loading을 활용해 메모리 측면에서도 유리하고 synchronized를 사용하지 않아 성능면에서도 우수합니다.

Demand Holder Idiom 방식은 현재 가장 널리 쓰이는 방법 중 하나 입니다.

싱글톤 패턴과 정적 클래스

실제로 굳이 싱글톤 패턴(Singleton Pattern)을 사용하지 않고 정적 메서드로만 이루어진 정적 클래스를 사용해도 비슷한 효과를 만들수 있습니다.

하지만 싱글톤 패턴(Singleton Pattern)을 이용하는 방법과 가장 차이가 나는 점은 객체를 생성하지 않고 메서드만 이용한다는 점입니다.

마치며

오늘은싱글톤 패턴(Singleton Pattern)에 대해 알아봤습니다.

싱글톤 패턴(Singleton Pattern)의 개념과 관련해 추가적인 질문이나 오류, 오타가 있을시 댓글로 남겨주세요.

출처

JAVA 객체지향 디자인패턴

Share