Singleton Pattern 에 대하여

"디자인 패턴"이란 마치 레고 블록 조립 설명서처럼, 프로그램을 만들 때 자주 발생하는 문제들을 해결하기 위해 개발자들이 오랫동안 사용해온 설계 지침이라고 얘기할 수 있습니다.
오늘은 다양한 디자인 패턴 중, 싱글톤 패턴에 대해 알아보도록 하겠습니다.
싱글톤 패턴(Singleton Pattern)
싱글톤 패턴은 특정 클래스의 인스턴스를 단 하나만 생성하여 시스템 전체에서 공유하는 디자인 패턴입니다.
즉, 하나의 클래스를 기반으로 여러 개의 객체를 만들 수 있는 일반적인 객체지향 프로그래밍의 방식과 달리, 싱글톤 패턴에서는 의도적으로 단 하나의 객체만 생성하여 사용합니다.
이렇게 생성된 단 하나의 객체는 시스템 전체에서 공유되므로, 메모리 효율성을 높이고 전역적인 상태를 관리하는 데 유용하게 활용됩니다.
물론 그의 반대 급부로 의존성이 높아진다는 단점은 가질 수 밖에 없겠죠.
JS 에서의 싱글톤
객체 리터럴 또는 new Object() 로 생성된 객체는 고유하다
자바스크립트의 객체는 프로토타입 기반으로 생성되기 때문에, 같은 생성자를 사용하더라도 서로 다른 객체를 생성할 수 있습니다.
즉, 리터럴 {} 이나 new Object() 를 사용하여 생성된 객체는 메모리 상의 다른 공간에 할당되어 서로 다른 객체로 취급됩니다.
const obj = {
name: 'do-not-do-that'
};
const obj2 = {
name: 'ghost'
};
console.log(obj === obj2); // false
보이듯, obj 와 obj2 는 다른 인스턴스를 가집니다.
이 또한 new Object 라는 클래스에서 나왔기 때문에 어느정도 싱글톤 패턴이라 부를 수는 있지만, 실제 개발자들이 흔히 말하는 싱글톤 패턴은 보통 아래와 같이 구성되는 경우가 많습니다.
class Singleton {
constructor(){
if(!Singleton.instance) {
Singleton.instance = this;
}
return Singleton.instance;
}
getInstance(){
return this;
}
}
const obj = new Singleton();
const obj2 = new Singleton();
console.log(obj === obj2); // true
위 코드와 아래 코드의 차이점은,
객체의 생성을 제어하고, 단 하나의 인스턴스만을 반환하는 매커니즘이 존재하냐/하지않냐의 차이입니다.
데이터베이스 연결 모듈
싱글톤 패턴은 데이터베이스 연결 모듈에 많이 쓰입니다.
const URL = 'mongodb://localhost:27017/test';
const createConnection = url => ({"url": url});
class DB {
constructor(url) {
if (!DB.instance) {
DB.instance = createConnection(url);
}
return DB.instance;
}
connect(){
return this.instance;
}
}
const obj = new DB(URL);
const obj2 = new DB(URL);
console.log(obj === obj2); // true
하나의 DB 인스턴스를 기반으로 obj, obj2 를 생성할 수 있습니다.
Mongo 에서의 싱글톤
위의 데이터베이스 예제에 이어서,
Node.js 에서 MongoDB 데이터베이스를 연결할 때 쓰는 mongoose 모듈을 살펴보도록 하겠습니다.
mongoose 에서 데이터베이스를 연결하려면, connect() 함수를 호출하면 됩니다.
이 함수가 반환하는 인스턴스는 싱글톤 인스턴스 입니다.
connect() 함수 구현을 살펴봅시다.
Mongoose.prototype.connect = async function connect(uri, options) {
if (typeof options === 'function' || (arguments.length >= 3 && typeof arguments[2] === 'function')) {
throw new MongooseError('Mongoose.prototype.connect() no longer accepts a callback');
}
const _mongoose = this instanceof Mongoose ? this : mongoose;
if (_mongoose.connection == null) {
_createDefaultConnection(_mongoose);
}
const conn = _mongoose.connection;
return conn.openUri(uri, options).then(() => _mongoose);
};
위 connect 메서드는 싱글톤 패턴을 이용해 단 하나의 Mongoose 연결만 생성하도록 구현되어있습니다.
Mongoose 는 클래스 기반이 아닌 모듈 기반으로 설계되어 있어 아까 class 로 구현된 싱글톤과는 조금 다른 형태로 보입니다.
하지만 mongoose.connection 이라는 전역적인 객체를 통해 단 하나의 인스턴스만을 관리하는 방식은 클래스 기반의 싱글톤 패턴과 유사한 역할을 하고 있음을 알 수 있습니다.
mongo 말고 다른 데이터베이스도 살펴볼까요?
MySQL의 싱글톤
Node.js 에서 MySQL 데이터베이스를 연결할 때도 싱글톤 패턴이 사용됩니다.
// 메인 모듈
const mysql = require('mysql');
const pool = mysql.createPool({
host: '',
user: '',
password: '',
database: '',
connectionLimit: 7
});
pool.connect();
// user 모듈
pool.query(query, function (err, results, fields) {
if(err) throw error;
...
})
// post 모듈
pool.query(query, function (err, results, fields) {
if(err) throw error;
...
})
pool 변수는 단 하나의 MySQL 연결 풀을 나타내고, 애플리케이션 전체에서 이를 공유해 사용합니다.
싱글톤 패턴의 단점
싱글톤 패턴은 테스트할 때 상당히 큰 애로사항을 불러 일으킵니다.
싱글톤 패턴은 미리 생성된 하나의 인스턴스를 기반으로 구현되는 패턴인데, 제각각 기능을 하면서 서로 독립적으로 실행되는 단위 테스트의 경우 상당히 곤란해집니다.
이를 해결하려면, 각 테스트마다 필요한 인스턴스를 각각 만들어주는 코드를 다시 또 작성해줘야하는 등의 불편함이 수반될 수 있습니다.
의존성 주입
싱글톤 패턴은 모듈간 결합도를 높일 수도 있습니다.
이때, 의존성 주입을 통해 모듈간의 결합을 조금 더 느슨하게 만들어줄 수 있는데요.
메인 모듈이 직접 다른 하위 모듈에 대한 의존성을 주는게 아니라, 중간 관리자 급인 dependency injector 를 고용하여 메인 모듈이 간접적으로 의존성을 주입할 수 있게 할 수 있습니다.
이렇게 하면 의존성이 낮아질 수 있겠죠. 디커플링 되는 과정입니다.
오늘 이렇게 싱글톤 패턴에 대해 알아봤습니다.
개인적으로 가장 흔히 접하는 패턴이 아닌가 싶기도 하고, 개발을 하면서 싱글톤 패턴을 차용한 로직을 많이 접했기 때문에 꼭 알아둬야하는 패턴 중 하나라고 생각합니다.
질문이 있다면 언제든 댓글 남겨주세요.