싱글톤 패턴 (Singleton Pattern) for JavaScript

최은우·2023년 4월 13일
0

Design Pattern

목록 보기
2/5
post-thumbnail

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. 자원 관리
    객체를 하나만 생성하므로 자원(메모리 등)의 낭비를 막을 수 있습니다.
  2. 전역 객체
    싱글톤은 전역 객체처럼 동작하므로, 여러 곳에서 접근하여 사용할 수 있습니다.
  3. 상태 유지
    여러 객체에서 공유해야 하는 상태를 유지하기 쉽습니다.
  4. 안정성
    다중 스레드 환경에서도 안정성을 보장할 수 있습니다.

단점

  1. 의존성 문제
    싱글톤 객체에 의존하는 다른 객체들이 있을 경우 의존성 문제가 발생할 수 있습니다.
  2. 테스트 어려움
    TDD(Test Driven Development)에 걸림돌이 됩니다. 싱글톤 객체를 테스트 할 때, 객체가 이미 생성되어 있어 '독립적인' 객체를 만들기가 어려워집니다. 따라서 테스트를 제어하기 어려울 수 있습니다.
  3. 객체의 역할이 명확하지 않음
    객체가 하나만 존재하기 떄문에 그 객체가 어떤 역할을 수행하는지 명확하지 않을 경우 혼란을 야기할 수 있습니다.
  4. 유연성 제한
    싱글톤 객체를 확장하거나 변경하기가 어렵습니다. 일단 생성되면 수정이 불가능하며, 상속을 통한 확장이 어렵습니다.

1번 단점에 대해 더 알아보겠습니다.

싱글톤 패턴은 서로 다른 모듈에서 같은 싱글톤 객체를 참조하거나 사용하려면 싱글톤 클래스에 의존해야 합니다. 이러한 모듈들이 쌓이면 모듈간에 결합이 강하게 만들어집니다.
->
이렇게 되면 하나의 모듈이 변경될 때 다른 모듈에 영향을 미치게 됩니다.
이는 유지보수와 확장성을 저하시키는 요인 중 하나입니다.

따라서 의존성 주입을 통해 모듈간의 결합을 어느정도 느슨하게 만들 수 있습니다.


의존성 주입

의존성 주입은 객체간의 의존성을 낮추기 위한 하나의 디자인 패턴입니다.

의존성 주입은 외부에서 의존하는 객체를 전달받아 사용하는 방식입니다. 즉, 객체가 필요로 하는 의존성을 객체 생성자나 setter 메서드 등을 통해 외부에서 전달받아 사용하도록 하는 것입니다.

이해가 잘 되지 않으니 javaScript의 예시 코드를 보며 자세히 살펴봅시다.

1. Interface Injection (인터페이스 주입)

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"

이렇게 출력되게 됩니다.


의존성 주입의 장 단점에 대해 알아봅시다.

장점

모듈들 간의 의존성이 약해지기 때문에 모듈들을 쉽게 교체할 수 있게 됩니다.
따라서 테스팅, 마이그레이션이 쉬워집니다.

단점

모듈들이 분리가 되기 때문에 클래수 수가 늘어나 복잡성이 증가할 수 있습니다.
또한 약간의 런타임 패널티도 생기게 됩니다.

의존성 주입 원칙

  • 상위 모듈은 하위 모듈에서 어떠한 것도 가져오지 않아야 합니다. 둘 다 추상화에 의존해야 하며, 이때 추상화는 세부 사항에 의존하지 말아야 합니다.

0개의 댓글