Ch.5 싱글턴 패턴

하나뿐인 특별한 객체 만들기

About

인스턴스가 하나뿐인 특별한 객체를 만들어 보자.

The Code

Singleton Pattern Implementation in TypeScript

전역 변수론 안되는가?

전역 변수에는 단점이 있다. 전역 변수에 객체를 대입하면 애플리케이션이 시작될 때 객체가 생성된다.

그 객체가 자원을 많이 차지한다고 해보자. 만약 애플리케이션이 끝날 때까지 그 객체를 한 번도 쓰지 않는다면 괜히 자원만 잡아먹는 쓸데없는 객체가 된다.

싱글턴 패턴을 사용하면 필요할 때만 객체를 만들 수 있다.

고전적인 싱글턴 패턴 구현법

이 코드에 한해서는 자바로 해보자.

public class Singleton {
    private static Singleton uniqueInstance;
  
    // 기타 인스턴스 변수
  
    private Singleton() {}
  
    public static Singleton getInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
    
    // 기타 메소드
}
  • private 접근 제어자로 기본 생성자의 접근을 막는다.

  • getInstance() 메소드는 uniqueInstance가 초기화되지 않았을 경우 초기화해서 반환하고, 초기화되었다면 인스턴스를 반환한다. 이러한 방식을 게으른 인스턴스 생성(lazy instantiation)이라고 한다.

  • 이 방식은 사실 문제점이 많다.

특징

  • 인스턴스가 절대 2개 이상이 되지 않는 유일한 객체를 만들 수 있다.

  • 설정 파일을 싱글톤으로 만들어 어떤 객체에서도 같은 자원을 쓸 수 있다.

  • 연결 풀, 스레드 풀과 같은 자원 풀을 관리하는 데도 자주 등장한다.

  • 객체 인스턴스가 여러 개 생겨서 의도하지 않은 버그를 맞닥뜨릴 때 사용하면 유용하다.

  • public으로 공개된 생성자가 없다. getInstance() 정적 메소드를 통해 인스턴스를 달라고 요청할 수 있다.

싱글톤 패턴(Singleton Pattern)의 정의

싱글턴 패턴(Singleton Pattern)은 클래스 인스턴스를 하나만 만들고, 그 인스턴스로의 전역 접근을 제공한다.

Head First Design Patterns - Singleton Pattern

멀티스레딩 환경에서의 싱글톤

싱글톤 패턴은 멀티스레드 환경에서 사용할 때 특히 주의가 필요하다. 인스턴스가 여러 번 초기화될 수 있기 때문이다.

해결 방법은 getInstance()를 동기화시키는 것이다.

public class Singleton {
    private static Singleton uniqueInstance;
  
    // 기타 인스턴스 변수
  
    private Singleton() {}
  
    public static synchronized Singleton getInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
    
    // 기타 메소드
}
  • synchronized 키워드를 추가하면 한 스레드가 메소드 사용을 끝내기 전까지 다른 스레드는 기다려야 한다. 따라서 2개의 스레드가 이 메소드를 동시에 실행하는 일은 일어나지 않는다.

  • 이러면 문제가 해결되긴 하지만 동기화 때문에 속도 문제가 생길 수 있다. 동기화가 꼭 필요한 시점은 메소드가 시작할 때 뿐이기 때문에 이 부분만 동기화하는 것이 좋다.

더 효율적인 방법으로 멀티스레딩 문제 해결하기

첫 번째 방법은, getInstance()의 속도가 그리 중요한게 아니라면 그냥 둔다. 다만 메소드를 동기화하면 성능이 100배 정도 저하된다는 사실을 기억해야 한다. 병목으로 작용한다면 다른 방법을 써야 한다.

두 번째 방법은, 인스턴스가 필요할 때는 생성하지 말고 다음과 같이 처음부터 만드는 것이다.

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

    private Singleton() {}
  
    public static Singleton getInstance() {
        return uniqueInstance;
    }
}
  • 정적 초기화 부분(static initializer)에서 Singleton의 인스턴스를 생성한다. 이러면 스레드를 써도 별 문제가 없다.

세 번째 방법은, DCL(Double-Checked-Locking)을 써서 getInstance()에서 동기화되는 부분을 줄이는 것이다.

DCL을 사용하면 인스턴스가 생성되어 있는지 확인한 다음 생성되어 있지 않았을 때만 동기화할 수 있다. 이러면 처음에만 동기화하고 나중에는 동기화하지 않아도 된다. 바로 우리가 원하던 것이다.

public class Singleton {
    private static Singleton uniqueInstance;

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

DCL은 자바 1.4 이전 버전에서는 쓸 수 없다.

싱글톤 패턴 이모저모

  • 간단한 패턴은 아니다.

  • 클래스 로더마다 다른 네임스페이스를 정의하기에 클래스 로더가 2개 이상이라면 같은 클래스를 여러 번 로딩할 수도 있다. 그러니 싱글턴을 그런 식으로 로딩하면 인스턴스가 여러 개 만들어지는 문제가 발생할 수 있으니 조심해서 사용해야 한다. 물론 클래스 로더를 직접 지정하면 문제를 피할 수 있다.

  • 리플렉션, 직렬화, 역직렬화도 싱글턴에서 문제가 될 수 있으니 주의하자.

  • 느슨한 결합 원칙에 따르면 "상호작용하는 객체 사이에서 최대한 느슨한 결합을 추구"해야 하는데 싱글턴을 사용하면 이 원칙을 위배하기 쉽다. 싱글턴을 바꾸면 연결된 모든 객체도 바꿔야 할 가능성이 높다.

  • 한 클래스는 1가지 일만 해야하는데 싱글턴은 자신의 인스턴스를 관리하는 일 외에도 그 인스턴스를 사용하고자 하는 목적에 부합하는 작업을 책임져야 하므로 2가지를 책임지고 있다고 볼 수 있다. 하지만 클래스 내에 인스턴스 관리 기능을 포함한 클래스를 적지 않게 볼 수 있다. 그러면 전체적인 디자인을 더 간단하게 만들 수 있기 때문이다. 또한, 많은 개발자가 싱글턴에 이미 익숙하다.

  • 싱글턴의 서브 클래스를 만드려면 private 생성자부터 해결해야 하는데 그러면 protected로 바꿔야 해서 싱글턴이라고 할 수 없다. 게다가 모든 서브클래스가 똑같은 인스턴스 변수를 공유하게 된다.

  • 자바의 전역 변수는 객체의 정적 레퍼런스이므로 게으른 인스턴스 생성을 할 수 없고, 처음부터 끝까지 인스턴스를 갖고 있어야 한다. 싱글턴 패턴은 클래스가 하나의 인스턴스만 갖도록 하고, 전역적인 접근을 제공할 때 사용한다. 전역 변수를 사용하면 두 번째 목표는 달성할 수 있지만 첫 번째 목표는 달성할 수 없다. 또한 전역 변수는 네임스페이스를 지저분하게 만든다.

enum을 이용한 싱글턴 패턴

지금까지 논의한 동기화 문제, 클래스 로딩 문제, 리플렉션, 직렬화와 역직렬화 문제 등은 enum을 써서 해결할 수 있다.

public enum Singleton {
    UNIQUE_INSTANCE;
    // 기타 필요한 필드

public class SingletonClient {
    public static void main(String[] args) {
        Singleton singleton = Singleton.UNIQUE_INSTANCE;
        // 여기서 싱글턴 사용
    }
}

이제 싱글턴이 필요할 땐 enum을 쓰면 된다. 만약 자바 관련 면접에서 enum을 쓰지 않고 싱글턴을 구현하는 방법을 묻는다면 앞에서 나온 싱글턴 패턴을 이야기할 수 있다.

Last updated

Was this helpful?