하나의 클래스에 오직 하나의 인스턴스만 가지는 패턴이다.
class Singleton {
constructor() {
if (!Singleton.instance) {
Singleton.instance = this;
}
return Singleton.instance;
}
getInstance() {
return this;
}
}
const a = new Singleton();
const b = new Singleton();
console.log(a === b); // true
a와 b는 하나의 인스턴스를 가진다.
인스턴스가 존재할 경우 에러를 던져주는 것으로 인스턴스를 하나만 생성할 수 있게한다.
let counter = 0;
class Counter {
constructor() {
if (Counter.instance) {
throw new Error("You can only create one instance!");
}
Counter.instance = this;
}
getInstance() {
return this;
}
getCount() {
return counter;
}
increment() {
return ++counter;
}
decrement() {
return --counter;
}
}
const counter1 = new Counter();
const counter2 = new Counter();
// Error: You can only create one instance!
Counter 인스턴스를 export 하기 전에 인스턴스를 freeze 하도록 한다.
class Counter {
...
}
const singletonCounter = Object.freeze(new Counter());
export default singletonCounter
Object.freeze 메서드는 객체를 사용하는 쪽에서 직접 객체를 수정할 수 없도록 해준다.
freeze 처리 된 인스턴스는 프로퍼티의 추가 및 수정이 불가하므로 Singleton 인스턴스의 프로퍼티를 덮어쓰는 실수를 예방할 수 있다.
let count = 0;
const counter = {
increment() {
return ++count;
},
decrement() {
return --count;
}
};
Object.freeze(counter);
export { counter };
Singleton pattern은 데이터베이스 연결 모듈에 많이 쓰인다.
const URL = "mongodb://localhost:27017/kundolapp";
const createConnection = (url) => ({ url: url });
class DB {
constructor(url) {
if (!DB.instance) {
DB.instance = this;
}
return DB.instance;
}
connect() {
return this.instance;
}
}
const DB1 = new DB(URL);
const DB2 = new DB(URL);
console.log(a === b);
DB.instance라는 하나의 인스턴스를 기반으로 a, b를 생성한다.
Mongoose.prototype.connect = function (uri, options, callback) {
const _mongoose = this instanceof Mongoose ? this : mongoose;
const conn = _mongoose.connection;
return _mongoose._promiseOrCallback(callback, (cb) => {
conn.openUri(uri, options, (err) => {
if (err != null) {
return cb(err);
}
return cb(null, _mongoose);
});
});
};
다른 모듈로부터 import될 때 Singleton인지 아닌지 분명하지 않기 때문에 여러 Singleton 인스턴스들이 앱에서 공유될 때 직접 수정하게 될 수 있고 예외로 이어질 수 있다.
let, const 키워드들은 변수를 블록 스코프 내에 선언하게 하여, 실수로 전역에 변수를 선언하는것을 예방한다.module 시스템은 export , import 구문으로 전역 객체를 수정하지 않고 모듈 내에서 전역으로 쓸 수 있는 변수를 만들게 해 준다.그러나 Singleton pattern은 일반적으로 앱에 전역 상태를 위해 사용한다. 코드의 여러 부분에서 수정가능한 하나의 객체를 직접 접근하도록 설계하면 예외가 발생하기 쉬워진다.
Singleton pattern은 모듈 간의 결합을 강하게 만들 수 있다는 단점이 있다.
이때, 의존성 주입(DI, Dependency Injection)을 통해 ****모듈 간의 결합을 조금 더 느슨하게 만들어 이를 해결할 수 있다.
위의 그림처럼 메인 모듈이 직접 하위 모듈에 대한 의존성을 주는 것이 아니라, 중간에 의존성 주입자를 도입하여 메인 모듈이 간접적으로 의존성을 주입하게 한다.
이를 통해 메인 모듈은 하위 모듈에 대한 의존성이 떨어지게 된다. 이를 ‘디커플링이 된다’라고 한다.
모듈을 쉽게 교체할 수 있기 때문에 테스팅에 유리하다.
또한 애플리케이션 의존성 방향이 일관되고, 모듈간의 관계들이 조금 더 명확해진다.
모듈들이 더욱 분리되므로 클래스 수가 늘어나 복잡성이 증가될 수 있다.
“상위 모듈은 하위 모듈에서 어떠한 것도 가져오지 않아야 한다.”
“둘 다 추상화에 의존해야 하며, 추상화는 세부 사항에 의존하지 말아야 한다.”
React에서는 전역 상태 관리를 위해 Singleton 객체를 만드는 대신 상태 관리 도구를 사용한다.
Singleton은 인스턴스의 값을 직접 수정할 수 있는 반면에, 언급한 도구들은 읽기 전용 상태를 제공한다.
이를 통해 컴포넌트가 직접 상태를 업데이트하게 두는 것이 아니라 개발자가 의도한대로만 수정되도록 한다.
면접을 위한 CS 전공지식 노트 -주홍철
JS Patterns - https://www.patterns.dev/posts/singleton-pattern