프로그래밍 Design Pattern 이해하기 - 5 싱글턴 패턴
싱글턴 패턴 (One of kind Objects : 유일무이한 객체)
개발자 : 싱글턴 패턴은 왜써요!?
고수 : 여러가지 중 단 하나만을 필요로 하는 객체들이 많다. 환경 세팅과 관련된 thread pools, caches나, 로깅을 위한 오브젝트(Log) 혹은 장치 드라이버(그래픽카드나 프린터와 같은) 것들이 그런 것들이다. 사실 우리는 다양한 타입의 객체를 사용하고 있지만, 만약 프로그램을 짤 때 (하나만 필요한 경우에도) 모든 객체들을 각각의 인스턴스를 만들고 사용하게 되면, 실수로 인해 오류가 발생하거나, 코드의 중복 등 유지보수가 힘들어진다.
개발자 : 그래! 이해했어! 하나로 동작하는 클래스를 만드는 이유에 대해서는 알겠는데, 그렇다면 난 이 포스팅을 봐야해? 그냥 전역변수를 static 으로 만들어 사용하면 되는 것 아냐?
고수 : static 으로 생성을 하게 되면 애플리케이션이 시작하는 순간 객체가 생성되게 되. 애플리케이션이 아주 오랜시간 구동된다고 했을 때 실제로 사용이 자주 하지 않는데도 불구하고 자원을 낭비하는 경우가 있어. 때문에 우리는 싱글턴 패턴을 통해 우리가 필요한 순간에 객체들을 만들고 사용하는 것에 대해 이야기 할 거야.
The Little Singleton
싱글턴 Object는 어떻게 만들지 ?
- new MyObject();
그러면 다른 객체에서 MyOjbect를 원하는 경우엔? 또 new MyObject를 부르지않아?
- 맞아
그러면 우리는 하나 이상의 인스턴스를 사용하는 것 아냐?
- 맞아 만약 그것이 일반적인 public class 라면,
아니라면?
- 음, 퍼블릭한 클래스가 아니라면 같은 패키지에서만 클래스를 인스턴스화 할 수 있겠지. 그러나 private class라 하더라도 여러번 인스턴스를 만들 수 있어.
음 이런 코드를 이야기하는거야?
public MyClass {
private MyClass() {}
}
- 아니. 저런 코드는 있을 수 없어. 저런 정의를 할 수 없어.
private 을 써서 외부의 클래스들이 new MyClass를 할 수 없도록 막은 것 뿐이야.
"new"가 아닌 MyClass의 다른 생성자를 통해 인스턴스를 만들도록 할거야
public MyClass{
public static MyClass getInstance(){
}
}
이렇게 MyClass를 static 메서드로 return 하게 만드는 거지. 그럼 필요할 때 MyClass.getInstance();로 호출하면 될 것 같아.
public MyClass{
private MyClass() {}
public static MyClass getInstance(){
return new MyClass();
}
}
싱글턴과의 인터뷰
"싱글턴 자기소개를 해주시죠"
"음 전 유니크한 존재에요. 오로지 저 하나밖에 없죠"
"하나요?"
"네. 하나요. 저는 싱글턴 패턴에 기반해서 언제 어디에서든 호출을 하더라도 오로지 하나의 인스턴스만을 가져요. 환경설정 등을 할 때 사용해요. 만약에 앱이 구동되는 상황에서 '단 한 가지가 필요한 경우에도' 객체가 카피되어 여러개가 만들어진다면, 그건 굉장한 혼란을 일으킬거에요. 저를 사용하면, 앱안의 모든 객체들이 저를 Global resource로 사용할 수 있죠"
"그러면 하나만 묻죠. 어떻게 당신이 하나라는 걸 아나요? 누군가가 new operator로 만들수 있는 것 아닌가요?"
"아니에요! 저는 unique합니다"
"음 개발자들이 하나이상의 인스턴스를 만들지 못한다는거죠?"
"네! 저는 public constructor를 가지고 있지 않거든요"
"Public constructor를 가지고 있지않다니!!!! 정말로요?"
"네~! 저는 private constructor로 사용되요"
"그러면 어떻게 동작을하죠?"
"static method인 getInstnace()를 가지고 있어요. 이 함수를 부르면 단 한번만 인스턴스를 만들고 일할 준비를 하죠"
자 위와 같이 초콜렛보일러를 만들고 최적화를 하기 위해 다중 스레드를 사용하도록 만들어보았다!
그런데 갑자기 문제가 생겨버렸다!
ChocolateBoiler boiler = ChocolateBoiler.getInstance();
fill();
boil();
drain();
멀티플한 스레드들, 두 군데의 스레드에서 위와 같이 만들어놓은 싱글톤을 사용했을 때 동기화의 문제가 생겨버렸다.
아래와 같이 만들면 문제는 쉽게 해결된다.
이렇게 하면 멀티플한 스레드에서 해당 인스턴스를 가져와도 먼저 호출한 곳에서 처리가 되기 때문에 동시에 실행시키는 일은 일어나지 않게 된다! 하지만, 동기화를 하게 되면 불필요한 오버헤드가 증가할 뿐더러 속도문제가 생긴다. (정말 멀티플 하지 않는 이상 과연 크게 속도차이가 날까는 싶지만...)
그렇다면! 더 효율적인 방법을 찾아보자!
1. getInstance()의 속도가 그리 중요하지 않다면, 그냥 내버려 둔다.
맞는 말이다. 동기화를 하면 성능이 100배 정도 느려진다. (생각보다 꽤 느려지는군...) getInstance()가 병목으로 작용한다면, 다른 방법을 생각해봐야 한다.
2. 인스턴스를 필요할 때 생성하지 말고 처음부터 만들어버린다.
이렇게 만들게 되면 클래스가 로딩 될 때 JVM에서 Singleton의 유일한 인스턴스를 생성해준다!
3. DCL (Double-Checking Locking)을 써서 getInstance()에서 동기화 되는 부분을 줄인다.
DCL(Double-Checking Locking)을 사용하면 일단 인스턴스가 생성되어 있는지 확인한다음 생성되어있지 않았을 때만 동기화를 할 수 있다. (자바 1.4 이전 버전에서는 사용할 수 없다.) Volatile 키워드를 쓰면 멀티스레딩을 사용하더라도 uniqueInstance변수가 Singletone 인스턴스로 초기화 되는 과정이 올바르게 작동하도록 할 수 있다.
Q: 그냥 static 으로 메서드들만들고 쓰면 되지 않냐?
A: 예. 만약 당신의 클래스가 이렇게 싱글톤을 쓰지 않으면 굉잫이 복잡해질 수 있다. static으로 initialization하는 것을 핸들링하는 것은 굉장히 Messy(지저분하다)하다. 특히나 멀티플한 클래스들일 수록. 종종 이런 시나리오는 버그를 찾기 굉장히 힘들게 만든다. 싱글턴방법을 쓰면 조금 더 글로벌한 것들을 쉽게 구현할 수 있다.
Q: OO 원칙에 위배되는 것 아니냐!?
A: "One class, One Reponsibility(단일 책임 원칙)", 싱글톤에 위배 되는 것이 맞다.
(단일 책임 원칙은, 모든 클래스는 단 하나의 책임만 가지며 그 책임을 완전히 캡슐화해야 함을 일컫는다.예를 들어 보고서를 편집하고 출력하는 모듈을 생각해보자. 이 모듈은 두 가지 이유로 변경 될 수 있다. 첫번째는 보고서의 내용 때문에 변경될 수 있고, 두번째는 보고서의 형식때문에 변경될 수 있다. 첫번째 것은 내용이 변화하는 것이고, 두번째는 꾸미기 위한 이유기 때문에 두가지는 성향이 다르다. 때문에 이 모듈은 두가지로 나눠야 한다. 이런 이유는 한 관심사에 집중하도록 유지하는 것이 클래스를 튼튼하게 만들기 때문이다) 싱글턴은 확실히 두가지 책임을 진다고 볼수도 있다. 기능에 대한 책임과 자기 자신의 인스턴스를 관리하는 유틸리티성의 책임 두가지이다. 하지만 자신으 인스턴스를 관리하는 것은 책임으로 보기 힘들다. 이미 널리 사용되고 있기 때문에 많은 개발자들이 익숙해 져있다.
* Java 1.2 이전 버전에서는 싱글턴의 GC의 대상이 되어버려 중간중간 새 인스턴스가 만들어진다는 버그가 있었으나 고쳐졌다.