Singleton Pattern 에 대하여

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 모듈을 살펴보도록 하겠습니다.

GitHub - Automattic/mongoose: MongoDB object modeling designed to work in an asynchronous environment.
MongoDB object modeling designed to work in an asynchronous environment. - Automattic/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 를 고용하여 메인 모듈이 간접적으로 의존성을 주입할 수 있게 할 수 있습니다.

이렇게 하면 의존성이 낮아질 수 있겠죠. 디커플링 되는 과정입니다.

오늘 이렇게 싱글톤 패턴에 대해 알아봤습니다.

개인적으로 가장 흔히 접하는 패턴이 아닌가 싶기도 하고, 개발을 하면서 싱글톤 패턴을 차용한 로직을 많이 접했기 때문에 꼭 알아둬야하는 패턴 중 하나라고 생각합니다.

질문이 있다면 언제든 댓글 남겨주세요.