슬기로운 개발생활

[Design Pattern] 싱글톤 패턴(Singleton Pattern) 알아보기

by coco3o
반응형

위키백과

싱글톤 패턴이란

싱글톤 패턴은 인스턴스를 불필요하게 생성하지 않고 오직 한 개의 인스턴스만 생성하여 사용되는 디자인패턴이다.

즉, 생성자의 호출이 반복적으로 이루어져도 실제로 생성되는 객체는 최초 생성된 객체를 반환 해주는 것이다.

 

싱글톤 패턴을 사용하는 이유

위에서도 말했지만, 인스턴스를 오직 한 개만 생성하여 사용한다면 어떤 장점이 있을까?

가장 먼저 떠올릴 수 있는 장점은 메모리 낭비 방지일 것이다. 

그리고 이미 생성된 인스턴스를 활용함으로써 속도 측면에서도 장점이 있다고 볼 수 있다.

 

또 다른 장점은 다른 클래스 간에 데이터 공유가 쉽다는 것이다.

싱글톤으로 생성된 객체는 전역성을 띄기 때문에 다른 객체와 공유가 용이하다.

하지만 만약 여러 클래스의 인스턴스에서 싱글톤 인스턴스의 데이터에 동시에 접근하게 된다면 동시성 문제가 생길 수 있다.

 

인스턴스가 한 개만 존재하는 것을 보증하고 싶은 경우 싱글톤 패턴을 사용한다.

 

싱글톤 패턴의 문제점

싱글톤 패턴을 사용하면 위와 같은 장점들이 있지만 문제점도 존재한다.

 

먼저 싱글톤 패턴을 구현하는 코드가 많이 필요하다. 정적 메소드에서 객체 생성을 체크하고 생성자를 호출하는 경우에 멀티스레딩 환경에서 발생할 수 있는 동시성 문제 해결을 위해 syncronized 키워드를 사용해야 한다. 자세한건 아래에서 코드로 설명하겠다.

 

두 번째는 테스트하기 어렵다는 것이다. 싱글톤 인스턴스는 자원을 공유하기 때문에 테스트시 매번 인스턴스의 상태를 초기화 시켜주어야 한다.

 

그리고 세번째로는 싱글톤으로 만든 인스턴스의 역할이 복잡한 경우 해당 싱글톤 객체를 사용하는 다른 객체간의 결합도가  높아져 객체 지향 설계 원칙에 어긋나게 된다. (개방-폐쇄 원칙)

 

이외에도 자식클래스를 만들 수 없다는 점과, 내부 상태를 변경하기 어렵다는 점 등 여러가지 문제점들이 존재한다.


다양한 싱글톤 패턴 구현 방식


Lazy initialization(늦은 초기화) 싱글톤 패턴

public class Singleton {

    private static Singleton instance;

    private Singleton() {}

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

static한 자기자신의 클래스를 필드로 만들고, 인스턴스가 필요하여 요청할 때 생성되는 형태로 작성되었다.

 

private 생성자와 static 메소드를 사용한 가장 보편적인 방식이다.

 

하지만 위의 코드는 멀티 쓰레드 환경에서 취약하다는 문제점이 있다.

 

예를 들어, 쓰레드A와 쓰레드B가 있다고 하자.

쓰레드A가 getInstance() 메소드의 if문을 지날 때 instance가 null이라면 쓰레드A은 인스턴스를 생성하려고 할 것이다. 이 때

쓰레드B로 제어권이 넘어간다면 쓰레드B 역시 if문이 수행되고 쓰레드B 또한 인스턴스를 생성할 것이다.

이렇게 되면 결국 인스턴스가 두 번 생성되는 문제가 발생한다.

 

synchronized를 추가해 멀티 쓰레드 환경에서 안전하게 구현

public class Singleton {

    private static Singleton instance;

    private Singleton() {}

    public static synchronized Singleton getInstance() {

        if (instance == null) {
            instance = new Singleton();
        }

        return instance;
    }
}

Lazy initialization의 쓰레드 동기화 문제의 가장 쉬운 해결방법은 synchronized(동기화) 키워드이다.

 

※ 쓰레드가 synchronized 되어있는 곳에서 작업하고 있다면 다른 쓰레드가 접근하지 못하도록 lock 걸어줌

 

getInstance() 메소드를 synchronized로 처리하면 멀티 쓰레드의 동시 접근에 대한 문제는 해결하게 된다.

 

하지만, 이 방법은 매번  인스턴스를 리턴 받을 때마다 쓰레드를 동기화하기 때문에 성능 저하가 생긴다는 단점이 있다.

(실제로 인스턴스가 2개 이상 생성될 확률은 매우 적다. 이 때문에 synchronized를 추가했지만,

인스턴스 초기화가 완료된 시점 이후라면 synchronized는 불필요하게 사용되고, 별다른 역할을 하지 못한다.)


Eager initialization(이른 초기화) 싱글톤 패턴

public class Singleton {

    private static Singleton instance = new Singleton();

    private Singleton() {}

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

이 방법은 위에서 언급했던 멀티 쓰레드 환경에서 유발되는 모든 문제를 해결한다.

 

쓰레드가 getInstance()를 호출하는 시점이 아닌, Class가 로딩되는 시점 즉, static 영역의 데이터 로딩시점에 

인스턴스를 생성하기 때문에 하나의 인스턴스만 생성되는 것을 보장해준다.

 

미리 만들어 놓은 인스턴스를 리턴하기만 하면 되니까 이 방법은 Thread-safe하며 소스도 간결하고 성능 역시 좋다.

 

하지만 이 방법은 인스턴스를 미리 만들어놓기 때문에 인스턴스를 사용하지 않는다면 메모리 낭비에 불과하다는 단점이 있다.


DCL(Double Checked Locking) 싱글톤 패턴

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;
    }
}

DCL 싱글톤 패턴은 static 필드에 volatile 키워드를 써야 자바 1.5 이상부터 동작하는 DCL 기법이 완성된다.

volatile란?
- Java 변수를 Main Memory에 저장하겠다라는 것을 명시
- 멀티 쓰레드 환경에서 하나의 쓰레드만 read & write 하고 나머지 쓰레드가 read 만 하여 가장 최신의 값을 보장
- 매번 변수의 값을 읽을 때마다 CPU Cache에 저장된 값이 아닌 Main Memory에서 읽는다.
- 또한 변수의 값을 작성할 때마다 Main Memory에 작성한다.
volatile 키워드를 써야하는 이유?
volatile 키워드를 사용하지 않는 멀티 쓰레드는 작업을 수행하는 동안 성능 향상을 위해 각각 메모리에서 읽은 변수 값을 CPU Cache에 저장하게 된다. 그리고 쓰레드가 변수 값을 읽어온다면 각각의 CPU Cache에 저장된 값이 다르기 때문에 변수 값 불일치 문제가 발생하게 된다.
그래서 volatile 키워드를 추가해 공통된 Main Memory에 저장하고 읽게끔 하여 변수 값 불일치 문제를 해결한다.

getInstance() 메소드 레벨에 synchronized가 있지 않고 메소드 내부에 존재한다.

 

메소드 레벨에 synchronized가 있지 않기 때문에 호출할 때마다 synchronized 걸리지 않고,

인스턴스가 이미 존재한다면 synchronized를 쓰지 않기 때문에 성능 이슈를 피할 수 있다.

인스턴스를 필요로 하는 시점에 만들 수 있다는 장점도 있다.

 

이 방법은 상당히 안정적이고 문제가 없는 방법으로 인정되고 있다.

 

※ DCL 싱글톤 패턴을 사용한다면 반드시 volatile 키워드를 같이 사용하자.


Lazy Holder 싱글톤 패턴

public class Singleton {

    private Singleton() {}

    //inner class
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

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

현 시점에서 가장 완벽하다고 평가받고있는 방법이다. Java 버전과도 무관하고 성능도 뛰어나다.

 

Lazy initialization 방식을 가져가면서 쓰레드간 동기화문제를 동시에 해결한다.

 

중첩클래스 SingletonHolder는 getInstance() 메소드가 호출되기 전에는 참조 되지 않으며,

최초로 getInstance() 메소드가 호출될 때 클래스로더에 의해 Singleton 인스턴스를 생성하여 리턴한다.

SingletonHolder 내부 인스턴스는 static 이기 때문에 클래스 로딩 시점에 한 번만 호출된다는 점을 이용한 것이며,

final을 사용해 다시 값이 할당되지 않도록 한다.

 

volatile이나 synchronized 같은 키워드가 없어도 Thread-safe 하면서 성능도 보장하는 아주 훌륭한 방법이다.


참고 자료: 

백기선님 [GoF 디자인 패턴] 中 싱글톤 패턴

https://javaplant.tistory.com/21

https://tecoble.techcourse.co.kr/post/2020-11-07-singleton/

 

반응형

블로그의 정보

슬기로운 개발생활

coco3o

활동하기