Singleton이라는 의미는 전체 앱에서 해당 객체는 유일해야 함을 보장하고 싶을 때 만드는 패턴이다.
클래스는 다른 클래스를 상속할 수 있는 기능인 extends라는 문을 가지고 있다. 이 extends의 기능을 정확하게 알아야만 싱글톤이 어떤 장점을 가지고 있는지를 이해할 수 있다.
일단 예시로 두가지의 자료를 만들어본다
class SingletonBase {
static instance;
constructor() {
if (!SingletonBase.instance) {
SingletonBase.instance = this;
} else {
console.log(" already has instance.");
console.log(" return existing instance.");
}
return SingletonBase.instance;
}
}
class Derived extends SingletonBase {
constructor() {
super();
this.config = {
host: "localhost",
user: process.env.DATABASE_USERNAME || "root",
password: process.env.DATABASE_PASSWORD || "",
database: process.env.DATABASE_NAME || "learnmysql",
};
}
}
const singleton = new Drived();
우선 일반적으로 생성자 함수가 new라는 연산자를 받으면, 자바스크립트 엔진은 메모리에 저장할 임시객체를 하나 만든다. ( this 바인딩을 할 )
그리고 나서 이 객체를 constructor을 통해 초기화한다.
하지만 extends가 들어가면 과정이 살짝 달라지게 된다.
생성자 함수이자 객체인 class Derived는 이미 컴파일 시점에서, 즉 이 해당 클래스 함수를 해석하여 객체로 만들 때에 이미
1. prototype 프로퍼티
2. [[Prototype]] 슬롯의 정의를 완료한다.
[[Prototype]] 슬롯에 들어가는 것은 객체로, 일반적인 경우 Object.prototype이라고 하는 빌트인 생성자의 프로토타입 프로퍼티를 슬롯으로 넣고 프로퍼티들로 클래스 내부에 메서드 형태로 정의한 함수들과 static 상태값등이 들어가있는 상태이다.
즉 간단히 말해, 저 class Derived 가 함수객체로 평가될 시점에는 프로퍼티에 이미
{
prototype : {
method1,
method2,
...
[[Object.prototype]]
}
}
형태의 프로토타입 프로퍼티가 평가시점에 존재한다.
자 그럼 런타임 시점이 되어 이제 new 연산자를통해 임시 인스턴스가 전달되었다. 그런데 여기서 중요한점은, new를 호출한 상대가 바로 Derived 클래스가 아닌 그 안에 extends 로 이미 컴파일로 토큰 파악 시점에 확인되었던 수퍼클래스인 SingleToneBase 클래스가 위임해서 실행하고 있다는 점이다.
참고로 new.target을 함수 내부에서 조회해서 해당 함수가 생성자 함수로서 호출된건지 아닌지를 파악할 수 있다. 기본적으론 undefined이다
new는 연산자이므로 객체가 아니다. 해당 내용은 함수가 호출되면서 실행 컨텍스트를 만들 때 암묵적으로 만드는 변수값중에 하나이다.
자 그럼 이제 자바스크립트 엔진은 이 대상이 상속하는 클래스가 존재한다는 것을 확인했으므로, constructor의 super을 실행하여 new 연산자를 통한 암묵적인 더미객체 생성을 상속 클래스에게 넘긴다 ( 위에서는 SingletonBase가 된다 )
이 싱글톤 클래스는 일반 클래스때와 같이, new를 통해 임시 인스턴스를 만들었고, 이 인스턴스에 this를 바인딩한다.
자 그럼 이 싱글톤 클래스 내에서 가리키는 this 는 new를 통해 만들어진 임시 인스턴스 객체이고, 싱글톤 클래스는 constructor을 통해 이 this를 초기화하는 작업을 진행한다 (상태든, 메서드든)
그 후, 초기화한 객체의 [[prototype]] 슬롯에 들어갈 객체로 Singleton.prototype이 아닌 Derived 생성자에서 정의되어 있는 prototype 프로퍼티가 들어간다
new.target을 콘솔로 찍어보면 class Derived extends Singleton이라는 문구를 볼 수 있다.
처음 생성자 함수가 평가되면서 객체화할때, extends기호가 존재한다면 prototype 프로퍼티에 contructor은 Derived가 되지만, [[prototype]] 슬롯에 들어가는 내용은 extends했던 SingletonBase의 prototype 프로퍼티가 된다.
그 후 SingletonBase 는 초기화한 객체를 Derived에게 넘기고 남은 초기화작업이 진행되게 된다
싱글톤 패턴은 그 전에 static으로 해당 자기 자신의 constructor까지 초기화가 완료된 인스턴스를 할당한다.
이 행동이 싱글톤 패턴의 의미를 만들어낸다. 예를들어,
constructor() {
static instance;
if (!SingletonBase.instance) {
SingletonBase.instance = this;
} else {
console.log(" already has instance.");
console.log(" return existing instance.");
}
return SingletonBase.instance;
}
SingletonBase의 constructor을 살펴보면, static으로 instance 값을 정의하고 있다.
static은 생성자 함수객체가 평가되는 순간 프로퍼티로 주입되는 값으로, 생성자 함수가 초기화를 하지 않더라도 사용이 가능하다.
현재는 undefined이지만, 만약 단 한번이라도 Derived를 통해 객체가 생성된다면 위에서 설명했던 new 의 더미객체 생성 위임에 따라
if (!SingletonBase.instance) {
SingletonBase.instance = this;
}
이 부분이 실행되면서 Singleton 생성자의 프로퍼티안에 instance의 값이 할당되게 된다.
Singleton이라는 의미는 유일한 객체를 보장해야 한다고 했었다.
class SingletonBase {
static instance;
constructor() {
if (!SingletonBase.instance) {
SingletonBase.instance = this;
} else {
console.log(" already has instance.");
console.log(" return existing instance.");
}
return SingletonBase.instance;
}
}
위와같이 static으로 정의된 값이 있고, 만약 단 한번이라도 Derived를 통해 객체가 생성이 된다면
Singleton 함수객체의 instance 프로퍼티에는 만들어져 저장된 this가 할당되게 된다.
만약 다음번에 Derived 생성자를 통해서 함수 객체를 만든다 하더라도 이미 instance에 초기화가 완료된 this가 할당되어 있으므로 그것을 그대로 리턴하는 방식으로 유일성을 유지한다.