싱글턴 패턴(Singleton Pattern)은 어떤 클래스의 인스턴스가 프로그램 전체에서 단 하나만 존재하도록 강제하고,
그 하나뿐인 인스턴스에 어디서든 접근할 수 있는 전역 접근점을 제공하는 디자인 패턴이다.
로그 기록기, 설정(Config) 객체, DB 커넥션 풀처럼 "여러 개가 있으면 오히려 곤란한" 대상에 주로 쓴다.
아래에서 일반 클래스와 싱글턴 클래스에 각각 "인스턴스 생성"을 여러 번 눌러 차이를 눈으로 확인해 보자.
🧱 일반 클래스 Logger()
생성 시도 횟수0
실제 생성된 객체 수0
호출할 때마다 서로 다른 메모리 주소를 가진 새 객체가 매번 만들어진다.
로그 파일 핸들을 여러 번 열거나, 설정값이 객체마다 따로 노는 문제가 생길 수 있다.
🔒 싱글턴 클래스 Logger.instance()
생성 시도 횟수0
실제 생성된 객체 수0
처음 호출될 때만 객체를 만들고(게으른 초기화, Lazy Init), 이후 호출은 이미 만들어둔 그 객체를
그대로 돌려준다. 몇 번을 호출해도 메모리 주소가 동일하다.
🐍 코드로 보는 구현
classLogger:
_instance = None# 클래스 변수 — 인스턴스를 여기 보관def__new__(cls):
if cls._instance isNone:
# 아직 없으면 딱 한 번만 생성
cls._instance = super().__new__(cls)
cls._instance.logs = []
return cls._instance # 있으면 그대로 반환defwrite(self, msg):
self.logs.append(msg)
a = Logger()
b = Logger()
assert a is b # True — 완전히 같은 객체!
// IIFE(즉시실행함수)로 비공개 인스턴스를 감춘다const Logger = (function() {
let instance; // 클로저에 저장 — 외부에서 직접 접근 불가functioncreateInstance() {
return { logs: [] };
}
return {
getInstance() {
if (!instance) instance = createInstance();
return instance;
}
};
})();
Logger.getInstance() === Logger.getInstance(); // true
import threading
classLogger:
_instance = None
_lock = threading.Lock() # 락 하나 추가def__new__(cls):
if cls._instance isNone:
with cls._lock: # 이중 검사 잠금(DCL)# 락을 기다리는 동안 다른 스레드가# 먼저 만들었을 수 있으니 한 번 더 확인if cls._instance isNone:
cls._instance = super().__new__(cls)
return cls._instance
왜 스레드 안전이 문제될까? 두 스레드가 if cls._instance is None을
"동시에" 통과하면 객체가 두 번 만들어질 수 있다. 락(Lock)으로 "생성 구간"을 한 번에 하나의 스레드만
지나가게 막아야 진짜 하나만 보장된다.
🌍 실전에서 쓰이는 곳
로거 (Logger) 모든 모듈이 같은 로그 파일에 순서대로 기록하려면 로거 객체가 하나여야 한다.
설정 관리자 환경변수·설정값을 앱 전체에서 동일하게 읽도록 하나의 Config 객체로 통일한다.
DB 커넥션 풀 연결 풀 객체가 여러 개면 각자 다른 연결을 관리해 자원 낭비·설정 불일치가 생긴다.
⚠️ 주의점 — 만능이 아니다
① 숨겨진 전역 상태 어디서든 접근 가능하다는 건, 어디서든 값을 몰래 바꿀 수 있다는 뜻이다.
코드 흐름을 추적하기 어려워진다.
② 테스트가 어려움 테스트마다 상태를 초기화하기 힘들고, 가짜 객체(Mock)로 바꿔 끼우기도 까다롭다.
③ 대안 전역 싱글턴 대신, 필요한 객체를 함수/생성자 인자로 명시적으로 넘겨주는
의존성 주입(Dependency Injection)이 최근에는 더 선호된다.