Singleton
싱글톤
클래스의 인스턴스가 하나만 존재하도록 보장하는 GoF 디자인 패턴. Lazy, Eager, Double-checked locking 등 다양한 구현 방식 제공.
싱글톤
클래스의 인스턴스가 하나만 존재하도록 보장하는 GoF 디자인 패턴. Lazy, Eager, Double-checked locking 등 다양한 구현 방식 제공.
싱글톤(Singleton)은 클래스의 인스턴스가 애플리케이션 전체에서 오직 하나만 존재하도록 보장하는 생성 디자인 패턴입니다. 이 패턴은 전역적으로 접근 가능한 단일 객체가 필요할 때 사용됩니다. 예를 들어 데이터베이스 연결 풀, 로깅 시스템, 설정 관리자 등 여러 곳에서 동일한 인스턴스를 공유해야 하는 경우에 적합합니다. 싱글톤은 생성자를 private으로 선언하고, 정적 메서드를 통해 유일한 인스턴스에 접근하는 방식으로 구현됩니다.
싱글톤은 1994년 "Gang of Four(GoF)"가 발표한 23가지 디자인 패턴 중 하나로, 생성 패턴(Creational Pattern)에 속합니다. GoF 패턴 중에서도 가장 널리 알려지고 많이 사용되는 패턴이지만, 동시에 가장 논쟁이 많은 패턴이기도 합니다. 객체지향 설계의 기본 원칙인 SOLID 중 단일 책임 원칙(SRP)과 개방-폐쇄 원칙(OCP)을 위반할 수 있으며, 전역 상태를 만들어 코드의 결합도를 높인다는 비판을 받습니다.
싱글톤 구현 방식은 크게 세 가지로 나뉩니다. Eager Initialization(즉시 초기화)은 클래스 로딩 시점에 인스턴스를 생성하여 스레드 안전하지만, 사용하지 않아도 메모리를 점유합니다. Lazy Initialization(지연 초기화)은 처음 요청할 때 인스턴스를 생성하여 자원을 절약하지만, 멀티스레드 환경에서 동기화 문제가 발생할 수 있습니다. Double-Checked Locking은 성능과 스레드 안전성을 모두 확보하지만, 구현이 복잡하고 언어별로 volatile 키워드 등 추가 처리가 필요합니다.
실무에서 싱글톤은 로깅, 캐시, 설정 관리, 스레드 풀 등에서 여전히 활용되지만, 많은 개발자들이 "안티패턴"으로 간주합니다. 싱글톤은 단위 테스트를 어렵게 만들고, 의존성을 숨기며, 전역 상태로 인한 예측 불가능한 동작을 유발할 수 있습니다. 현대적인 소프트웨어 개발에서는 싱글톤 대신 의존성 주입(Dependency Injection) 컨테이너를 통해 객체의 생명주기를 관리하는 방식이 권장됩니다. Spring, Guice 같은 DI 프레임워크에서는 싱글톤 스코프를 지원하면서도 테스트 용이성을 유지합니다.
클래스 로딩 시점에 인스턴스 생성. 간단하고 스레드 안전.
첫 요청 시 인스턴스 생성. 자원 효율적이나 동기화 필요.
성능과 스레드 안전성 모두 확보. volatile 키워드 필수.
내부 정적 클래스로 지연 로딩. Java에서 가장 권장.
# Python 싱글톤 구현 예제
from threading import Lock
from typing import Optional
# 1. 데코레이터를 활용한 싱글톤 (가장 Pythonic)
def singleton(cls):
"""싱글톤 데코레이터"""
instances = {}
lock = Lock()
def get_instance(*args, **kwargs):
if cls not in instances:
with lock: # 스레드 안전
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance
@singleton
class DatabaseConnection:
"""데코레이터 방식 싱글톤"""
def __init__(self, host: str = "localhost"):
self.host = host
self.connected = False
print(f"DB 연결 생성: {host}")
def connect(self):
self.connected = True
print("DB 연결됨")
# 2. 메타클래스를 활용한 싱글톤 (고급)
class SingletonMeta(type):
"""싱글톤 메타클래스"""
_instances = {}
_lock = Lock()
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
with cls._lock:
if cls not in cls._instances:
instance = super().__call__(*args, **kwargs)
cls._instances[cls] = instance
return cls._instances[cls]
class Logger(metaclass=SingletonMeta):
"""메타클래스 방식 싱글톤"""
def __init__(self):
self.logs = []
print("Logger 인스턴스 생성")
def log(self, message: str):
self.logs.append(message)
print(f"[LOG] {message}")
# 3. __new__를 활용한 싱글톤 (클래식)
class ConfigManager:
"""__new__ 오버라이드 방식"""
_instance: Optional['ConfigManager'] = None
_lock = Lock()
def __new__(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self):
if self._initialized:
return
self._initialized = True
self.config = {}
print("ConfigManager 초기화")
def set(self, key: str, value):
self.config[key] = value
def get(self, key: str, default=None):
return self.config.get(key, default)
# 4. 모듈 레벨 싱글톤 (가장 간단, Python 권장)
# config.py 파일로 분리하면 자동으로 싱글톤
class _AppConfig:
def __init__(self):
self.debug = False
self.version = "1.0.0"
app_config = _AppConfig() # 모듈 로드 시 한 번만 생성
# 사용 예제
if __name__ == "__main__":
# 데코레이터 싱글톤 테스트
db1 = DatabaseConnection("mysql://prod")
db2 = DatabaseConnection("mysql://dev") # 무시됨
print(f"같은 인스턴스? {db1 is db2}") # True
print(f"호스트: {db1.host}") # mysql://prod
# 메타클래스 싱글톤 테스트
logger1 = Logger()
logger2 = Logger() # 기존 인스턴스 반환
logger1.log("첫 번째 로그")
print(f"같은 인스턴스? {logger1 is logger2}") # True
# __new__ 싱글톤 테스트
config1 = ConfigManager()
config1.set("api_key", "secret123")
config2 = ConfigManager()
print(f"API Key: {config2.get('api_key')}") # secret123
"싱글톤 구현 시 멀티스레드 환경을 고려해야 합니다. 가장 간단한 Eager 방식은 클래스 로딩 시 인스턴스를 만들어 스레드 안전하지만, 사용하지 않아도 메모리를 점유해요. Lazy 방식은 필요할 때 생성하지만 동기화 없이는 두 개 이상 인스턴스가 생길 수 있습니다. 그래서 Double-Checked Locking을 쓰는데, Java에서는 volatile 키워드가 필수입니다. 개인적으로 Java라면 Bill Pugh 방식이나 Enum을 추천드려요. 내부 정적 클래스를 활용하면 지연 로딩과 스레드 안전성을 모두 얻을 수 있고, Enum은 직렬화와 리플렉션 공격까지 방어됩니다."
"이 UserService가 싱글톤일 필요가 있을까요? 지금 구조로는 단위 테스트가 거의 불가능해요. getInstance()로 직접 호출하니 Mock 주입이 안 되고, 전역 상태가 테스트 간에 공유되어 테스트 순서에 따라 결과가 달라질 수 있습니다. 또 DatabaseConnection 싱글톤에 직접 의존하니 숨겨진 의존성이 생겼어요. 차라리 의존성 주입을 도입해서 생성자로 주입받고, 객체 생명주기는 DI 컨테이너가 관리하게 하면 어떨까요? Spring의 @Scope('singleton')처럼요. 그러면 프로덕션에서는 싱글톤으로 동작하면서도 테스트에서는 Mock을 주입할 수 있습니다."
"로깅이나 설정 관리처럼 진짜 전역적으로 하나만 있어야 하는 경우에만 싱글톤을 쓰고, 그 외에는 DI 컨테이너의 싱글톤 스코프를 활용합시다. 직접 싱글톤을 구현하면 테스트 격리가 어렵고, 순환 의존성 발생 시 디버깅이 힘들어요. 특히 우리 프로젝트처럼 마이크로서비스 환경에서는 인스턴스가 여러 개 떠도 문제가 없어야 하는데, 싱글톤 가정이 깨질 수 있습니다. 데이터베이스 커넥션 풀은 HikariCP처럼 검증된 라이브러리를 쓰고, 캐시는 Redis로 외부화하는 게 확장성 면에서 낫습니다."
싱글톤은 단위 테스트를 매우 어렵게 만듭니다. getInstance()를 직접 호출하면 Mock 객체로 대체할 수 없고, 테스트 간에 상태가 공유되어 테스트 격리가 깨집니다. 테스트 순서에 따라 결과가 달라지는 "Flaky Test"가 발생할 수 있습니다. 의존성 주입과 인터페이스를 활용하여 테스트 시 Mock을 주입할 수 있도록 설계하세요.
싱글톤을 사용하면 클래스의 의존성이 생성자나 메서드 시그니처에 드러나지 않습니다. 코드 내부에서 getInstance()를 호출하므로 외부에서는 어떤 의존성이 있는지 파악하기 어렵습니다. 이는 코드의 결합도를 높이고, 리팩토링과 유지보수를 어렵게 만듭니다. 의존 관계가 명시적으로 드러나도록 생성자 주입을 사용하세요.
싱글톤은 본질적으로 전역 상태를 만듭니다. 애플리케이션 어디서든 접근하고 수정할 수 있어 예측 불가능한 동작과 버그를 유발합니다. 멀티스레드 환경에서는 경쟁 조건이 발생할 수 있고, 마이크로서비스나 클러스터 환경에서는 인스턴스가 여러 개 존재할 수 있어 싱글톤 가정이 깨질 수 있습니다. 상태를 최소화하거나 외부 저장소(Redis 등)로 분리하세요.