카테고리 없음

✨싱글톤 패턴 (Singleton Pattern) 이란?

lamarcK 2025. 3. 29. 08:48

싱글톤 패턴은 소프트웨어 디자인 패턴 중 생성 패턴(Creational Pattern)의 하나로, 특정 클래스의 인스턴스(Instance)오직 하나만 생성되도록 보장하고, 그 인스턴스에 대한 전역적인 접근점(Global Point of Access)을 제공하는 패턴을 의미한다.

즉, 애플리케이션이 시작될 때 어떤 클래스가 최초 한 번만 메모리를 할당(Static)하고 그 메모리에 인스턴스를 만들어 사용하는 디자인 패턴이다.

📌 싱글톤 패턴의 목적 및 사용 이유

  1. 유일한 인스턴스 보장: 시스템 전체에서 특정 클래스의 인스턴스가 단 하나만 존재해야 하는 경우에 사용된다. 예를 들어, 시스템의 환경 설정을 관리하는 클래스, 로깅(Logging) 작업을 처리하는 클래스, 데이터베이스 연결 풀(Connection Pool) 등은 여러 인스턴스가 존재하면 상태가 꼬이거나 리소스가 낭비될 수 있다. 싱글톤은 이를 방지하고 유일성을 보장한다.
  2. 전역 접근성 및 리소스 공유: 생성된 단일 인스턴스에 대해 어디서든 쉽게 접근할 수 있는 방법을 제공한다. 이를 통해 인스턴스를 여기저기 전달할 필요 없이 필요할 때 전역 접근점(주로 정적 메서드)을 통해 바로 사용할 수 있으며, 공유 리소스(예: DB 커넥션)를 효율적으로 관리할 수 있다.
  3. 메모리 절약: 최초 한 번의 new 연산자를 통해서만 객체를 생성하므로, 이후에는 생성된 객체를 계속 반환받아 사용함으로써 메모리 낭비를 방지할 수 있다.

💾 싱글톤 패턴의 일반적인 구현 방식 (개념)

싱글톤 패턴을 구현하는 전형적인 방법은 다음과 같은 요소들을 포함한다.

  1. private 생성자: 외부에서 new 키워드를 사용하여 임의로 인스턴스를 생성하는 것을 막기 위해 생성자를 private으로 선언한다.
  2. private static 인스턴스 변수: 클래스 내부에 유일한 인스턴스를 저장하기 위한 private static 변수를 선언한다.
  3. public static 접근 메서드 (예: getInstance()): 외부에서 유일한 인스턴스에 접근할 수 있도록 public static 메서드(관례적으로 getInstance() 라는 이름을 많이 사용)를 제공한다. 이 메서드는 내부적으로 인스턴스가 이미 생성되었는지 확인하고, 생성되지 않았다면 private 생성자를 호출하여 인스턴스를 생성한 후 저장한다 (지연 초기화 - Lazy Initialization). 이미 생성되어 있다면 기존 인스턴스를 반환한다.

✅ 싱글톤 패턴의 장점

  • 클래스의 인스턴스가 단 하나임을 보장한다.
  • 인스턴스에 대한 전역 접근점을 제공하여 접근이 용이하다.
  • 필요할 때만 인스턴스를 생성하는 **지연 초기화(Lazy Initialization)**가 가능하다.
  • 고정된 메모리 영역을 사용하므로 메모리 낭비를 방지할 수 있다.

❌ 싱글톤 패턴의 단점 및 고려사항

  • 전역 상태(Global State): 싱글톤 인스턴스는 사실상 전역 상태를 갖게 되므로, 코드의 여러 부분에서 공유될 때 예상치 못한 부작용(Side Effect)을 일으킬 수 있고 코드 간의 의존성이 높아질 수 있다.
  • 테스트 어려움: 싱글톤 인스턴스에 의존하는 코드는 단위 테스트(Unit Test)를 수행하기 어려울 수 있다. 싱글톤 인스턴스를 Mock 객체로 대체하기 어렵기 때문이다. 이 때문에 의존성 주입(Dependency Injection) 패턴이 더 선호되기도 한다.
  • 단일 책임 원칙(SRP) 위배 가능성: 싱글톤 클래스는 자신의 핵심 로직 외에도 '인스턴스 생성 및 관리'라는 추가적인 책임을 갖게 된다.
  • 유연성 저하: 싱글톤 사용은 코드의 유연성을 떨어뜨릴 수 있다. 나중에 싱글톤이 아닌 여러 인스턴스가 필요하도록 요구사항이 변경될 경우 수정이 어려울 수 있다.
  • 동시성 문제: 멀티 스레드 환경에서는 getInstance() 메서드 내에서 인스턴스를 생성하는 부분에 대한 동기화(Synchronization) 처리를 하지 않으면 여러 인스턴스가 생성될 수 있는 문제가 발생할 수 있다. (Double-checked locking, Initialization-on-demand holder idiom 등의 기법 사용 필요)

✨ 싱글톤 패턴(Singleton Pattern)의 주요 사용처

싱글톤 패턴은 앞서 설명했듯이, 클래스의 인스턴스가 오직 하나만 생성되어야 하고, 그 유일한 인스턴스에 대한 전역적인 접근점이 필요할 때 사용된다. 주로 다음과 같은 경우에 활용되는 경향이 있다.

  1. ⚙️ 설정 관리 (Configuration Management):
    • 애플리케이션 전체에서 공유되어야 하는 환경 설정 정보(예: 서버 주소, API 키, 기본 언어 설정 등)를 관리하는 클래스에 사용될 수 있다. 설정 정보는 한 곳에서 관리되고 모든 부분이 동일한 설정 값을 참조해야 하므로, 유일한 인스턴스를 갖는 것이 자연스럽다. (Configuration 객체)
  2. 📄 로깅 (Logging):
    • 시스템의 여러 부분에서 발생하는 이벤트나 오류를 기록하는 로거(Logger) 클래스에 자주 사용된다. 모든 로그 메시지를 하나의 로거 인스턴스를 통해 중앙에서 관리하고, 특정 파일이나 출력 스트림에 순차적으로 기록하도록 제어해야 하기 때문이다. 여러 로거 인스턴스가 동일한 파일에 동시에 접근하려고 하면 문제가 발생할 수 있다. (Logger 객체)
  3. 🗄️ 데이터베이스 연결 풀 (Database Connection Pool):
    • 데이터베이스 연결은 생성 비용이 비싼 리소스다. 따라서 미리 일정 개수의 연결을 만들어두고(풀링, Pooling) 필요할 때 빌려 쓰고 반납하는 방식으로 효율성을 높인다. 이 연결 풀을 관리하는 객체는 시스템 전체에서 단 하나만 존재하여 연결을 중앙에서 관리하고 제어해야 한다. 여러 개의 풀 관리자가 있다면 연결 관리가 비효율적이고 복잡해진다. (Connection Pool 매니저)
  4. 🧵 스레드 풀 (Thread Pool) 또는 작업 큐 (Task Queue):
    • 애플리케이션 전반에서 사용될 작업 스레드를 관리하는 스레드 풀이나, 비동기 작업을 처리하기 위한 작업 큐를 관리하는 객체 역시 싱글톤으로 구현될 수 있다. 리소스(스레드, 큐 공간)를 효율적으로 공유하고 관리하기 위해 중앙 관리 지점이 필요하다.
  5. ⚡️ 캐시 (Cache):
    • 자주 사용되지만 가져오는 데 비용이 드는 데이터(예: DB 조회 결과, 외부 API 응답)를 메모리에 저장해두고 재사용하는 캐시 객체를 싱글톤으로 만들 수 있다. 애플리케이션의 모든 부분이 동일한 캐시 저장소를 참조하여 데이터의 일관성을 유지하고 중복 생성을 막기 위함이다. (Cache 매니저)
  6. 🖥️ 하드웨어 접근 제어:
    • 프린터, 그래픽 카드 등 특정 하드웨어 자원에 대한 접근을 제어하는 클래스를 싱글톤으로 구현할 수 있다. 여러 곳에서 동시에 하드웨어에 접근하려 할 때 발생할 수 있는 충돌을 방지하고 접근 순서나 상태를 관리하기 위해 단일 제어 지점이 필요할 수 있다.

📌 공통적인 목적

위 예시들의 공통점은 시스템 전체에서 공유되어야 하는 자원을 관리하거나, 유일하게 존재해야 하는 시스템 구성 요소를 표현하거나, 작업을 중앙에서 조율해야 하는 경우라는 것이다. 싱글톤은 이러한 상황에서 유일한 인스턴스전역 접근점을 제공함으로써 목적을 달성하도록 돕는다.

⚠️ 주의사항

언급했듯이, 싱글톤 패턴은 이러한 전통적인 사용 사례들이 있지만, 전역 상태를 만들고 테스트를 어렵게 만드는 등의 단점으로 인해 현대적인 개발에서는 사용을 지양하거나 신중하게 접근하는 경향이 있다. 특히 **의존성 주입(Dependency Injection)**과 같은 기법을 사용하면 싱글톤의 장점(객체 재사용, 중앙 관리)을 취하면서도 단점을 상당 부분 해소할 수 있기 때문에 많은 프레임워크와 개발자들이 DI를 선호한다.

따라서 위 사용처들은 "싱글톤이 사용될 수 있는 대표적인 경우"로 이해하되, 실제 적용 시에는 정말로 싱글톤이 최선인지, 대안은 없는지 충분히 고민하는 것이 중요하다.

 

✨ 싱글톤 패턴 코드 예시 (JavaScript)

JavaScript는 Java나 C#과 같이 클래스 생성자를 private으로 선언하는 직접적인 키워드는 없지만, **클로저(Closure)**나 **모듈 패턴(Module Pattern)**을 활용하여 싱글톤 패턴을 구현할 수 있다.

 

다음은 가장 일반적인 방법 중 하나인 모듈 패턴(IIFE - 즉시 실행 함수 표현식 활용) 예시를 보여준다.

📌 모듈 패턴을 사용한 싱글톤 예시

이 방식은 클로저를 이용하여 instance 변수와 초기화 로직(init 함수)을 외부로부터 감추고, 오직 getInstance 메서드만을 노출하여 싱글톤을 구현한다.

const LoggerService = (() => {
  let instance; // 유일한 인스턴스를 저장할 비공개 변수

  // 인스턴스 생성을 담당하는 내부 함수 (생성자 역할)
  function init() {
    // 비공개 멤버 (예: 로그 기록 배열)
    const logs = [];

    // 공개될 메서드 정의
    return {
      log: function(message) {
        const timestamp = new Date().toISOString();
        const logEntry = `${timestamp} - ${message}`;
        logs.push(logEntry);
        console.log(`LOG: ${logEntry}`); // 콘솔에도 출력 (예시)
      },
      getLogs: function() {
        // 로그 기록 전체 반환 (복사본 반환 등 고려 가능)
        return logs;
      },
      getLogCount: function() {
        return logs.length;
      }
    };
  }

  // 외부에 노출될 객체 (getInstance 메서드만 포함)
  return {
    getInstance: function() {
      // 인스턴스가 아직 없으면 생성 (Lazy Initialization)
      if (!instance) {
        console.log("LoggerService 인스턴스를 생성합니다.");
        instance = init(); // init 함수를 호출하여 실제 인스턴스 내용 생성
      } else {
        console.log("기존 LoggerService 인스턴스를 반환합니다.");
      }
      // 항상 동일한 인스턴스 반환
      return instance;
    }
  };
})(); // 즉시 실행 함수 호출

// --- 사용 예시 ---

// getInstance를 통해서만 인스턴스 얻기
const logger1 = LoggerService.getInstance();
const logger2 = LoggerService.getInstance();

// logger1과 logger2는 동일한 인스턴스를 참조한다.
console.log("logger1 === logger2:", logger1 === logger2); // true 출력

// 어느 인스턴스에서든 로그를 남기면 동일한 저장소에 기록된다.
logger1.log("첫 번째 로그 메시지.");
logger2.log("두 번째 로그 메시지 from logger2.");
logger1.log("세 번째 로그입니다.");

// 로그 개수 확인 (어떤 인스턴스에서 확인하든 동일)
console.log("총 로그 개수 (logger1):", logger1.getLogCount()); // 3 출력
console.log("총 로그 개수 (logger2):", logger2.getLogCount()); // 3 출력

// 전체 로그 확인
console.log("전체 로그 기록:", logger1.getLogs());

💾 코드 설명

  1. 즉시 실행 함수 표현식 (IIFE): (() => { ... })(); 구문은 함수를 정의함과 동시에 즉시 실행한다. 이는 내부 변수(instance, init)를 외부 스코프에서 접근할 수 없도록 비공개(Private) 스코프를 만드는 역할을 한다. 클로저의 특성 덕분에 내부에 선언된 instance 변수는 IIFE 실행이 끝나도 사라지지 않고 getInstance 메서드를 통해 계속 접근 가능하다.
  2. instance 변수: IIFE 스코프 내에 선언되어 유일한 싱글톤 인스턴스를 저장한다. 외부에서는 직접 접근할 수 없다.
  3. init() 함수: 실제 싱글톤 객체가 수행할 초기화 로직과 공개(Public) 메서드들을 정의하여 객체로 반환한다. 이 함수는 getInstance 내부에서 단 한 번만 호출된다.
  4. getInstance() 메서드: 외부에 노출되는 유일한 메서드다. 내부적으로 instance 변수를 확인하여,
    • null 또는 undefined이면 (즉, 아직 인스턴스가 생성되지 않았으면) init() 함수를 호출하여 인스턴스를 생성하고 instance 변수에 저장한다. 이를 **지연 초기화(Lazy Initialization)**라고 한다.
    • 이미 instance 변수에 값이 있으면 (즉, 인스턴스가 이미 존재하면) 기존 인스턴스를 그대로 반환한다.
  5. 결과: LoggerService.getInstance()를 몇 번 호출하든 항상 동일한 객체(인스턴스)가 반환되며, 이 객체 내부의 상태(예: logs 배열)는 모든 곳에서 공유된다.

🤔 참고: ES6 클래스 사용 시

ES6 클래스 문법을 사용해서 비슷하게 구현할 수도 있지만, JavaScript 클래스의 생성자는 기본적으로 public이므로 완전한 private 생성자 강제는 조금 더 복잡하거나 # (private class fields) 같은 최신 문법을 사용해야 한다. 모듈 패턴은 전통적으로 JavaScript에서 캡슐화와 정보 은닉을 구현하는 데 널리 사용되어 온 방식이다.

소스 및 관련 콘텐츠