클래스는 new
키워드를 사용하여 인스턴스화 시킬 수 있으며, new
를 사용할때마다 말 그대로 새로운 인스턴스를 생성한다. 하지만 경우에 따라서 인스턴스를 단 하나만 생성하여 재사용 하고 싶은 때도 있다.
예를들어, 클래스 안에 프로그램 내 여러 곳에서 공용으로 접근하는 자원을 저장하는 경우가 있을 수 있다.
Singleton 패턴은 비교적 단순하게 Singleton이 될 클래스 하나만 있으면 된다. 단, 생성된 인스턴스를 얻기위한 static메소드를 구현하는 것이 일반적이며 생성자를 private으로 처리하여 안정성을 더할 수 있다.
class Singleton {
private static instance: Singleton;
private count = 0;
private constructor() {
console.log('Singleton created');
}
public static getInstance(): Singleton {
if (!Singleton.instance) {
Singleton.instance = new Singleton();
}
return Singleton.instance;
}
public addCount(): number {
return this.count++;
}
}
Singleton
의 인스턴스는 오로지 getInstance
메소드를 통해서만 얻을 수 있으며, 생성자를 사용해 직접 인스턴스를 생성하려하는 경우 에러가 발생한다.
const s1 = new Singleton();
// Constructor of class 'Singleton' is private and only accessible within the class declaration.ts(2673)
그리고 자바스크립트에서는 자바처럼 생성된 인스턴스를 단순히 s1 == s2
로 비교할 수가 없기 때문에 프로퍼티가 공유되는지를 addCount
메소드로 확인을 해보자.
const s1 = Singleton.getInstance();
const s2 = Singleton.getInstance();
console.log(s1.addCount());
console.log(s2.addCount());
Singleton created
0
1
생성자에서 console.log
를 한번만 출력하고, 또 s1
으로 호출한 addCount
메소드에 의해 s2
에서 호출할 때 영향을 받은 것을 확인 할 수 있다.
하지만 위 코드는 지극히 자바스러운 코드다.
자바스크립트에서는 모듈을 활용해 생성한 인스턴스만을 export
하는 방식도 흔하게 쓰인다.
// singleton.ts
class Singleton {
private count = 0;
constructor() {
console.log('Singleton created');
}
public getCount(): number {
return this.count++;
}
}
export const singleton = new Singleton();
이렇게 생성한 인스턴스만을 모듈로서 내보내게된다면, 외부에서는 어차피 클래스의 생성자에 접근할 방법이 없다. 예를들어, 두 개의 서로 다른 모듈에서 singleton
을 불러와 사용한다해도 이는 처음 singleton.ts
라는 모듈이 실행되면서 생성한 인스턴스를 그대로 사용할 뿐이다.
// test1.ts
import { singleton } from "./singleton";
import "./test2";
console.log("Singleton");
console.log(singleton.getCount());
// test2.ts
import { singleton } from "./singleton";
console.log("GOF");
console.log(singleton.getCount());
Singleton created
GOF
0
Singleton
1
이렇게 두번 import
를 하여도 인스턴스는 한번만 생성되고, 같은 인스턴스가 사용되는 것을 볼 수 있다.
Java언어로 배우는 디자인 패턴 입문 - 쉽게 배우는 Gof의 23가지 디자인패턴 (영진닷컴)
class Singleton {
private count = 0;
constructor() {
console.log('Singleton created');
}
public getCount(): number {
return this.count++;
}
}
export const singleton = new Singleton();
export
를 이용해 모듈화하였기 때문에 프로젝트 내에서 싱글톤으로서 동작
장점
단점
getInstance
메소드를 이용해 싱글톤 인스턴스 컨트롤
장점
getInstance
를 호출하기 전까지 생성되지 않는다.getInstance
를 호출하는 시점에 로깅, 트랙킹 등 혹은 기타 인스턴스 조작을 유동적으로 할 수 있다.단점
class Singleton {
private static instance: Singleton;
static getInstance(): Singleton {
if (this.instance === undefined) {
this.instance = new this()
}
return this.instance;
}
}
위 static method를 쓰는 방법은 타입추론이 잘 되지 않는다.
예를들어,
class Singleton {
private static instance: Singleton
static getInstance(): Singleton {
if (!this.instance) {
this.instance = new this()
}
return this.instance
}
}
class User extends Singleton {
constructor() {
super();
console.log('User constructor');
}
getName() {
return 'user'
}
}
const user = User.getInstance();
타입스크립트 컴파일러는 user
가 User
가 아닌 Singleton
으로 타입을 추론한다.
Singleton
클래스에 제네릭을 추가해 타입추론을 유도할 수 있다.
class Singleton<T extends Singleton<T>> {
private static instances: Record<string, any> = {};
static getInstance<T extends Singleton<T>>(this: new () => T): T {
const className = this.name;
if (!Singleton.instances[className]) {
Singleton.instances[className] = new this();
}
return Singleton.instances[className];
}
}
class User extends Singleton<User> {
constructor() {
super();
console.log('User constructor');
}
getName() {
return 'user';
}
}
const user = User.getInstance();
여기서,
<T>
대신 <T extends Singleton<T>>
로 함으로서 T
는 항상 Singleton
을 상속받는 클래스임을 명시할 수 있다.private static instances: Record<string, any> = {};
로 Record 형태로 타입핑을 한 것은, static 속성에는 제네릭을 사용하지 못하기 때문이다
Static members cannot reference class type parameters.ts(2302)
static getInstance<T extends Singleton<T>>(this: new () => T): T {
에서 매개변수에 this: new () => T
를 넣음으로서, getInstance
메소드를 호출하는 클래스가 Singleton
을 상속받는 클래스 T
임을 암시한다.