인스턴스가 하나 뿐인 특별한 객체를 만들 수 있게 해주는 패턴입니다.
싱글턴 패턴은 다이어그램만 놓고 보면 그 어떤 패턴보다도 간단합니다.
클래스 다이어그램에 클래스가 달랑 하나 밖에 없으니깐요.
클래스 디자인 관점에서 보면 아주 단순하지만, 구현하는 데 쉽지 않습니다.
어떤용도로 사용할까요?
- 스레드 풀
- 캐시
- 대화상자
- 사용자 설정
- 레지스트리 설정을 처리하는 객체
- 로그 기록용 객체
- 프린터
- 그래픽 카드
같은 디바이스를 위한 디바이스 드라이버 같은 걸로 예로 들 수 있습니다.
public으로 선언된 거라면 클래스는 언제든 인스턴스를 만들 수 있습니다.
객체를 생성하는 방식은 new 연산자를 사용을 하면 된다는 걸 알고 계실겁니다.
만약 위와 같이 public 클래스로 선언하지 않은 클래스라면 같은 패키지 안에 있는 클래스에서만 인스턴스를 만들 수 있습니다.
하지만, 같은 패키지에 속한 클래스에서는 여전히 인스턴스를 두 개 이상 만들 수 있습니다.
하지만? 아래와 같은 코드라면?
public MyClass {
private MyClass() {}
}
생성자가 private 으로 선언 되어있기 때문에 인스턴스를 만들 수 없는 클래스입니다.
private 생성자된 생성자를 사용할 수 있는 객체는 없습니다.
why?
생성자를 호출하려면 일단 그 클래스의 인스턴스가 있어야 되는데,
다른 클래스에서 이 클래스의 인스턴스를 만들 수 없기 때문에 인스턴스를 만드는 것이 불가능합니다.
MyClass 형식의 객체에서만 private 으로 선언된 생성자를 사용할 수 있고,
다른 어떤 클래스에서도 new MyClass() 라고 할 수 없습니다. 따라서, 인스턴스를 만들 수 없습니다.
public MyClass {
public static MyClass getInstance() {
}
}
위의 코드는 MyClass 에 정적메소드가 있습니다.
정적 메소드는
Myclass.getInstance();
위와 같은 방법으로 호출할 수 있습니다.
클래스 메소드라고 불립니다. 정적 메세드를 지칭할 때는 클래스 이름을 써야합니다.
이 두 가지를 합쳐봅시다!
public MyClass {
private MyClass() {}
public static MyClass getInstance() {
return new MyClass();
}
}
이렇게 하면, 객체의 인스턴스를 만들 수가 있습니다.
자. 이제 마무리해서 의사코드가 아닌 제대로 인스턴스를 하나만 만들어질 수 있도록 싱글 패턴 구현 코드를 작성해보겠습니다.
public class Singleton {
// 클래스의 유일한 인스턴스를 저장하기 위한 정적 변수입니다.
private static Singleton uniqueInstance;
private Singleton() {}
public static Singleton getInstance() {
if(uniqueInstance == null) { // uniqueInstance이 null이라면, 인스턴스가 생성되지 않았습니다.
// private으로 선언된 생성자를 이용해서 Single 객체를 만든 다음 uniqueInstance에 그 객체를 대입합니다.
// 이렇게 하면 인스턴스가 필요한 상황이 닥치기 전에는 아예 인스턴스를 생성하지 않게 됩니다.
// 이런 방식을 lazy instantiation 이라합니다.
uniqueInstance = new Singleton();
}
// uniqueInstance가 null이 아니면 이미 객체가 생성된 것입니다. 그런 경우는 바로 return 선언문으로 갑니다.
return uniqueInstance;
}
}
하지만, 하나의 인스턴스만을 생성하고 싶지만, 인스턴스가 하나가 아닌 그 이상이 만들어져 버리는 문제가 발생하기도 합니다.
그러한 문제는 스레드가 하나가 아니라,
다중 스레드일 경우에 위와 같은 코드를 구현을 해도 두 개 이상의 인스턴스를 생성합니다.
2개의 스레드가 돌 경우, JVM에서 아래와 같은 순서로 작업이 처리됩니다.
[1번 스레드]
public static Singleton getInstance() {
> uniqueInstance null
[2번 스레드]
public static Singleton getInstance() {
> uniqueInstance null
[1번 스레드]
if(uniqueInstance == null) {
> uniqueInstance null
[2번 스레드]
if(uniqueInstance == null) {
> uniqueInstance null
[1번 스레드]
uniqueInstance = new Singleton();
> uniqueInstance <object1>
return uniqueInstance;
> uniqueInstance <object1>
[2번 스레드]
uniqueInstance = new Singleton();
> uniqueInstance <object2>
return uniqueInstance;
> uniqueInstance <obejct2>
위 와 같이 두 스레드가 코드를 읽고 흘러가게 되면, 인스턴스가 2개가 만들어지게 됩니다.
이 문제는
getInstance()를 동기화 시키기만 하면 멀티스레딩과 관련된 문제가 간단하게 해결됩니다.
아래에는 해결된 코드입니다.
public class Singleton {
// 클래스의 유일한 인스턴스를 저장하기 위한 정적 변수입니다.
private static Singleton uniqueInstance;
private Singleton() {}
// getInstance() 에 synchronized 키워드만 추가하면
// 한 스레드가 메소드 사용을 끝내기 전까지 다른 스레드는 기다려합니다.
// 즉, 두 스레드가 이 메소드를 동시에 실행 시키는 일은 일어나지 않습니다.
public static synchronized Singleton getInstance() {
if(uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
위와 같이 코드를 짜게되면, 동기화하여 속도에 문제가 생기게 됩니다.
사실 동기화가 꼭 필요한 시점은 이 메소드가 시작되는 때 뿐입니다.
일단 uniqueInstance 변수에 Singleton 인스턴스를 대입하고 나면 굳이 이 메소드를 동기화된 상태로 유지시킬 필요가 없습니다.
첫 번째 과정을 제외하면 동기화는 불필요한 오버헤드만 증가시킬뿐입니다.
그래서 몇 가지 방법을 소개하겠습니다.
- getInstance() 의 속도가 그리 중요하지 않다면 그냥 둡니다.
> 애플리케이션에 큰 부담을 주지 않는다면 그냥 놔둬도 됩니다. 동기화시크는 게 그리 어려운 일도 아니고 효율 면에서도 괜찮을 수 있습니다. 메소드를 동기화하면 성능이 100배 정도 저하된다는 것은 기억합시다. 만약, getInstance() 가 애플리케이션에서 병목으로 작용한다면 다른 방법을 생각해봐야 합니다.
- 인스턴스를 필요할 때 생성하지 말고, 처음부터 만들어 버립니다.
> 애플리케이션에서 반드시 Singleton의 인스턴스를 생성하고, 그 인스턴스를 항상 사용한다면, 또는 인스턴스를 실행 중에 수시로 만들고 관리하기가 성가시다면 아래와 같은 식으로 처음부터 Singleton 인스턴스를 만들어 버리는 것도 괜찮은 방법입니다.
public class Singleton {
// 정적 초기화 부분(static initialiger) 에서 Singleton의 인스턴스를 생성하면 스레드를 써도 문제 없습니다.
private static Singleton uniqueInstance = new Singleton();
private Singleton () {}
public static Singleton getInstance() {
return uniqueInstance(); // 인스턴스가 존재하니, 그냥 리턴만하면 됩니다.
}
}
이런 접근법을 사용하면 클래스가 로딩될 때, JVM에서 Singleton의 유일한 인스턴스를 생성해줍니다.
JVM에서 유일한 인스턴스를 새엇ㅇ하기 전에는 그 어떤 스레드도 uniqueInstance 정적 변수에 접근할 수 없습니다.
- DCL(Double-checking-Locking)을 써서 getInstance() 에서 동기화되는 부분을 줄입니다.
> DCL을 사용하면, 일단 인스턴스가 생성되어 있는지 확인한 다음, 생성되어 있지 않았을 때만 동기화를 할 수 있씁니다.
이렇게 하면 처음에만 동기화를 하고 나중에는 동기화를 하지 않아도 됩니다. (딱 이겁니다.)
아래 코드에 주석설명으로 추가 설명을 하겠습니다.
public class Signleotn {
private volatile static Singleton uniqueInstance;
public static Singleton getInstance() {
if (uniqueInstance == null) { // 인스턴스가 있는지 확인하고, 없으면 동기화된 블럭으로 들어갑니다.
synchronized (Singleton.class) { // 이렇게 하면, 처음에만 동기화가 됩니다.
if (uniqueInstance == null) {
//블록으로 들어온 후에도 다시 한 번 변수가 null인지 확인한 다음 인스턴스를 생성합니다.
uniqueInstance = new Singleton();
}
}
}
}
}
getInstance () 메소드를 사용할 때,
속도가 문제가 될 수 있다면 이런 식으로 Singleton 구현함으로써 오버헤드를 극적으로 줄일 수 있습니다.
여기서 잠깐,
volatile 키워드를 사용하면 멀티스레딩을 쓰더라도 uniqueInstance 변수가 Signleton 인스턴스를 초기화 되는 과정이 올바르게 진행되로록 할 수 있습니다.
(volatile 에 대해서는 추가로 블로그 작성 한 뒤에, 링크를 걸 예정)
'Architecture > Design Pattern' 카테고리의 다른 글
[디자인 패턴] 팩토리 메소드 패턴 (0) | 2022.12.01 |
---|