Skip to content

싱글톤 패턴(Singleton Pattern)

소프트웨어 설계에서 특정 객체가 애플리케이션 전체에 걸쳐 오직 하나만 존재해야 하는 경우가 있다.
이럴 경우 사용하는 방법이 싱글톤 패턴(Singleton Pattern)이며, 무분별한 인스턴스 생성을 막고 공유 자원을 중앙에서 관리하는 디자인 패턴이다.
해당 글에서는 싱글톤 패턴에 대한 설명과 왜 이 패턴을 언제 써야 하는가?에 대한 내용을 다룬다.

싱글톤 패턴의 단일성

싱글톤 패턴은 클래스를 단 한 번만 인스턴스화하고, 그 인스턴스에 대한 전역적인 접근 지점을 제공하는 생성 패턴이다.
핵심은 외부에서 새로운 객체를 만들 수 없도록 차단하고, 이미 생성된 인스턴스가 있다면 그것을 반환하도록 설계하는 것이다.

코드와 같이 확인하기 전 이해하고 있어야하는 메서드는 아래와 같다.

메서드 이름역할 설명
getInstanceCounter 클래스의 단일 인스턴스를 반환
getCount현재 counter 변수의 값을 반환
incrementcounter 변수의 값을 1 증가
decrementcounter 변수의 값을 1 감소
javascript
    let counter = 0;

    class Counter {
        getInstance() {
            return this;
        }

        getCount() {
            return counter;
        }

        increment() {
            return ++counter;
        }

        decrement() {
            return --counter;
        }
    }

위의 코드는 Singleton 패턴의 조건을 만족하지 않는다.
Singleton은 인스턴스를 단 한 번만 만들수 있어야 한다.

javascript
const counter1 = new Counter1();
const counter2 = new Counter2();

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

그렇기에 위의 코드를 실행해보면 new 메서드를 두 번 호출하여 counter1counter2를 각각 별개의 인스턴스를 가리키도록 했기에 각 인스턴스의 getInstance() 메서드를 호출해 동일한 인스턴스가 아니다.

singleton pattern 01

위 다이어그램을 보면 counter1, counter2서로 다른 객체처럼 보이지만, 실제로는 동일한 인스턴스를 참조하고 있다는 점이다.

javascript
let instance; // instance 변수 선언
let counter = 0;

class Counter {
    constructor() {
        if (instance) {
            throw new Error("단일 인스턴스만 허용됩니다 !");
        }
        instance = this;
    }
}

Counter 클래스 생성자에서 instance 변수가 새로 생성된 인스턴스를 가리키도록 한다.
이제 instance라는 변수가 값이 있음을 검사하는 것으로 새로운 인스턴스의 생성을 막을 수 있다.

javascript
const counter1 = new Counter1();
const counter2 = new Counter2();
// Error: 단일 인스턴스만 허용됩니다 !

위와 같이 인스턴스를 여러 변 만들려고 하면 예외가 발생하여 더 이상 진행할 수 없게 된다.

javascript
// Object.freeze를 통해 인스턴스의 불변성을 보장
const singletonCounter = Object.freeze(new Counter());
export default singletonCounter;

이렇게 만들어진 Counter 인스턴스를 export 하기 전에 인스턴스를 Object.freeze하여 메서드는 객체를 사용하는 쪽에서 직접 개체를 수정할 수 없도록 해준다.
이럴 경우 freeze처리 된 인스턴스는 프로퍼티의 추가 및 수정이 불가하므로 Singleton 인스턴스의 프로퍼티를 덮어쓰는 실수를 예방할 수 있다.

싱글톤 패턴 적용

구성한 코드로 예제 코드를 구현하면 아래와 같이 동작한다.
서로 다른 모듈에서 동일한 Counter Singleton 인스턴스를 공유하며, 각 버튼 클릭은 counter 값을 증가시킨다.

위 동작을 다이어그램으로 표현하면 아래와 같이 Singleton 패턴에 의해 Counter 인스턴스는 하나만 생성되며, 각 버튼 모듈은 이를 공유하는 상태이다.

singleton pattern 02

어떻게 공유가 가능한걸까 ?

처음에는 스크립트 초기화 단계(Initialization Phase)가 진행된다.

singleton pattern 03

즉시 실행 함수(IIFE)가 실행되면서 클로저가 생성되고, 클로저 스코프 내에 instance 변수가 선언되지만 아직 아무것도 할당되지 않아 undefined 상태이다.

singleton pattern 04

첫 번째 인스턴스 요청 getInstance()가 처음 호출된다.
instance가 비어있으므로, 힙(Heap) 메모리에 새로운 싱글톤 객체가 생성된다.
주소 : @HeapAddr_123

생성된 객체의 주소값이 클로저의 instance 변수와 요청한 instanceA 변수에 모두 할당된다.
이제 두 변수 모두 힙에 있는 동일한 객체를 가리킨다.

singleton pattern 05

두 번째 인스턴스 요청 getInstance()가 다시 호출된다.
클로저의 instance 변수에 이미 객체 주소값 @HeapAddr_123이 존재한다.
그렇기에 새로운 객체를 생성하지 않고, 기존 instance 변수가 가리키는 객체의 주소값을 그대로 instanceB 변수에 반환한다.
결과적으로 instanceA, instanceB 그리고 클로저의 instance 까지 모두가 힙에 있는 단 하나의 객체를 공유하게 된다.

장점과 단점

장점

싱글톤 패턴의 장점은 중앙 집중식 관리로 메모리 리소스 최적화와 데이터 정합성을 보장한다.
즉, 객체를 매번 생성하는 것은 메모리를 낭비하게 된다.

싱글톤은 단 하나의 인스턴스만 생성하여 이를 방지하여, 가비지 컬렉션(Garbage Collection)의 오버헤드를 줄여준다.

또한, 모든 모듈이 동일한 인스턴스를 참조하기에 데이터 파편화가 발생되지 않는다.
A 모듈에서 B 모듈에 즉시 반영되는 것을 보장하므로, 복잡한 동기화 로직 없이도 전역 상태의 일관성을 유지할 수 있게된다.

단점

싱글톤은 장점도 있지만, 안티 패턴(Anti-pattern)으로 분류되기도 한다.

  • 테스트의 고립성 파괴(Testing Challenges): 단위 테스트(Unit Test)의 핵심은 독립성이다.
    하지만, 싱글톤은 전역 상태를 유지하므로, A테스트에서 변경된 데이터가 B테스트에 그대로 남아 영향을 주게된다.
    이를 해결하려면 매 테스트마다 강제로 초기화하는 별도의 로직이 필요하여 신뢰도가 떨어진다.

  • 강한 결합도와 의존성 은닉(Tight Coupling & Hidden Dependencies): 모듈이 싱글톤을 직접 import해서 사용하면, 해당 모듈해당 모듈만 봐서는 무엇에 의존하는지 알 수 없고 코드의 유연성을 저하시키고 추후 싱글톤을 다른 객체로 교체하거나 리팩토링할 때 코드 전체를 고쳐야하는 상황이 올수도 있기 때문이다.

  • 예측 불가능한 사이드 이펙트(Unpredictable Side Effects): 전역에서 접근 가능하다는 것은 누가 언제 데이터를 변경했는지 추적하기 어렵다는 것이다. 그렇기에 규모가 커질수록 데이터 흐름을 파악하기 힘들어져 디버깅 비용이 증가할 수 있다.

언제 싱글톤 패턴을 써야 할까

애플리케이션 전역에서 단 하나만 존재해야 하며, 동일한 상태를 공유하는 다음 객체들에 사용하는 것이 효과적인 것 같다.

설명
데이터베이스 연결 관리인스턴스 생성 비용이 크고 연결 개수에 제한이 있는 자원을 단일 인스턴스로 중앙에서 효율적으로 통제
환경 설정 관리애플리케이션 전체가 공유해야 하는 읽기 전용 설정 값을 단일 지점에서 관리하여 불필요한 메모리 낭비를 방지
로깅(Logger)여러 모듈에서 발생하는 로그를 하나의 통로로 수집하여 로그 포맷과 흐름의 일관성을 보장

React 상태 관리와 싱글톤의 관계

React 환경에서는 싱글톤 객체를 직접 만드는 대신 Redux, Zustand, Context API와 같은 도구를 사용한다.

싱글톤 패턴 (Singleton)React 상태 관리 (Redux 등)
데이터 수정인스턴스 프로퍼티에 직접 접근해 수정액션과 리듀서를 통한 읽기 전용 상태로 예측 가능한 업데이트 제공
리렌더링값 변경 시 UI를 강제로 업데이트할 로직 필요상태 변경 시 관련 컴포넌트가 자동으로 리렌더링됨
의도적 제한누구나 어디서든 수정 가능 (Side Effect 추적 어려움)Dispatcher, Reducer 등을 통해 정해진 흐름(Flux 아키텍처 등)으로만 변경

싱글톤과 유사해 보이지만 이러한 도구들은 인스턴스의 값을 직접 수정할 수 있는 싱글톤과 달리 불변성(Immutability)을 지향하고, Redux를 사용할 땐 컴포넌트에서 디스패쳐를 통해 넘긴 액션에 대해 실행된 순수함수 리듀서를 통해서만 상태를 업데이트할 수 있다.

이러한 접근 방식은 전역 상태의 단점을 완전히 제거하지는 못 하더라도, 컴포넌트가 직접 상태를 업데이트하게 두는 것이 아닌 개발자가 의도한 대로 수정되도록 하는 것에 큰 차이가 있다.