본문 바로가기

OOP, FP/디자인패턴

헤드퍼스트 디자인 패턴: 5. 싱글턴 패턴

지난 포스팅에선 객체 생성을 추상 인터페이스를 통해 캡슐화하여 

일련의 제품들을 공급할 수 있는 추상 팩토리 패턴에 대해 알아보았다.


이번엔 어떤 클래스의 인스턴스 개수를 프로그램에서 하나로 제한하는 싱글턴 패턴에 대해 복습한다.


싱글턴패턴

해당 클래스의 인스턴스가 하나만 만들어지고, 어디서든지 그 인스턴스에 접근할 수 있도록 하기 위한 패턴


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

싱글턴 패턴의 고전적인 구현법은 간단하다.

클래스의 생성자를 사용할 수 없게 private으로 선언한 뒤 메소드로 인스턴스에 접근할 수 있게 한다.

인스턴스에 접근하는 메소드가 호출되었을 때, 프로그램 메모리에서 해당 인스턴스가 하나도 생성되지 않았을 경우에만

한번 생성하고 이후에는 인스턴스를 생성하지 않게하면 된다.


public class Singleton {


private static Singleton uniqueInstance;


private Singleton() { }


// 인스턴스에 접근을 허용하기 위한 메소드

public static Singleton getInstance() {


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


return uniqueInstance;

}

}


Singleton이라는 클래스에 자신의 인스턴스를 저장하기 위한 정적 변수를 선언했다.

이후 private 키워드를 이용해 생성자를 외부로부터 감춘다.


마지막으로 getInstance()라는 정적 메소드를 정의하여 정적 변수인 uniqueInstance를 반환한다.

이 때, 반드시 uniqueInstance가 null인지 체크하여 null일 경우 private 생성자를 호출해서 인스턴스를 만들어야한다.

이렇게하면 인스턴스가 필요한 상황이 닥치기 전에는 아예 인스턴스를 생성하지 않게 된다.

이런 방법을 게으른 인스턴스 생성(Lazy Instantation)이라고 부른다.


싱글턴 패턴의 특징


(1) 싱글턴 패턴은 완전히 유일하다 (= 인스턴스가 절대 두 개 이상 존재하지 않는다.)

- 레지스트리의 설정과 같은 정보를 담는 객체가 있을 때, 해당 객체는 하나만 있어야 한다.

  레지스트리 설정이 여러 개 있다면 서로 다른 설정 내역들이 있다는 뜻이므로 혼란을 야기한다.

  하지만, 싱글턴 패턴을 사용하면 애플리케이션에 존재하는 어떤 객체에서도 똑같은 자우너을 활용하도록 할 수 있다.


- 연결 풀, 스레드 풀과 같은 자원풀을 관리하는 데도 싱글턴 패턴이 자주 등장한다.


(2) 개발자가 자신도 모르는 사이에 객체 인스턴스가 여러 개 생기면서 의도하지 않은 버그가 발생하는 일을 방지할 수 있다.


(3) 싱글턴 객체가 필요할 때는 인스턴스를 달라고 요청을 해야한다.

- 보통 getInstance()라는 정적 메소드를 제공하며 다른 어떤 클래스에서도 싱글턴 클래스의 인스턴스를 추가로 만들지 못하게 해야 한다.

  이러한 요청은 어디서든 이루어질 수 있어야 한다.



그러나 이러한 방식은 스레드에서 공유 자원을 사용하는 것과 같은 동기화 문제가 존재한다.

싱글턴 객체가 생성되지 않은 상태에서 두 개의 스레드가 getInstance()를 거의 동시에 호출했다고 하더라도

uniqueInstance == null 을 검사하는 시점이 달라서 인스턴스가 두 개 만들어지는 상황이 벌어진다.


이를 방지하기 위해 자바에서는 동기화 키워드를 제공한다.


public class Singleton {


private static Singleton uniqueInstance;


private Singleton() { }


// 인스턴스에 접근을 허용하기 위한 메소드

public static synchronized Singleton getInstance() {


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


return uniqueInstance;

}

}


synchronized로 메소드 접근을 동기화할 수 있다.

메소드를 동기화하게 되면 다른 클라이언트들은 내 차례가 끝나기 전까지 기다려야 한다.

10번째로 호출한 클라이언트는 앞의 9명의 클라이언트의 메소드 호출이 끝날때까지 기다려야한다.

때문에 동기화는 늘 속도 관련 문제가 딸려온다.


동기화와 속도 보완


(1) 가장 간단한 방법은 클래스(정적) 변수에 아예 시작부터 인스턴스를 대입하는 방법이다.

- 미리 생성되어 있으므로 null검사, 객체 생성을 할 필요가 없다.

- private static Singleton uniqueInstance = new Singleton();

  와 같이 정적 변수를 선언과 동시에 인스턴스화하면 클래스가 로딩될 때 JVM이 유일한 인스턴스를 생성해준다. 


(2) getInstance()의 속도가 중요하지 않다면 그냥 메소드를 동기화시킨 상태로 둔다.

- getInstance()메소드가 애플리케이션의 성능에 큰 부담을 주지 않는다면 그냥 놔둬도 된다.

  동기화의 방법이 어렵거나 복잡하지도 않기 때문이다.

  (메소드를 동기화하면 성능이 약 100배 정도 저하된다고 한다.)


(3) DCL(Double-Checking Locking)을 써서 getInstance() 메소드에서 동기화되는 부분을 줄인다.

- DCL을 사용하면 일단 인스턴스가 생성되어 있는지 확인한 다음 생성 되지 않았을 때만 동기화를 할 수 있다.

  이렇게 하면 처음에만 동기화를 하고 나중에는 동기화가 필요없게된다.



public class Singleton {


private volatile static Singleton uniqueInstance;


private Singleton() { }


// 인스턴스에 접근을 허용하기 위한 메소드

public static Singleton getInstance() {


if (uniqueInstance == null) {


synchronized (Singleton.class) {


if(uniqueInstance == null) {


    uniqueInstance = new Singleton();

}

      }
}


return uniqueInstance;

}

}


맨 처음 if문에서 인스턴스가 있는지 확인하고 없다면 동기화(synchronized)된 블럭으로 감싼다.

이렇게 하면 맨 처음 인스턴스가 없을 때만 동기화가 되며 다음 두 번째부턴 비동기 상태가 된다.


동기화 블럭 내부에선 또 다시 if문으로 null검사를 하고 null일 경우에만 인스턴스를 생성한다.


* volatile 키워드를 사용하면 멀티스레딩을 쓰더라도 uniqueInstance 변수가 Singleton 인스턴스로 초기화

  되는 과정이 올바르게 진행되도록 할 수 있다. (자바 1.4 이전)


* DCL을 이용하면 싱글톤의 동기화 오버헤드를 극적으로 줄일 수 있다.


* 클래스 로더마다 서로 다른 네임스페이스를 정의하기 때문에 싱글턴 인스턴스가 두 개 생성 될 수 있으므로 주의해야한다.



의문사항


(1) 모든 메소드와 변수가 static으로 이루어진 클래스를 만들어도 되지 않나?

- 복잡한 초기화가 필요 없는 경우에만 싱글턴 대신 static으로만 이루어진 클래스를 이용할 수 있다.

  하지만 자바에서 정적 초기화를 처리하는 방법때문에 일이 복잡해질 수도 있다.

  특히, 여러 클래스가 얽혀 있는 경우 초기화 순서와 관련된 아주 찾아내기 어려운 복잡미묘한 버그가 생길 가능성이 있다.


(2) 싱글턴 패턴은 단일 역할 원칙을 위반하는 것이 아닌가?

- 싱글턴은 자신의 인스턴스를 관리하는 역할과 원래 그 인스턴스를 사용하고자하는 목적에 부합하는 역할을 맡고 있다.

  따라서 두 가지 역할을 책임지고 있다고 할 수 있다.

  하지만 클래스 내에 자신의 인스턴스를 관리하는 기능을 포함하고 있는 클래스를 적지 않게 볼 수 있다.

  이는 전체적인 디자인을 좀 더 간단하게 만들 수 있기 때문이다.

  싱글턴에 들어있는 기능을 캡슐화해야 된다는 의견도 많이 있다.


(3) 싱글턴 객체의 서브클래스를 만들어도 되는가?

- 우선 싱글턴 패턴에서는 클래스의 생성자가 private이다.

  때문에 서브 클래스에서 해당 클래스를 확장할 수 없게 된다.


  생성자를 고친다고 하더라도 정적 변수를 바탕으로 구성되어 있기 때문에 모든 서브클래스에서 같은 인스턴스 변수를 공유하게 된다.

  따라서 베이스 클래스에서 레지스트리 같은 것을 구현해놓아야한다.

  확장해서 무언가 큰 이점을 얻을게 아니면 바람직하지 않다고 한다.

  또 싱글턴 패턴은 제한된 용도로 특수한 상황에서 사용하기 위한 용도이므로

  개발하면서 싱글턴을 꽤 많이 사용하고 있다면 전반적인 디자인을 다시 한 번 생각해보는 것이 좋다.



어떤 클래스의 인스턴스 개수를 프로그램에서 하나로 제한하고 전역 접근을 제공하는 싱글턴 패턴에 대한 복습을 마치겠다.


1. 스트래티지 패턴

2. 옵저버 패턴

3. 데코레이터 패턴

4-1. 팩토리 메서드 패턴

4-2. 추상 팩토리 패턴

5. 싱글턴 패턴