본문 바로가기

Computer Science/Design Pattern

<디자인 패턴> 싱글톤 패턴

1. 소개

 싱글톤 패턴에 대해 patterns.dev는 다음과 같이 소개하고 있다.

 

Singletons are classes which can be instantiated once, and can be accessed globally. This single instance  can be shared throughout our application, which makes Singletons great for managing global state in an application.

 

 간단히 말해 싱글톤은 단 한번만 인스턴스화되는 클래스이며, 전역상태를 관리하는 데 용이한 패턴이라는 것이다.

 

 

2. 싱글톤

* 개념

 - 앱 전반에서 사용되는 숫자 카운터가 있을 때, 흔히 아래와 같이 작성할 수 있다.

let counter = 0;

class Counter {
  getInstance() {
    return this;
  }

  getCount() {
    return counter;
  }

  increment() {
    return ++counter;
  }

  decrement() {
    return --counter;
  }
}

// ----------------
const counter1 = new Counter();
const counter2 = new Counter();

counter1.increment();
console.log(counter2.getCount()); // 1
console.log(counter); // 1

console.log(counter1.getInstance() === counter2.getInstance()); // false

 

 - counter는 전역변수로써 공유되고 있다. getInstance 를 통해 클래스가 생성하는 인스턴스 객체를 꺼낼 수 있도록 하였으나, 서로 다른 인스턴스를 갖게 되는 것을 볼 수 있다.

 

 - 따라서 싱글톤의 정의에 맞지 않으며 불필요하게 또 다른 객체를 생성하는 꼴이다.

let instance;
let counter = 0;

class Counter {
  constructor() {
    if (instance) {
      throw new Error("You can only create one instance!");
    }
    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!

 

 - 여러가지 방법 중 하나는 임의의 변수(instance)를 선언하고, 이를 활용하여 다른 인스턴스를 생성하지 못하도록 강제하는 것이다. 이제 이 싱글톤 인스턴스를 외부에서 변경하지 못하도록 freeze하고 export하면 완성이다.

 

const singletonCounter = Object.freeze(new Counter());
export default singletonCounter;

 

 - 이제 우리는 plusButton.js 에서든 minusButton.js 에서든 해당 객체를 불러와서 incrementdecrement 를 호출하여 전역 카운터를 관리할 수 있게되었다.

 

* 직접 구현

 - 필자 개인적인 견해로는 싱글톤 클래스 바깥에 변수가 있는 것이 마음에 들지 않는다. 클래스 하나가 완전한 역할을 할 수 있도록 최신 문법으로 만들어 보자.

export class Counter {
  constructor() {
    this.counter = 0;
  }

  static getInstance() {
    if (!this.instance) {
      this.instance = new this();
    }
    return this.instance;
  }

  getInstance() {
    return this.instance;
  }

  getCount() {
    return this.counter;
  }

  increment() {
    return ++this.counter;
  }

  decrement() {
    return --this.counter;
  }
}

const counter1 = Counter.getInstance();
const counter2 = Counter.getInstance();
console.log(counter1.getInstance() === counter2.getInstance()); // true
counter1.increment();
console.log(counter2.getCount()); // 1

 

 - 위와 같이 작성할 경우 class 를 직접 export해서 사용할 수 있고, 직관적이다.

 

 - static 메서드를 사용하여 클래스에서 직접 인스턴스를 생성 또는 Get 할 수 있는 기능을 제공하고, 일반 getInstance 는 기존과 동일한 역할을 하도록 하였다.

 

 - 물론 이 클래스는 new 키워드로 객체를 생성하면 안된다는 제한점이 있기는 하다.

 

* 단점

 - 싱글톤은 메모리 절약에 유리하지만 자바스크립트에서는 안티패턴으로 여겨진다.

 

 - 대부분의 Java나 C++ 같은 객체지향언어는 객체를 만들기 위해 반드시 클래스를 사용해야 한다. 그러나 JS는 객체인스턴스를 얼마든지 만들 수 있고, 이러한 일반 객체를 export하기만 하더라도 위와  같은 작업이 사실 필요 없다.

let count = 0;

const counter = {
  increment() {
    return ++count;
  },
  decrement() {
    return --count;
  }
};

Object.freeze(counter);
export { counter };

 

 - counter는 JS에서 객체이며 참조값이기 때문에 다른 파일에서 import하더라도 동일한 참조를 사용하여 공유한다. 즉 JS에서 싱글톤 패턴은 과한 코드 낭비다.

 

 - 또 다른 단점으로는 테스트가 있다. 싱글톤에 의존적인 코드는 하나의 인스턴스로 관리되므로 모든 테스트가 의존적일 수 밖에 없다. 이를 극복하기 위해 테스트 케이스 하나하나마다 인스턴스를 리셋하도록 해야한다.

 

 - 그 이에도 실수로 직접적으로 싱글톤을 변경할 경우 어플리케이션 전체에 영향을 미칠 수 있다.

 

 - 프론트엔드의 메인 프레임워크라 할 수 있는 리액트의 경우에는 싱글톤을 사용하는 대신 Redux나 React Context와 같은 전역상태에 의존한다. 이는 싱글톤처럼 가변상태가 아닌 읽기 전용 상태만들 제공한다. 변경이 일어나야 할 경우는 어떻게 하는가. 이들은 컴포넌트들이 디스패처 액션을 전송하고, 이를 이용한 순수함수(리듀서)만이 상태를 업데이트할 수 있도록 하였다. 순수함수이므로 직접 글로벌 상태를 갱신하지 않으므로 우리가 원하는대로 상태를 조작할 수 있게 된다.

 


참고

 

  본 포스팅은 patterns.dev에서 디자인 패턴을 공부하고 개인적으로 필요한 내용을 추가 및 정리한 글입니다. 프론트엔드 위주의 설명이 되어 있으며, 언어는 JavaScript를 사용합니다.

 

 

Singleton Pattern

Share a single global instance throughout our application

www.patterns.dev