싱글톤 패턴은 클래스 인스턴스를 하나만 만들고, 그 인스턴스로의 전역 접근을 제공하는 패턴
class Singleton {
// 하나뿐인 인스턴스가 저장됨
private static uniqueInstance: Singleton;
// private 생성자를 통해 외부에서 인스턴스 생성 불가능
private constructor() {}
public static getInstance(): Singleton {
if (!this.uniqueInstance) {
this.uniqueInstance = new Singleton();
}
return this.uniqueInstance;
}
// 기타 메소드
}
private static 인스턴스 변수
private 생성자
new 키워드로 인스턴스 생성을 방지public static getInstance() 메서드
유일한 인스턴스 보장
전역 접근 지점
지연 초기화(Lazy Initialization)
상태 공유
class ChocolateBoiler {
private static uniqueInstance: ChocolateBoiler;
private empty: boolean;
private boiled: boolean;
private constructor() {
this.empty = true;
this.boiled = false;
}
public static getInstance(): ChocolateBoiler {
if (!this.uniqueInstance) {
this.uniqueInstance = new ChocolateBoiler();
}
return this.uniqueInstance;
}
public fill(): void {
if (this.isEmpty()) {
this.empty = false;
this.boiled = false;
// 보일러에 우유와 초콜릿을 혼합한 재료를 넣음
}
}
public drain(): void {
if (!this.isEmpty() && this.isBoiled()) {
// 끓인 재료를 다음 단계로 넘김
this.empty = true;
}
}
public boil(): void {
if (!this.isEmpty() && !this.isBoiled()) {
// 재료를 끓임
this.boiled = true;
}
}
public isEmpty(): boolean {
return this.empty;
}
public isBoiled(): boolean {
return this.boiled;
}
}
class Database {
private static instance: Database;
private connection: any;
private constructor() {
this.connection = null;
}
public static getInstance(): Database {
if (!Database.instance) {
Database.instance = new Database();
}
return Database.instance;
}
connect() {
if (!this.connection) {
this.connection = { status: 'connected' };
console.log('DB 연결됨');
}
}
}
// 사용
const db1 = Database.getInstance();
const db2 = Database.getInstance();
console.log(db1 === db2); // true
class Database {
private static instance: Database;
private connection: any;
private constructor() {
this.connection = null;
}
public static getInstance(): Database {
if (!Database.instance) {
Database.instance = new Database();
}
return Database.instance;
}
connect() {
if (!this.connection) {
this.connection = { status: 'connected' };
console.log('DB 연결됨');
}
}
}
장점: 전통적이고 명확함
사용 시기: 일반적인 싱글톤 구현
const UserManager = (() => {
let instance: any;
function createInstance() {
return {
users: [] as string[],
addUser(name: string) {
this.users.push(name);
},
getUsers() {
return this.users;
},
};
}
return {
getInstance() {
if (!instance) {
instance = createInstance();
}
return instance;
},
};
})();
// 사용
const userMgr1 = UserManager.getInstance();
const userMgr2 = UserManager.getInstance();
console.log(userMgr1 === userMgr2); // true
장점: 클로저를 활용한 완벽한 캡슐화
사용 시기: private 변수가 필요할 때
const AppConfig = {
apiUrl: 'https://api.example.com',
timeout: 5000,
setApiUrl(url: string) {
this.apiUrl = url;
},
getApiUrl() {
return this.apiUrl;
},
};
// 객체 리터럴은 그 자체로 싱글톤이며, 추가 인스턴스 생성 불가
장점: 가장 간단하고 직관적
사용 시기: 간단한 설정이나 상수 관리
class SessionManager {
private sessionData: Map<string, any> = new Map();
setSession(key: string, value: any) {
this.sessionData.set(key, value);
}
getSession(key: string) {
return this.sessionData.get(key);
}
clearSession() {
this.sessionData.clear();
}
}
// 모듈에서 단 하나의 인스턴스만 생성하고 export
export const sessionManager = new SessionManager();
// 다른 파일에서: import { sessionManager } from './session';
// ES6 모듈은 캐싱되므로 항상 같은 인스턴스를 반환
장점: 가장 자연스럽고 JavaScript다운 방식
사용 시기: 대부분의 실무 상황
|
두 스레드에서 getInstance()를 동시에 호출하면 인스턴스가 두 개 이상 생성될 수 있음
JavaScript는 기본적으로 단일 스레드이므로 synchronized 키워드가 필요 없음
하지만 Web Workers를 사용하면 각 워커마다 별도 인스턴스가 생성될 수 있으니 주의
class ChocolateBoiler {
// 클래스 로딩 시점에 즉시 생성
private static uniqueInstance = new ChocolateBoiler();
private constructor() {}
public static getInstance(): ChocolateBoiler {
return this.uniqueInstance;
}
}
장점: 스레드 안전 보장
단점: 사용하지 않아도 인스턴스가 생성됨
다른 언어에서 사용하는 기법이지만, JavaScript에서는 단일 스레드 특성상 불필요
export const config = {
apiUrl: process.env.API_URL,
apiKey: process.env.API_KEY,
timeout: 5000,
};
Redux Store, Vuex Store 등
싱글톤 패턴은 다음 디자인 원칙을 따른다:
- 클래스가 자신의 인스턴스를 관리하도록 한다
- 전역 접근 지점을 제공하되, 무분별한 전역 변수 사용을 방지한다
테스트의 어려움
의존성 숨김
멀티스레드 환경
과도한 사용 지양
전역 상태 관리