싱글톤 패턴
오직 한 개의 클래스 인스턴스만을 갖도록 보장하고, 이에 대한 전역적인 접근점을 제공한다. 시스템 런타임, 환경 세팅에 대한 정보 등 인스턴스가 여러 개일 때 발생하는 문제들을 해결할 수 있는 패턴이다.
구조
- Singleton - Instance() 연산을 정의하여 유일한 인스턴스로 접근할 수 있도록 한다. 유일한 인스턴스를 생성하는 책임을 맡는다.
기본 인터페이스
//싱글톤 클래스
public class Singleton
{
//자기 자신을 멤버로 갖는다.
private static Singleton m_instance;
public int a;
private Singleton(){}
//Instance() 클래스는 단일체 인스턴스를 생성하고 접근하는 책임을 가진다.
public static Singleton Instance()
{
if (m_instance == null)
{
m_instance = new Singleton();
}
return m_instance;
}
}
public class MainTest
{
public static int Main(string[] args)
{
//Singleton에 3을 저장
Singleton.Instance().a = 3;
//싱글톤 객체가 유지됨
int a = Singleton.Instance().a;
Console.WriteLine(a);
return 0;
}
}
3
왜 사용해야 할까?
싱글톤 패턴은 관리자 객체(Manager)를 생성하는 데 도움을 준다. 게임 내 환경설정 정보를 관리하는 데 A에서 호출한 환경설정 객체와 B 에서 호출한 환경설정 객체가 다르다면 어떻게 될까? A에선 키보드를 통해 조작하도록 설정했는데, B에선 키보드 + 마우스로 조작하도록 설정되었다면 A의 환경설정을 따라갈 지, B의 환경설정을 따라갈 지 결과에 혼란이 발생하게 될 것이다. 하지만 환경설정이 단 하나만 있음을 보장한다면 A의 환경설정이든 B의 환경설정이든 같은 환경설정이기 때문에 결과를 하나로 유지할 수 있다.
가장 편한 점은 반복 사용되는 데이터를 공유할 수 있다는 점이다. 음악, 음향 등을 관리하는 SoundManager나 UI를 관리하는 UIManager, 게임의 전반적인 데이터를 관리하는 GameManager 같은 관리 객체들을 싱글톤 패턴으로 생성하면 어떤 클래스에서든 해당 Manager를 통해 접근, 수정이 용이해진다.
싱글톤 패턴은 메모리 절약하는 데에도 도움을 준다. 스타크래프트에서 유닛에 대한 정보를 제공해주는 DataManager가 있다고 가정해보자. DataManager가 하는 일은 클라이언트가 유닛의 정보를 요구하면 문서로 저장된 유닛 정보를 Unit 클래스로 변환하여 클라이언트에게 반환하는 것이다. 싱글톤 패턴을 사용하지 않는다면 클라이언트는 유닛 정보를 요구할 때마다 DataManager 객체를 생성하고, DataManager가 Unit 클래스로 변환하는 과정을 반복한다.
클라이언트 요구 -> DataManager 생성 -> Unit Data 불러오기 -> Unit 클래스로 변환 -> Unit 반환
해당 과정이 복잡하고, 많은 메모리를 사용할 가능성이 존재한다. DataManager는 많이 사용될 가능성이 존재하기 때문에 반복되는 생성-해체로 인해 메모리 릭(Memory Leak)이 발생할 가능성도 많다. 아예 처음 요구되었을 때 DataManager 객체를 생성하고 유지한 채, Unit 클래스로 변환한 결과를 저장함으로써 필요할 때마다 호출하면 이후 해당 유닛을 호출할 때에는 생성 및 변환 과정을 생략할 수 있다.
클라이언트 요구 -> DataManager 호출 -> Unit 반환
전역 변수와 싱글톤의 차이
전역 변수로 선언된 클래스도 컴파일 시점에 이미 클래스파일이 로드되며, 객체를 따로 생성하지 않고도 하나의 인스턴스로 사용이 가능하다. 그렇다면 왜 전역 변수 대신 싱글턴 패턴을 사용하는 것일까?
싱글턴 패턴은 전역 변수의 단점을 보완한다. 우선 많은 메모리를 잡아먹는 클래스가 있다고 해보자. 이 클래스를 전역변수로 선언하면 그 클래스의 인스턴스를 사용하지 않아도 이미 메모리상에 올라가버린다. 이런 상황에 그 거대한 클래스를 한번도 사용하지 않는다면 메모리 낭비가 되는 것이다. 반면에 싱글턴 패턴은 클래스를 사용하기 전에는 메모리를 사용하지 않지만 클래스를 사용하는 시점에 객체를 생성하기 때문에 불필요한 메모리 낭비를 줄일 수 있는 장점이 있다.
장점
- 유일하게 존재하는 인스턴스로의 접근을 통제할 수 있다.
- 데이터를 쉽게 공유하고 유지할 수 있다.
- 반복적인 객체와 데이터 생성에 필요한 메모리를 절약할 수 있다.
단점
- 모듈 간의 결합도(Coupling)가 높아지기 때문에 디버깅이 어려워질 수 있다.
- 멀티쓰레드 환경에서 동시에 여러 쓰레드가 접근하면 오류가 발생할 수 있기 때문에 동기화 과정을 추가해야한다.
- SOLID 원칙 다수 위반 : 단일 인스턴스를 생성하기 때문에 한 객체가 많은 책임을 가질 수 있고(SRP), 모듈 간 결합도가 높아지기 때문에 OCP를 위반하고, 단일 인스턴스가 추상 클래스가 아닌 구체 클래스에 의존하기 때문에 DIP도 위반한다.
추가 예시
멀티쓰레드에서의 싱글톤
지금까지 배운 싱글톤은 게으른 초기화(Lazy Singletone)이라고 한다. 게으른 싱글톤 패턴은 멀티쓰레드 환경에서 복수의 쓰레드에서 동시에 싱글톤 객체를 호출하면 경합 상태(Race Condition)으로 인해 서로 다른 싱글톤 객체를 호출하게 된다.
이러한 단점을 보완하기 위해 여러 코드 기법들이 존재하며, 각 코드 기법마다 장단점이 있기 때문에 상황에 맞게 사용하는 것이 좋다.
1) 이른 초기화(Eager Initialization)
- 프로그램이 시작할 때, 미리 만들어두는 가장 쉬운 방식이다.
- 복수의 쓰레드가 동시에 접근하더라도 이미 생성된 상태이기 때문에 바로 인스턴스를 반환한다. 쓰레드 세이프
- 싱글톤 객체를 사용하지 않더라도 이미 메모리에 적재된 상태이기 때문에 전역 변수를 사용하는 것과 별반 차이가 없다. 싱글톤 객체를 사용하기 전까지 공간 자원 낭비가 발생한다.
public class Singleton
{
//자기 자신을 멤버로 갖는다.
private readonly static Singleton INSTANCE = new Singleton();
private Singleton(){}
//Instance() 클래스는 단일체 인스턴스를 생성하고 접근하는 책임을 가진다.
public static Singleton Instance()
{
return INSTANCE;
}
}
2) 쓰레드 세이프 초기화(Thread Safe Initialization)
- Instance()를 실행 시 동기화 키워드를 사용하여 임계 영역(Critical Section)을 만드는 방식
- 쓰레드가 동시에 접근하는 것을 방지하지만, 많은 쓰레드가 호출할 경우 자원이 반환될 때까지 쓰레드 대기상태를 유지하기 때문에 성능저하가 발생할 가능성이 높다.
public class Singleton
{
private static Singleton m_instance;
//동기화용 락 오브젝트
private static object lockObject = new object();
private Singleton(){}
public static Singleton Instance()
{
//먼저 들어오는 쓰레드가 선점한다.
lock (lockObject)
{
if (m_instance == null)
{
m_instance = new Singleton();
}
return m_instance;
}
}
}
3) 이중 확인 잠금(Double-Checked Locking)
- 쓰레드 세이프 초기화 방식에서 매번 호출할 때마다 동기화를 해야하는 경우를 보완한 방식
- instance가 null일 경우에만 객체 생성을 위한 동기화 처리를 하기 때문에 이후 접근할 때는 동기화를 하지 않는다.
- 그러나 자바에서만 추가적인 키워드(voletile)를 통해 정상적으로 동작될 뿐, C#에서는 메모리 모델, 최적화, 리오더링 등의 문제로 인해 정상적으로 동작하지 않는다.
public class Singleton
{
//자기 자신을 멤버로 갖는다.
private static Singleton m_instance;
private static object lockObject = new object();
private Singleton(){}
//Instance() 클래스는 단일체 인스턴스를 생성하고 접근하는 책임을 가진다.
public static Singleton Instance()
{
//초기 null인 경우에만 동기화 처리를 한다.
if (m_instance == null)
{
lock (lockObject)
{
if (m_instance == null)
{
m_instance = new Singleton();
}
}
}
return m_instance;
}
}
4) Bill Pugh Solution
- 싱글톤 클래스에 정적 내부 클래스를 선언하는 방식
- Instance()함수가 호출되면 Singleton 클래스가 로드되고, Singleton 클래스 내부에 static 클래스가 존재하므로 이 또한 같이 로드된다. 즉, 싱글톤이 호출되어야 메모리에 할당되는 방식이기에 공간 자원 낭비를 없앨 수 있다.
- 클래스 호출과 동시에 이너클래스가 로드되고, 그와 동시에 Instance가 생성되므로 쓰레드 환경에서도 안전하다. 하지만...
public class Singleton
{
private Singleton () {}
//싱글톤 클래스가 호출되어야 static inner class가 메모리에 할당됨
private static class SettingHolder
{
public readonly static Singleton Instance = new Singleton();
}
public static Singleton Instance()
{
return SettingHolder.Instance;
}
}
참고자료
'프로그래밍 이론 > 디자인 패턴' 카테고리의 다른 글
07. [Design Pattern] 브릿지(Bridge) 패턴 (0) | 2023.11.23 |
---|---|
06. [Design Pattern] 어댑터(Adapter) 패턴 (0) | 2023.11.21 |
04. [Design Pattern] 프로토타입(Prototype) 패턴 (0) | 2023.11.07 |
03. [Design Pattern] 빌더(Builder) 패턴 (0) | 2023.11.07 |
02. [Design Pattern] 추상 팩토리(Abstract Factory) 패턴 (0) | 2023.10.27 |