Singleton
Singleton은 클래스가 오직 하나의 인스턴스만 갖도록 보장하면서 이 인스턴스에 대한 전역 접근점을 제공하는 생성 디자인 패턴입니다.

Intent
Singleton 패턴은 두 가지 문제를 동시에 해결합니다:
- 클래스에 인스턴스가 하나만 있도록 보장: 데이터베이스나 파일 같은 공유 리소스에 대한 접근을 제어할 때 유용합니다.
- 해당 인스턴스에 대한 전역 접근점 제공: 전역 변수처럼 어디서든 접근할 수 있지만, 덮어쓰기로부터 보호됩니다.
Problem
Singleton 패턴은 Single Responsibility Principle을 위반하여 두 가지 문제를 동시에 해결합니다:
단일 인스턴스 보장
일반 생성자는 항상 새로운 객체를 반환하므로, 생성자 호출만으로는 단일 인스턴스를 보장할 수 없습니다. 데이터베이스 연결이나 파일 시스템 접근 같은 공유 리소스를 제어하려면 단일 인스턴스가 필요합니다.
전역 접근점 제공
전역 변수는 편리하지만 위험합니다. 어떤 코드든 변수의 내용을 덮어쓸 수 있어 앱이 충돌할 수 있습니다. Singleton은 이 문제를 해결합니다.
Solution
모든 Singleton 구현에는 두 가지 공통 단계가 있습니다:
- 기본 생성자를 private으로 설정: 다른 객체가
new연산자를 사용하지 못하도록 합니다. - 정적 생성 메서드 제공: 생성자 역할을 하며, 내부적으로 private 생성자를 호출하여 객체를 생성하고 정적 필드에 저장합니다. 이후 모든 호출은 캐시된 객체를 반환합니다.
Real-World Analogy

정부가 Singleton 패턴의 좋은 예시입니다:
- 한 국가에는 하나의 공식 정부만 있습니다
- 정부를 구성하는 개인이 바뀌어도 “X국 정부”라는 명칭은 동일합니다
- 이 명칭은 해당 인스턴스에 대한 전역 접근점입니다
Structure


| 구성 요소 | 역할 |
|---|---|
| Singleton | 정적 메서드 getInstance()를 선언하여 동일한 인스턴스를 반환. 생성자는 클라이언트 코드로부터 숨겨짐 |
Pseudocode
데이터베이스 연결을 관리하는 Singleton 예시:
class Database { private static instance: Database | null = null;
// private 생성자로 외부에서 직접 인스턴스 생성 방지 private constructor() { // 데이터베이스 초기화 코드 console.log("Database connection initialized"); }
// 인스턴스를 가져오는 정적 메서드 public static getInstance(): Database { if (Database.instance === null) { Database.instance = new Database(); } return Database.instance; }
public query(sql: string): void { console.log(`Executing query: ${sql}`); // 데이터베이스 쿼리 로직 }}
// 클라이언트 코드class Application { main(): void { const foo = Database.getInstance(); foo.query("SELECT * FROM users");
const bar = Database.getInstance(); bar.query("SELECT * FROM products");
// foo와 bar는 동일한 객체를 참조 console.log(foo === bar); // true }}Thread-Safe 구현
멀티스레드 환경에서는 동시에 여러 스레드가 인스턴스를 생성하려 할 수 있습니다:
class ThreadSafeDatabase { private static instance: ThreadSafeDatabase | null = null; private static lock = new Object();
private constructor() { // 초기화 코드 }
public static getInstance(): ThreadSafeDatabase { // Double-checked locking if (ThreadSafeDatabase.instance === null) { // synchronized(ThreadSafeDatabase.lock) { if (ThreadSafeDatabase.instance === null) { ThreadSafeDatabase.instance = new ThreadSafeDatabase(); } // } } return ThreadSafeDatabase.instance; }}Applicability
다음의 경우에 패턴을 사용합니다:
단일 인스턴스가 필요할 때
프로그램의 모든 클라이언트가 공유하는 단일 인스턴스가 필요할 때 사용합니다. 예: 데이터베이스 연결, 로그 관리자 등.
전역 변수를 엄격하게 제어할 때
일반 전역 변수와 달리 Singleton은 인스턴스가 하나만 존재함을 보장합니다. Singleton 클래스만 캐시된 인스턴스를 교체할 수 있습니다.
How to Implement
-
정적 필드 추가: Singleton 인스턴스를 저장할 private static 필드를 클래스에 추가합니다.
-
정적 생성 메서드 선언: Singleton 인스턴스를 가져오는 public static 메서드를 선언합니다.
-
지연 초기화 구현: 메서드 내에서 첫 호출 시 새 객체를 생성하고 정적 필드에 저장합니다. 이후 모든 호출은 이 객체를 반환합니다.
-
생성자를 private으로: 클래스 생성자를 private으로 만듭니다. 정적 메서드만 이 생성자를 호출할 수 있습니다.
-
클라이언트 코드 수정: 생성자 직접 호출을 정적 생성 메서드 호출로 대체합니다.
Pros and Cons
장점
| 장점 | 설명 |
|---|---|
| 단일 인스턴스 보장 | 클래스에 인스턴스가 하나만 있음을 확신할 수 있습니다 |
| 전역 접근점 | 해당 인스턴스에 대한 전역 접근점을 얻습니다 |
| 지연 초기화 | 처음 요청될 때만 초기화됩니다 |
단점
| 단점 | 설명 |
|---|---|
| SRP 위반 | 두 가지 문제를 동시에 해결합니다 |
| 나쁜 설계 은폐 | 컴포넌트가 서로 너무 많이 알고 있음을 숨길 수 있습니다 |
| 멀티스레드 환경 | 여러 스레드가 동시에 인스턴스를 생성하지 않도록 처리해야 합니다 |
| 단위 테스트 어려움 | private 생성자와 static 메서드로 인해 모킹이 어렵습니다 |
Relations with Other Patterns
| 패턴 | 관계 |
|---|---|
| Facade | 하나의 facade 객체로 충분하므로 종종 Singleton으로 변환 가능 |
| Flyweight | 공유 상태를 하나의 flyweight 객체로 줄일 수 있다면 Singleton과 유사해짐 |
| Abstract Factory, Builder, Prototype | 모두 Singleton으로 구현될 수 있음 |