본문 바로가기

Design Patterns/영문 위키(Wikipedia)

Singleton pattern(싱글톤)

먼저 이 글은 영문 위키의 글을 번역한 글임을 알려드립니다.

영어 실력이 부족한 관계로 오역이 있을 수도 있으니 이해해주세요^^;

원문 주소 : http://en.wikipedia.org/wiki/Singleton_pattern



생성 패턴 - 싱글톤



소프트웨어 공학에서 싱글톤 패턴은 클래스의 인스턴스화를 하나의 객체로 제한을 둠으로써 싱글톤의 수학적 컨셉(수학에 싱글톤이라는게 있나 보군요. 참고 : http://en.wikipedia.org/wiki/Singleton_(mathematics))을 구현한 디자인 패턴이다. 이 패턴은  시스템에 걸쳐 딱 하나의 객체가 필요할 때에 유용하다. 


이 컨셉은 다음과 같은 시스템에서 일반적으로 사용된다. 

1. 오직 하나의 객체가 존재하는 것이 더 효과적일 때

2. 일정 개수의 객체만 생성하도록 제한을 둬야할 때


싱글톤 패턴을 사용함에 있어서 비판적인 시각도 있는데, 일부에서는 싱글톤이 남용되고 있거나 굳이 하나의 유일한 객체가 필요한 상황이 아닌 곳에서도 불필요하게 도입될 수도 있고, 애플리케이션 안에서 전역 상태(global state)를 도입한다고 판단하기 때문에 안티패턴(자주 사용되기는 하나 효과적이지 못하거나 부작용이 낳는 패턴)으로 간주하기도한다. 


C++에서는 싱글톤이 프로그래머에게 제어를 넘김으로써, 동적 초기화의 순서에 대한 예측 불가능으로부터 분리시킬 수 있다. (정확히는 모르겠지만, 의미를 추측해보면 c++에서는 전역 변수를 사용할 때 어떤 변수가 먼저 초기화될지 순서를 예측하기 힘들 수 있는데, 싱글톤을 이용하면 명시적으로 그 초기화 순서를 정할 수 있다는 의미인 것 같습니다.)


일반적인 사용 Common Uses

  • 추상 팩토리, 빌더, 프로토타입 패턴을 구현할 때에 싱글톤을 사용할 수 있다.
  • 오직 하나의 퍼사드 객체만이 필요하기 때문에, 퍼사드(Facade) 객체는 종종 싱글톤이다.
  • 상태 객체(State Object, 스테이트 패턴 객체를 의미하는 듯 합니다.)는 종종 싱글톤이다.
  • 싱글톤은 종종 아래와 같은 이유때문에 전역 변수보다 선호된다.
    • 싱글톤은 불필요한 변수들로 전역 네임스페이스를 오염시키지 않는다. 
    • 많은 언어에서 전역 변수가 항상 자원(메모리)을 소비하는데에 비해, 싱글톤은 게으른 할당과 초기화를 지원한다.


구조 Structure


구현 Implementation

싱글톤 패턴의 구현은 하나의 인스턴스와 전역 접근 원칙을 만족해야만한다. 이는 클래스 객체를 생성하지 않고 싱글톤 클래스 멤버에 접근할 수 있는 메카니즘과 클래스 객체들 사이에서 클래스 멤버의 값을 유지하는 메커니즘을 필요로 한다. 싱글톤 패턴은 객체가 없을 때에 새로운 클래스 객체를 생성하는 메서드를 가지는 클래스를 만듦으로써 구현할 수 있다. 만약 객체가 이미 존재한다면 해당 메서드에서는 이미 생성된 객체에 대한 레퍼런스를 반환한다. 객체가 다른 방법으로 생성되지 않도록 하기 위해서 생성자는 private으로 한다. 


간단한 스태틱 객체와 싱글톤의 차이에 주목하라. : 싱글톤은 스태틱 객체로 구현되기는 하지만, 늦게 생성될 수 있고 필요하기 전까지는 메모리나 자원을 필요로하지 않는다. 또다른 주목할만한 차이점은 인터페이스가 단순히 마커(marker)가 아닐 경우에, 스태틱 멤버 클래스는 그 인터페이스를 구현할 수 없다는 점이다. 따라서 클래스가 어떤 인터페이스를 구현하기 위해서는 스태틱 멤버 클래스가 아닌 싱글톤으로 구현해야 한다. (두번째 차이는 이해가 잘 안되네요;; 스태틱 멤버 클래스도 사용하는 부분에서만 스태틱으로 선언해서 사용하는 것이지 정의 부분은 일반 클래스랑 다를게 없는데, 왜 인터페이스를 구현하는 것이 안된다는지....?)


싱글톤 패턴은 멀티 스레드 기반의 애플리케이션에서 조심스럽게 사용해야 한다. 만약 싱글톤 객체가 아직 생성되지 않은 시점에서 두개의 스레드가 동시에 싱글톤의 생성 메서드를 호출하면, 두 스레드에서 동시에 싱글톤 객체의 생성여부를 체크할 것이고 이때 단 하나의 객체만 생성하도록 해야한다. 만약 프로그래밍 언어에서 동시(concurrent) 처리 능력(capabilities)을 가지고 있다면 그 메서드(싱글톤 객체를 체크하고 생성하는 메서드)는 상호 보완(mutually exclusive)적으로 만들어져야만 한다.(번역을 하니 말이 조금 어려운데요, 쉽게 풀자면 프로그래밍 언어에서 뮤텍스나 세마포어 같은 기능을 지원할 경우에 싱글톤의 객체 생성 메서드의 원자성을 보장해야한다는 의미입니다.)


이 문제에 대한 고전적인 해결방법은 그 클래스에 상호배제(mutual exclusion)를 사용해서 객체가 생성 중임을 알리는 것이다. (싱글톤 객체 생성 메서드에서 세마포어 등을 이용해서 두 개 이상의 스레드가 동시 접근이 불가능하도록 하라는 의미입니다.)


예 Example

여기에서 보여주는 자바 솔루션은 모두 스레드 세이프(thread-safe, 멀티 스레드 환경에서 상호배제를 보장해 준다는 것을 의미)하도록 만들었다. 자바 5.0부터는 싱글톤을 생성하는 가장 쉬운 방법은 enum 타입 방식인데 이 색션의 가장 마지막에 보여줄 것이다. 


게으른 초기화 Lazy initialization


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


전통적인 간단한 방법 Traditional simple way

이 방법은 언어 상의 특별한 구조가 필요없이 스레드 세이프한 방법이지만 위에서 보여준 게이른 초기화를 하지 않는 방법이다. 싱글톤 클래스가 초기화될 때 그 객치(instance)또한 생성된다. 이는 getInstance()를 호출하기 훨~씬 전일 수도 있다. 예를들어 싱글톤 클래스의 스태틱 메서드가 호출될 때 싱글톤 객체가 생성될 수 있다. 게으른 초기화가 필요없거나, 애플리케이션이 실행된 초기에 싱글톤 객체가 생성될 필요가 있거나, 당신의 싱글톤 클래스에 다른 스태틱 멤버나 스태틱 메서드가 없다면(스태틱 멤버나 메서드가 없다는 것은 유일한 스태틱 메서드인 getInstance()가 호춭될 때 싱글톤 객체가 생성됨을 의미한다.), 이 간단한 방법이 사용될 수 있을 것이다.


public class Singleton {
        private static final Singleton instance = new Singleton();
 
        // Private constructor prevents instantiation from other classes
        private Singleton() { }
 
        public static Singleton getInstance() {
                return instance;
        }
}


스태틱 블럭 초기화 Static block initialization

몇몇 저자들은 전처리(pre-processing)를 하는 비슷한 방법을 말한다. 이 방법에서 보면 전통적인 방법은 이 방법의 특별한 케이스로 볼 수 있다. 

public class Singleton {
  private static final Singleton instance;
 
  static {
    try {
      instance = new Singleton();
    } catch (IOException e) {
      throw new RuntimeException("Darn, an error's occurred!", e);
    }
  }
 
  public static Singleton getInstance() {
    return instance;
  }
 
  private Singleton() {
    // ...
  }
}


빌 퍼그의 방법 The solution of Bill Pugh

Maryland 대학의 컴퓨터 공학 연구자인 Bill Pugh는 자바에서 구현될 때 싱글톤 패턴의 코드 이슈에 관해 글을 썼었다. "Double-checked locking" 에 들어있는 Pugh의 노력은 자바5의 메모리 모델의 변화를 야기했고, 자바에서 싱글톤을 구현하는 표준 방법으로 간주됐다. initialization on demand holder idiom으로 알려진 이 기술은 가능한 게으르게 하고, 모든 자바 버전에서 동작한다.  이 방법은 클래스 초기화에 대한 언어의 보증(guarantees)을 이용하고, 따라서 모든 자바를 따르는 컴파일러나 가상머신에서 잘 동작한다.


중첩된 클래스는 getInstance()가 호출되기 전까지는 참조되지 않는다. 따라서 이 방법은 특별한 언어의 구조가 필요없는(예를들면 volatile이나 synchronized) 스레드 세이프(thread-safe)한 방법이다.


public class Singleton {
        // Private constructor prevents instantiation from other classes
        private Singleton() { }
 
        /**
        * SingletonHolder is loaded on the first execution of Singleton.getInstance() 
        * or the first access to SingletonHolder.INSTANCE, not before.
        */
        private static class SingletonHolder { 
                public static final Singleton instance = new Singleton();
        }
 
        public static Singleton getInstance() {
                return SingletonHolder.instance;
        }
}

내부 클래스인 Singletonholder는 static final이나 읽기만 가능한 클래스 멤버에 접근 가능한 프로퍼티(Property)를 구현함으로써 대체될 수 있다. C#에 있는 게으른(lazy) 객체처럼 Singleton.Instance 프로퍼티가 호출될 때마다, 이 싱글톤은 제일 처음 인스턴스화된다.


Enum을 이용한 방법 The Enum way

Joshua Bloch는 그의 책 "Effective Java"2판에서 enums을 지원하는 어떤 자바 버전이든 상관없이 "유일한 enum 타입은 싱글톤을 구현하는데 가장 좋은 방법이다.(a single-element enum type is the best way to implement singleton)"라고 언급했다. enum의 사용은 구현하기 매우 쉬운 방법이고, 직렬화 가능한 객체(serializable objects)와 관련된 문제가 없다.(이러한 문제가 있다면 다른 방법으로 우회해서 구현해야한다.)


public enum Singleton {
        INSTANCE;
        public void execute (String arg) {
                //... perform operation here ...
        }
}

public메서드는 필요한 타입의 인자를 사용하는데 쓰일 수 있다. ; 여기에서는 String 인자가 사용됐다.

이 접근법은 자바의 enum 값이 자바 프로그램 내에서 오직 한번만 인스턴스화 되는 점을 이용함으로써 싱글톤을 구현한다. 자바의 enum 값은 전역적으로 접근 가능하기 때문에, 이는 싱글톤이다. 이 방법의 문제점은 enum 타입이 유연성이 떨어질 수 있다는 점이다. ; 예를들면 늦은 초기화를 지원하지 못한다는 점이다.


프로토타입에 기반한 싱글톤 Prototype-based singleton

클래스 대신에 객체가 사용되는 프로토타입 기반 프로그래밍(prototype-based programming) 언어에서 "싱글톤"은 단순히 복제되지 않거나 다른 객체의 프로토타입으로 사용되지 않는 것을 의미한다. 


Foo := Object clone
Foo clone := Foo


팩토리 메서드 패턴과 함께 사용되는 예 Example of use with the factory method pattern

싱글톤 패턴은 구체적인 타입을 사용하는 부분에서 모르는 시스템 전역 자원을 생성하기 위해 종종 팩토리 메서드 패턴과의 조합으로 사용되고는 한다. 이 두 패턴을 사용하는 한 예는 자바의 Abstract Window Toolkit(AWT)이다.

java.awt.Toolkit은 다양한 AWT 컴포넌트를 특정 네이티브 툴킷 구현으로 바인딩시키는(bind) 추상클래스이다. Toolkit클래스는 Toolkit.getDefaultToolkit() 팩토리 메서드를 가지고 있는데, 이 메서드는 플랫폼에 특화된 Toolkit의 서브클래스를 리턴한다. AWT가 바인딩을 위한 오직 하나의 객체만 필요로 하고, 이 유일한 객체는 상대적으로 생성 비용이 크게 들기 때문에, Toolkit 객체는 싱글톤으로 한다. toolkit 메서드는 한 객체에서 구현되어야 하고, 클래스의 static 메서드로 구현되어서는 안되는데 그 이유는 구체적인 구현은 플랫폼 독립적인 컴포넌트에게 알려져 있지 않기 때문이다. Toolkit클래스의 서브클래스 이름은 System.getProperties()를 통해 얻어지는 "awt.toolkit" 환경 속성으로 정해진다.


예를들어, toolkit에 의해 수행되는 바인딩은 java.awt.Window를 플랫폼 특화된 java.awt.peer.WindowPeer 구현으로 바인딩하는 지원 구현(backing implementation)을 허락한다. Window클래스와 window를 사용하는 애플리케이션도 peer의 어떤 플랫폼 특화된 서브클래스가 사용되는지 알 필요가 없다.


문제점 Drawbacks

이 패턴은 전역 상태(global state)를 애플리케이션에 도입함으로써, 유닛 테스트를 더 어렵게 만든다. 또한 멀티 스레드에서 싱글톤에 접근하기 위해서는 직렬화되어야(serialised) 하기 때문에(예를들면 lock을 걸어서) 프로그램에서 동시성 가능성도 낮춘다는 것을 잘 알아야 한다. dependency injection의 대변자들은 이 패턴이 private과 static메서들르 사용하기 때문에 안티패턴으로 간주한다. 일부에서는 자바나 PHP같은 언어에서 리플렉션(reflection)을 이용해서 싱글톤 패턴을 부수는 방법을 제안하기도 했다.