HOWTO · Python

Python의 싱글톤 디자인 패턴

이 튜토리얼은 Python에서 싱글톤 디자인 패턴의 구현을 보여줍니다.

이 페이지의 내용

Singleton은 정해진 범위 안에서 클래스의 공유 인스턴스를 하나로 제한합니다. Python에서는 데코레이터, 기본 클래스, 메타클래스 또는 모듈 상태를 사용해 이 동작을 구현할 수 있습니다. 각 방식은 객체 생성 과정을 다르게 바꾸며, 어느 방식도 자동으로 스레드 안전성이나 여러 프로세스가 공유하는 단일 객체를 보장하지 않습니다. 아래 예제는 객체 동일성을 확인하므로 코드에서 클래스를 사용하는 방식에 맞춰 방법을 선택할 수 있습니다.

Python에서 Singleton이 동작하는 방식

일반적으로 클래스를 호출할 때마다 새 객체가 만들어집니다. Singleton은 객체 생성 경로를 바꿔 여러 번 호출해도 같은 객체를 반환합니다. 이 보장은 보통 클래스별, Python 인터프리터별로 적용됩니다. 모든 프로그램이나 프로세스가 전역 객체 하나를 공유한다는 뜻은 아닙니다. 예제에서는 first is second로 두 이름이 같은 객체를 가리키는지 확인합니다.

데코레이터로 Singleton 구현하기

데코레이터는 클래스를 첫 인스턴스를 캐시하는 함수로 바꿀 수 있습니다. 코드가 간결하고 캐시가 데코레이트한 클래스 가까이에 놓입니다. 대신 공개된 이름은 원래 클래스가 아니라 래퍼 함수가 됩니다. Sample을 통해 클래스 메서드를 호출하는 등의 클래스 수준 작업이 예상대로 동작하지 않을 수 있고, 클래스를 검사하는 도구에 보이는 인터페이스도 달라집니다.

다음 예제를 실행해 보세요.

이 섹션의 코드와 표시되는 레이블 또는 출력은 영어로 유지됩니다.

def singleton(class_):
    instances = {}

    def get_instance(*args, **kwargs):
        if class_ not in instances:
            instances[class_] = class_(*args, **kwargs)
        return instances[class_]

    return get_instance


@singleton
class Sample:
    def __init__(self, name="shared"):
        self.name = name


first = Sample()
second = Sample("ignored after construction")
print(first is second)
print(second.name)

제공된 런타임 검사에서 예제는 통과했습니다. 첫 호출이 인스턴스를 만들고 이후 호출은 그 인스턴스를 반환하므로, 나중에 전달한 생성자 인수는 다시 초기화하지 않습니다. 출력은 두 이름이 같은 객체를 가리키며 이름이 shared로 유지됨을 보여 줍니다.

기본 클래스로 Singleton 구현하기

기본 클래스는 __new__에서 인스턴스 생성을 한곳에 모을 수 있습니다. __new__는 __init__보다 먼저 실행됩니다. 서로 다른 하위 클래스가 실수로 같은 객체를 공유하지 않도록 구체적인 하위 클래스별로 인스턴스를 저장하세요. Python는 __new__가 반환된 뒤에도 __init__을 호출하므로 객체를 만들 때마다 초기화가 실행됩니다. 초기화를 반복해도 안전하도록 작성하거나, 반복 초기화가 상태를 덮어쓴다면 초기화 여부를 확인하는 방어 코드를 두세요.

이 섹션의 코드와 표시되는 레이블 또는 출력은 영어로 유지됩니다.

class SingletonBase:
    _instances = {}

    def __new__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super().__new__(cls)
        return cls._instances[cls]


class Cache(SingletonBase):
    def __init__(self):
        if not hasattr(self, "values"):
            self.values = {}


first = Cache()
second = Cache()
print(first is second)
print(second.values)

초기화 여부 확인 코드는 반복되는 __init__ 호출이 기존 딕셔너리를 바꾸지 못하게 합니다. 기본 클래스 방식은 다른 상속 동작과 결합하기 어려워질 수 있으며, 특히 여러 기본 클래스가 객체 생성 과정을 각자 바꾸는 경우 문제가 복잡해집니다. Singleton 동작을 재사용 가능한 클래스 계층에 넣기 전에 이 비용을 고려하세요.

메타클래스로 Singleton 구현하기

메타클래스는 클래스 생성을 제어합니다. Python 3 형식에서는 선언한 클래스가 계속 클래스로 유지되며, 메타클래스를 사용하는 클래스마다 인스턴스를 따로 캐시합니다. 클래스가 호출될 때 해당 메타클래스의 __call__ 메서드가 실행됩니다.

이 섹션의 코드와 표시되는 레이블 또는 출력은 영어로 유지됩니다.

class SingletonMeta(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]


class Service(metaclass=SingletonMeta):
    def __init__(self, endpoint="local"):
        self.endpoint = endpoint


first = Service()
second = Service("another endpoint")
print(first is second)
print(second.endpoint)

이 방식은 함수로 바뀐 데코레이터보다 클래스 수준 작업을 잘 보존하지만, 메타클래스가 기본 클래스에 필요한 다른 메타클래스와 충돌할 수 있습니다. 첫 호출의 인수가 캐시할 인스턴스를 초기화하고, 이후 호출은 __init__을 다시 실행하지 않고 그 인스턴스를 반환합니다. 여기에 표시된 metaclass=... 선언은 Python 3 문법입니다. 이전 Python 2 형식은 현재 지침이 아닙니다.

모듈을 Singleton으로 사용하기

Python는 인터프리터 안에서 가져온 모듈을 sys.modules에 캐시합니다. 따라서 같은 이름으로 같은 모듈을 가져오면 호출자들은 동일한 모듈 객체와 모듈 수준의 공유 상태를 사용합니다. 공유 자원이 클래스 인터페이스를 가진 인스턴스보다 함수와 상태의 묶음에 가까울 때는 이 방법이 가장 간단한 선택인 경우가 많습니다.

이 섹션의 코드와 표시되는 레이블 또는 출력은 영어로 유지됩니다.

import importlib

first = importlib.import_module("__main__")
second = importlib.import_module("__main__")
first.shared_value = "available through the module"
print(first is second)
print(second.shared_value)

이 간단한 예제는 현재 실행 중인 모듈을 표준 이름으로 가져옵니다. 애플리케이션에서는 공유 상태와 함수를 전용 모듈에 두고, 사용하는 코드가 일관된 방식으로 그 모듈을 가져오도록 하세요. 모듈 캐시는 인터프리터 범위로 제한됩니다. 별도 프로세스에는 별도의 모듈 객체가 있으며, 모듈을 다시 불러오면 코드가 다시 실행될 수 있습니다.

접근 방식 선택과 한계 이해하기

호출자에게 공유 함수나 설정이 필요하면 모듈 상태를 사용하고, 클래스를 유지하면서 객체 생성을 제어해야 하면 메타클래스를 사용하세요. 래퍼 인터페이스가 괜찮다면 데코레이터도 선택할 수 있습니다. 기본 클래스는 상속을 통해 동작을 재사용할 수 있지만 초기화와 상속에 제약이 생깁니다. 의존성 주입으로 일반 객체를 전달해 문제를 해결할 수 있다면, 그 편이 테스트하고 동작을 파악하기 쉬운 경우가 많습니다.

이 예제들이 보여 주는 것은 일반적인 단일 스레드 환경에서 객체를 반복 생성할 때의 동작뿐입니다. 동시 실행 중 최초 호출이 겹치면 생성 과정을 동기화하지 않는 한 경쟁 상태가 발생할 수 있고, 프로세스나 인터프리터마다 캐시도 따로 존재합니다. 여러 스레드, 프로세스 또는 컴퓨터가 조율해야 하는 자원에는 명시적인 동기화나 공유 서비스 설계를 사용하세요. 의도한 범위 안에서 공유 인스턴스 하나가 실제로 필요할 때만 Singleton을 선택하세요.