5. Singleton Pattern

2021. 9. 15. 13:44Computer Sciences/Design Patterns

싱글톤 패턴

해당 클래스의 인스턴스를 단 하나만 만들고 해당 인스턴스를 필요로 하는 객체들이 공유하여 사용하도록 하는 패턴이다.

public class SingletonObj {

    // static 멤버로 해당 클래스를 만들어놓았다.
    private static SingletonObj singletonObj;

    // other fields...

    // 생성자를 private으로 만들어 인스턴스를 생성하지 못하게 막았다.
    private SingletonObj() {}

    // 이 메서드를 통해 이 클래스의 인스턴스에 접근하도록 한다.
    public static SingletonObj getInstance() {
        if (singletonObj == null) {
            singletonObj = new SingletonObj();
        }

        return singletonObj;
    }

    // other methods...
}

하지만 위와 같이 구현한 싱글톤은 문제가 있다.

멀티 쓰레드 환경에서는 쓰레드의 컨텍스트 스위칭이 발생한다. 이때 singletonObjnull을 판단하는 조건문이 위와 같은 상황에 놓인다면 하나만 생성되어야 하는 싱글톤 패턴이 무너지게 된다. 따라서 다음과 같이 수정할 수 있다.

public class SingletonObj {

    // static 멤버로 해당 클래스를 만들어놓았다.
    private static SingletonObj singletonObj;

    // other fields...

    // 생성자를 private으로 만들어 인스턴스를 생성하지 못하게 막았다.
    private SingletonObj() {}

    // 이 메서드를 통해 이 클래스의 인스턴스에 접근하도록 한다.
    public static synchronized SingletonObj getInstance() {
        if (singletonObj == null) {
            singletonObj = new SingletonObj();
        }

        return singletonObj;
    }

    // other methods...
}

synchronized 키워드를 사용해서 동기화 처리를 해주었다. 이렇게 사용하면 해당 메서드를 사용할 수 있는 쓰레드는 단 한개이기 때문에 위와 같은 오류가 발생하지 않는다. 하지만 동기화 처리를 하기 때문에 성능이 많이 느려진다.
그리고 아래와 같이 해결할 수도 있다.

public class SingletonObj {

    // 애초에 클래스가 정의될 때 인스턴스를 만들어놓는다.
    private static SingletonObj singletonObj = new SingletonObj();

    // other fields...

    // 생성자를 private으로 만들어 인스턴스를 생성하지 못하게 막았다.
    private SingletonObj() {}

    // 이 메서드를 통해 이 클래스의 인스턴스에 접근하도록 한다.
    public static SingletonObj getInstance() {
        return singletonObj;
    }

    // other methods...
}

아예 클래스가 정의될 때 인스턴스를 만들어 놓는 방법이다. 간단하고 편리하다.
그리고 DCL(Double-Checking Locking)을 통해 해결할 수도 있다. 이는 인스턴스가 생성되어 있는지 확인한 후 생성되어 있지 않은 경우에만 동기화를 하는 것이다.

public class SingletonObj {

    // static 멤버로 해당 클래스를 만들어놓았다.
    private volatile static SingletonObj singletonObj;

    // other fields...

    // 생성자를 private으로 만들어 인스턴스를 생성하지 못하게 막았다.
    private SingletonObj() {}

    // 이 메서드를 통해 이 클래스의 인스턴스에 접근하도록 한다.
    public static SingletonObj getInstance() {
        if (singletonObj == null) {
            synchronized(SingletonObj.class) {
                if (singletonObj == null) {
                    singletonObj = new SingletonObj();
                }
            }
        }

        return singletonObj;
    }

    // other methods...
}
volatile은 자바에서 멀티 쓰레드 환경에서 발생하는 문제를 해결해주는 키워드이다. 컴퓨터는 내부에서 캐시를 사용한다. CPU는 먼저 캐시 테이블을 탐색하서 캐시 히트라면 캐시 테이블의 값을 가져오고 캐시 미스라면 메모리에서 값을 가져오고 캐시 테이블에 반영한다. 이때 문제가 발생한다. 만약 런타임에 값이 바뀐 것이 있다. 그런데 캐시에는 변경되지 않은 이전 값이 들어있다면 CPU는 변경되지 않은 캐시의 값을 사용한다. 이렇게 되면 쓰레드별로 사용하는 값이 어떻게 다른지 알 방법이 없다. 이러한 문제를 해결하기 위해 volatile을 사용한다. 이 키워드를 사용하면 해당 멤버는 메모리의 값만을 사용한다고 명시하는 것이다. 성능은 떨어질 수 있으나 멀티쓰레드 환경의 동기화 문제를 해결한다.

그리고 단지 인스턴스 뿐만 아니라 싱글톤에서는 공유 변수 사용을 피해야 한다. 이 또한 멀티 쓰레드 환경에서 문제가 발생한다. 여러 쓰레드가 공유 자원을 사용하면 이에 대한 동기화 처리를 반드시 해주어야 한다. 아니면 사용하는 것을 지양해야 한다.

필요성

이런 패턴이 필요한 이유는 무엇일까? 웹 서버가 싱글톤 패턴을 이용하는 대표적인 예시이다.

Java 기반의 웹 서버는 대부분 멀티쓰레드로 동작한다. 그래서 사용자의 요청마다 쓰레드를 생성하여 HTTP 요청, 응답을 처리한다. 이때 여러 사용자가 동일한 리소스에 접근한다고 해보자. 그러면 해당 리소스에 대한 인스턴스를 요청마다 새로 생성할 것이다. 이는 서버 입장에서 매우 큰 부담이 된다. 이용자가 한 두명일때는 감당이 되겠지만 10만, 100만 명 이상이 될 경우 감당하기 힘들게 된다. 이 때문에 공통으로 사용되는 자원들은 싱글톤 패턴을 적용하여 하나의 인스턴스만 만들어놓고 사용자들의 동일한 자원 요청에 대해서는 이 인스턴스를 사용하는 것이다. 스프링에서는 대표적으로 컨트롤러, 서비스, 리포지토리와 같은 클래스가 싱글톤 패턴이 적용된다.

'Computer Sciences > Design Patterns' 카테고리의 다른 글

7. Adapter Pattern  (0) 2021.10.14
6. Command Pattern  (0) 2021.10.14
4. Abstract Factory Method  (0) 2021.09.15
3. Factory & Factory Method Pattern  (0) 2021.09.15
2. Decorator Pattern  (0) 2021.09.06