Singleton : 오직 하나의 객체만을 생성할 수 있는 클래스
javaScript로 된 singleton을 보며 이해를 해봅시다.
const obj = {
a: 1
};
const obj2 = {
a: 1
};
console.log(obj === obj2)
// false
위의 console.log 결과로 obj와 obj2가 다른 인스턴스를 가진다는 것을 알 수 있습니다.
class Singleton {
constructor() {
if (!Singleton.instance) {
Singleton.instance = this
}
return Singleton.instance
}
getInstance() {
return this.instance
}
}
const a = new Singleton()
const b = new Singleton()
console.log(a === b) // true
다음과 같이 싱글톤을 활용하였을 때 얻는 장점과 단점은 무엇이 있을지 살펴보시죠.
1번 단점에 대해 더 알아보겠습니다.
싱글톤 패턴은 서로 다른 모듈에서 같은 싱글톤 객체를 참조하거나 사용하려면 싱글톤 클래스에 의존해야 합니다. 이러한 모듈들이 쌓이면 모듈간에 결합이 강하게 만들어집니다.
->
이렇게 되면 하나의 모듈이 변경될 때 다른 모듈에 영향을 미치게 됩니다.
이는 유지보수와 확장성을 저하시키는 요인 중 하나입니다.
따라서 의존성 주입을 통해 모듈간의 결합을 어느정도 느슨하게 만들 수 있습니다.
의존성 주입은 객체간의 의존성을 낮추기 위한 하나의 디자인 패턴입니다.
의존성 주입은 외부에서 의존하는 객체를 전달받아 사용하는 방식입니다. 즉, 객체가 필요로 하는 의존성을 객체 생성자나 setter 메서드 등을 통해 외부에서 전달받아 사용하도록 하는 것입니다.
이해가 잘 되지 않으니 javaScript의 예시 코드를 보며 자세히 살펴봅시다.
class Singleton {
constructor(logger) {
if (Singleton.instance) {
return Singleton.instance;
}
this.logger = logger;
this.logLevel = 'DEBUG';
Singleton.instance = this;
}
static getInstance(logger) {
if (!Singleton.instance) {
Singleton.instance = new Singleton(logger);
}
return Singleton.instance;
}
setLogLevel(logLevel) {
this.logLevel = logLevel;
}
doSomething() {
// 로그 레벨이 INFO 이상일 때만 로그를 남기고, 그 외에는 로그를 남기지 않음
if (this.logLevel === 'INFO' || this.logLevel === 'DEBUG') {
this.logger.log(`Doing something with log level ${this.logLevel}`);
}
// 실제로 무언가를 수행하는 코드
console.log('Doing something');
}
}
class Logger {
constructor() {
// 실제로는 외부 서비스와 연결하여 로그를 기록하게 됨
this.logs = [];
}
log(message) {
this.logs.push(message);
console.log(`[Logger] ${message}`);
}
}
const logger = new Logger();
const singletonInstance1 = Singleton.getInstance(logger);
const singletonInstance2 = Singleton.getInstance(logger);
// true
console.log(singletonInstance1 === singletonInstance2);
singletonInstance1.doSomething();
singletonInstance2.setLogLevel('INFO');
singletonInstance2.doSomething();
조금 복잡하지만 위의 코드를 보겠습니다.
전반적인 구조는 Singleton이라는 싱글톤에 Logger라는 의존성을 주입한 것입니다.
만약 Logger라는 인터페이스 없이
const singletonInstance1 = Singleton.getInstance();
const singletonInstance2 = Singleton.getInstance();
singletonInstance1.doSomething();
singletonInstance2.setLogLevel('INFO');
singletonInstance2.doSomething();
이 코드를 실행했으면 어떻게 됐을까요?
singletonInstance1의 setLogLevel객체 또한 'INFO'로 변경되어 있었을 것입니다.
하지만 Logger라는 의존성을 주입한 결과
console.log(logger)
// (2) ["Doing something with log level DEB...]
// 0: "Doing something with log level DEBUG"
// 1: "Doing something with log level INFO"
이렇게 출력되게 됩니다.
의존성 주입의 장 단점에 대해 알아봅시다.
모듈들 간의 의존성이 약해지기 때문에 모듈들을 쉽게 교체할 수 있게 됩니다.
따라서 테스팅, 마이그레이션이 쉬워집니다.
모듈들이 분리가 되기 때문에 클래수 수가 늘어나 복잡성이 증가할 수 있습니다.
또한 약간의 런타임 패널티도 생기게 됩니다.