애플리케이션이 시작될 때, 클래스에 인스턴스 하나만 있도록 하면서(최초 한 번만 메모리를 할당) 해당 인스턴스에 대한 전역 접근 지점을 제공하는 디자인 패턴이다.
주로 공통된 객체를 여러 개 생성해서 사용해야 하는 상황에서 사용한다. 데이터베이스에서 커넥션풀, 스레드풀, 캐시, 로그 기록 객체 등이 있다.
getInstance
메서드를 호출하는 것이 싱글턴 클래스를 가져올 수 있는 유일한 방법이어야 하며, 클래스 호출 시 이미 생성된 싱글턴 클래스가 있다면 해당 클래스를 반환하거나 반대의 경우에는 싱글턴 클래스를 인스턴스화(instantiating) 한다.
클래스가 하나의 인스턴스 객체로만 존재하기 때문에 다른 클래스의 인스턴스들이 데이터를 공유, 절대적으로 한 개만 존재하는 것을 보증한다는 장점 등이 있다.
반대로 객체 지향 설계 원칙 중에 개방-폐쇄 원칙이란 것이 존재한다. 싱글턴 클래스를 사용한 다른 클래스의 결합 도가 높아지게 되면, 유지보수가 힘들고 테스트도 원활하게 진행할 수 없는 문제점이 발생할 수 있다(모듈성 저하). 또한 싱글턴에 의존하는 클래스를 다른 콘텍스트에서 사용하려면 싱글턴도 다른 콘텍스트로 전달해야 한다. 이 또한 대부분은 유닛 테스트를 생성하는 동안 발생한다. 또한 멀티 스레드 환경에서 동기화 처리를 하지 않았을 때, 인스턴스가 2개가 생성되는 문제도 발생할 수 있다.
from collections import defaultdict
class SingletonMeta(type):
_instances = defaultdict()
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
instance = super().__call__(*args, **kwargs)
cls._instances[cls] = instance
print(f"input kwargs: {kwargs} \n current class is {cls._instances[cls]}")
return cls._instances[cls]
class SingletonWithMetaClass(metaclass=SingletonMeta):
def __init__(self, *args, **kwargs):
self._name: str = kwargs["name"]
@property
def name(self) -> str:
return self._name
@name.setter
def name(self, name: str) -> None:
setattr(self, "_name", name)
@name.deleter
def name(self):
del self._name
def __repr__(self) -> str:
return f"this class is {self.name}'s class"
if __name__ == "__main__":
p1 = SingletonWithMetaClass(name="John")
p2 = SingletonWithMetaClass(name="Ming")
print(p1.name, p2.name)
print(id(p1), id(p2))
""" 결과:
input kwargs: {'name': 'John'}
input kwargs: {'name': 'Ming'}
# 클래스를 호출 할 때마다 input data는 제대로 입력되고 있다. 하지만
John John
# 기존 클래스의 attribute는 변경되지 않는다.
4566202784 4566202784
# class의 id도 동일한 주소값을 가지고 있다.
"""
class SingletonWithNewMethod(object):
def __new__(cls, *args, **kwargs):
if not hasattr(cls, "_instance"):
print("__new__ is called", end="\n")
cls._instance = super().__new__(cls)
print(f"input kwargs: {kwargs}")
return cls._instance
def __init__(self, **kwargs):
cls = type(self)
if not hasattr(cls, "_init"):
print("__init__ is called", end="\n")
self._name = kwargs["name"]
print(f"input kwargs: {kwargs}")
cls._init = True
@property
def name(self) -> str:
return self._name
@name.setter
def name(self, name: str) -> None:
setattr(self, "_name", name)
@name.deleter
def name(self) -> None:
del self._name
def __repr__(self) -> str:
return f"this class is {self.name}'s class"
if __name__ == "__main__":
p3 = SingletonWithNewMethod(name="John")
p4 = SingletonWithNewMethod(name="Ming")
print(p3.name, p4.name)
print(id(p3), id(p4))
""" 결과
__new__ is called
input kwargs: {'name': 'John'}
__init__ is called
input kwargs: {'name': 'John'}
# 최초 클래스 호출 시 __new__ 메소드가 작동하고, 이후에는 __init__이 작동한다.
John John
4566202832 4566202832
# 위 방식과 동일하게 최초 생성된 싱글턴 클래스는 내부 attribute값이 변경되지 않고, 동일한 id를 가지게 된다.
"""
from threading import Lock, Thread
class SingletonMultiThread(type):
_instances = defaultdict()
_lock: Lock = Lock()
def __call__(cls, *args, **kwargs):
with cls._lock:
if cls not in cls._instances:
instance = super().__call__(*args, **kwargs)
cls._instances[cls] = instance
return cls._instances[cls]
class SingletonMultiThread(metaclass=SingletonMultiThread):
_name = None
def __init__(self, **kwargs) -> None:
self._name: str | None = kwargs.get("name")
@property
def name(self):
return self._name
@name.setter
def name(self, name: str) -> None:
setattr(self, "_name", name)
@name.deleter
def name(self) -> None:
del self._name
def __repr__(self) -> str:
return f"this class is {self.name}'s class"
@property
def name(self):
return self._name
def test_singleton(name: str) -> None:
smt = SingletonMultiThread(name=name)
print(smt.name)
if __name__ == "__main__":
p5 = Thread(target=test_singleton, args=["John"])
p6 = Thread(target=test_singleton, args=["Ming"])
p5.start()
p6.start()
""" 결과
John
John
"""