Python과 JavaScript의 Singleton 구현은 어떻게 다른가

모듈 캐싱, 초기화 시점, 동시성 모델이 다른 Python과 JavaScript에서 Singleton을 구현하고 테스트를 위해 리셋하는 방식의 차이를 코드로 비교한다.

2026-08-13 · 최초 발행 2025-10-14

같은 패턴, 다른 런타임

애플리케이션 수명 범위 안에서 단 하나의 인스턴스를 보장하고 전역 접근 지점을 제공한다는 Singleton의 목표는 언어를 가리지 않는다. 하지만 Python과 JavaScript는 모듈 시스템, 런타임 초기화 규칙, 동시성 모델이 서로 달라서 실제 구현 방식과 안정성을 보장하는 방식에 차이가 생긴다. Python은 인터프리터 프로세스 단위로, JavaScript는 모듈 레지스트리(브라우저 Realm, Node.js 프로세스·Worker 단위)를 기준으로 범위가 정해진다. 중복 초기화를 막고 상태 일관성을 지키며 비용이 큰 리소스를 공유해 최적화한다는 목표는 같지만, 그 목표에 도달하는 경로가 다르다.

모듈이 캐싱되는 방식부터 다르다

Python은 import 시 모듈이 한 번만 실행되고 sys.modules에 캐시되어 재사용된다. 모듈 스코프 변수나 인스턴스는 자연스럽게 Singleton처럼 동작한다. JavaScript는 ES Module이 모듈 레지스트리에서 한 번만 평가되고 동일한 그래프 안에서 캐시된다. Node.js의 CommonJS는 require.cache로 유사하게 동작하지만, 번들러나 Worker 경계를 넘으면 별도의 인스턴스가 생길 수 있다.

초기화 시점과 지연 로딩

Python은 import 시점에 즉시 실행되는 경향이 있고, 지연 초기화가 필요하면 팩토리 함수·프로퍼티·데코레이터로 구현한다. JavaScript는 모듈 최상위에서 즉시 실행하거나 getInstance 클로저로 지연 초기화하는 두 방식을 모두 쓴다. Top-level await를 쓰면 초기화 순서와 지연 의도를 더 명확히 드러낼 수 있다.

동시성 모델과 생성 경합

Python은 멀티스레드 환경에서 최초 생성 구간을 보호해야 한다 — threading.Lock 기반 이중 검사 패턴을 흔히 쓴다. JavaScript는 단일 이벤트 루프 기반이라 생성 경합이 낮은 편이지만, Web Worker나 Node의 worker_threads, Cluster를 쓰면 프로세스·스레드마다 별도 인스턴스가 만들어질 수 있다.

테스트에서 Singleton을 어떻게 리셋하는가

Python에서는 importlib.reload나 의존성 주입(컨테이너)으로 상태를 초기화하고, pytest에서는 세션 격리 전략이 필요하다. JavaScript에서는 Hot Reload/HMR이나 Jest의 resetModules로 캐시를 무효화한다. ESM과 CJS를 혼용하면 모듈 레지스트리가 이원화될 수 있어 주의가 필요하다.

배포 경계에 따라 인스턴스가 갈린다

Python은 gunicorn 워커처럼 WSGI를 다중 프로세스로 운영하면 프로세스마다 별도의 Singleton이 존재하고, 프로세스 경계를 넘어서는 공유가 불가능하다. JavaScript는 브라우저 탭·iframe, Node Cluster·Worker마다 별도의 Singleton이 생기고, 서버리스 호출 단위에서는 콜드 스타트 때마다 매번 초기화될 가능성도 있다.

Python 구현

전제조건은 Python 3.10+, 단일 프로세스 내 기본 케이스를 가정하며 멀티 프로세스·워커는 별도 전략이 필요하다.

가장 단순한 형태는 모듈 단위 Singleton이다. config.py 파일이 한 번만 로드되면서 상태 공유가 성립한다.

# config.py
import os

APP_NAME = "myapp"
settings = {
    "dsn": os.getenv("DB_DSN", "postgres://localhost/app"),
    "pool_size": int(os.getenv("POOL_SIZE", "10")),
}
# use.py
from config import settings  # 동일 프로세스 내 어디서든 동일 dict 참조

경합을 관리해야 하는 경우에는 메타클래스와 스레드 안전 지연 초기화를 조합한다.

# singleton.py
from threading import Lock

class SingletonMeta(type):
    _instance = None
    _lock = Lock()

    def __call__(cls, *args, **kwargs):
        if cls._instance is None:
            with cls._lock:
                if cls._instance is None:  # 이중 검사
                    cls._instance = super().__call__(*args, **kwargs)
        return cls._instance

class DbClient(metaclass=SingletonMeta):
    def __init__(self, dsn: str):
        self.dsn = dsn
        # 실제 커넥션 풀 초기화 등
# use_db.py
from singleton import DbClient
db = DbClient("postgres://localhost/app")  # 어느 위치에서 호출해도 동일 인스턴스

테스트에서 상태를 리셋하려면 sys.modules.pop('singleton', None) 후 다시 import하거나, SingletonMeta._instance = None으로 재설정하면 된다.

JavaScript 구현

가장 단순한 형태는 ES Module 단위 Singleton이다.

// dbClient.js (ESM)
export class DbClient {
  constructor(dsn) {
    this.dsn = dsn; /* 커넥션 풀 초기화 등 */
  }
}
export default new DbClient(process.env.DB_DSN ?? "postgres://localhost/app");
// use.js (ESM)
import db from "./dbClient.js"; // 동일 모듈 그래프 내 단일 인스턴스

테스트나 초기화 순서 제어가 중요하다면 클로저 기반 지연 초기화가 더 다루기 쉽다.

// singletonFactory.js (ESM/CJS 공용 패턴)
class DbClient {
  constructor(dsn) {
    this.dsn = dsn;
  }
}

let instance;
export function getDb(dsn = process.env.DB_DSN ?? "postgres://localhost/app") {
  if (!instance) instance = new DbClient(dsn);
  return instance;
}

테스트에서 리셋하려면 Jest에서는 jest.resetModules()를 쓰거나 delete require.cache[require.resolve('./singletonFactory')]로 캐시를 지운다. ESM 환경에서는 동적 import에 고유 쿼리 파라미터를 붙여 격리하거나, 테스트 전역에서 instance = undefined로 재설정한다. 다만 Worker·iframe·Cluster별로 모듈 레지스트리가 분리될 수 있으므로, 경계를 넘어 공유해야 한다면 IPC/RPC나 외부 저장소가 필요하다.

런타임의 동작 흐름

JavaScript (Realm/Worker)없음있음import module/getInstanceModule Registry 존재 여부ESM 평가 or 함수 최초 호출인스턴스 생성/보관동일 Realm 동일 인스턴스반환Python (Process)없음있음import modulesys.modules 존재 여부모듈 코드 1회 실행인스턴스 생성/보관동일 프로세스 동일 인스턴스반환

에지 케이스로는, Python은 다중 프로세스마다 sys.modules가 독립적이고, JavaScript는 다중 Realm·Worker마다 레지스트리가 독립적이라는 점을 기억해야 한다.

지표로 비교하면

지표 Python JavaScript
성능 모듈 1회 로드 후 참조 비용 O(1). GIL 영향 내 스레드 경합 최소화 ESM 1회 평가 후 참조 O(1). 이벤트 루프 내 경합 낮음
확장성 프로세스 스케일아웃 시 프로세스별 인스턴스 분리. 외부 리소스 풀로 확장 Worker/Cluster로 수평 확장. IPC로 상태 일관성 유지 필요
일관성 sys.modules 단일 캐시로 강한 일관성. 리로드 시 주의 Module Registry 단위 일관성. 번들/HMR 시 모듈 경로 정규화 필요
안정성 스레드 안전 초기화 락 권장. 예외 시 부분 초기화 방지 처리 필요 단일 스레드 초기화 안정성 우수. Worker 경계에서 재초기화 관리 필요
운영 편의 import만으로 간단. pytest·reload 시 상태 초기화 절차 필요 ESM/CJS 선택지 많음. HMR·테스트에서 캐시 무효화 유틸 필요

실무에 적용할 때

설정이나 환경 변수를 한 곳에 모을 때는 Python에서는 config 모듈의 상수·dict로 바로 적용하고, JS에서는 ESM의 export default settings를 빌드 타임 주입과 조합한다. 데이터베이스 커넥션 풀은 Python에서 메타클래스·팩토리로 지연 초기화하고 락을 걸며, JS에서는 getInstance 패턴으로 필요할 때만 만드는 플라이웨이트 방식을 적용한다. 외부 SDK 클라이언트는 공용 인증 토큰과 재시도 정책을 공유하고, 테스트 시에는 모듈 캐시를 리셋해 격리한다.

이런 방식으로 중복 생성을 없애면 고비용 커넥션 기준으로 런칭 시간이 20~40% 단축될 것으로 기대된다. 전역 상태 충돌이나 중복 커넥션 오류가 줄어 일관성과 안정성도 높아지고, 의존성 주입 지점이 줄어 모듈 경로 기반 추적성도 좋아진다.

어느 쪽을 선택할 것인가

생성 경합은 Python에서는 락과 이중 검사로, JS에서는 클로저나 정적 초기화로 제어한다. 프로세스·Worker·서버리스 인보케이션 경계마다 Singleton이 재생성될 수 있다는 전제를 깔고 설계해야 한다. 테스트 전략은 모듈 캐시 무효화, 팩토리 기반 주입, 상태 초기화 헬퍼 제공이 핵심이다.

트레이드오프도 분명하다. 전역 상태는 결합도를 높이는 대신 접근을 편리하게 하고, 핫 리로드·테스트 격리는 어려워지는 대신 접근 경로는 단순해진다. 다중 인스턴스가 필요해지면 확장성이 제약되는 대신 일관성은 얻는다. 실무에서는 모듈 단순 패턴을 먼저 적용하고, 경합·경계·테스트 요구가 늘어날 때 지연 초기화·락·팩토리·DI로 단계적으로 정교화하는 편이 안전하다.

디자인 패턴Singleton 패턴PythonJavaScript모듈 시스템