Ch.5 싱글턴 패턴

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

About

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

The Code

전역 변수론 안되는가?

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

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

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

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

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

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)은 클래스 인스턴스를 하나만 만들고, 그 인스턴스로의 전역 접근을 제공한다.

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

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

해결 방법은 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