반응형
모노스테이트 싱글톤 패턴
- The Monostate Singleton Pattern
- 객체 생성여부 보다는 상태와 행위에 초점을 맞춘 패턴
- 단일 객체가 아닌, 모든 객체가 같은 상태를 공유하는 패턴
- __init__ 으로 구현 하는 방법
# __init__ 으로 구현하는 방법
class Mono:
__shared_state = {"공유":"데이터"}
def __init__(self):
self.data = 1
self.__dict__ = self.__shared_state
pass
obj = Mono()
obj.data = 9999
other_obj = Mono()
print(f"{obj=}")
print(f"{other_obj=}")
"""
결과값. 서로 다른 인스턴스임을 확인할 수 있음
obj=<__main__.Mono object at 0x1011ffb50>
other_obj=<__main__.Mono object at 0x101477790>
"""
print(f"{obj.__dict__=}")
print(f"{other_obj.__dict__=}")
"""
결과값. 서로 다른 인스턴스지만 상태는 공유하고 있다.
obj.__dict__={'공유': '데이터', 'data': 9999}
other_obj.__dict__={'공유': '데이터', 'data': 9999}
"""
"""
만약 'self.__dict__ = self.__shared_state' 를 하지 않는다면,
다음처럼 인스턴스 끼리 상태를 공유하지 못한다.
결과값.
obj.__dict__={'data': 9999}
other_obj.__dict__={'data': 1}
"""
- __new__ 로 구현하는 방법
# __new__ 로 구현 하는 방법
class Mono(object):
_shared_state = {}
def __new__(cls, *args, **kwargs):
obj = super(Mono, cls).__new__(cls, *args, **kwargs)
obj.__dict__ = cls._shared_state
return obj
obj = Mono()
obj.data = 9999
other_obj = Mono()
print(f"{obj=}")
print(f"{other_obj=}")
"""
결과값. 서로 다른 인스턴스임을 확인할 수 있음
obj=<__main__.Mono object at 0x100ed3730>
other_obj=<__main__.Mono object at 0x100ed3700>
"""
print(f"{obj.__dict__=}")
print(f"{other_obj.__dict__=}")
"""
결과값. 서로 다른 인스턴스지만 상태는 공유하고 있다.
obj.__dict__={'data': 9999}
other_obj.__dict__={'data': 9999}
"""
❓ 같은 상태를 가진다면 단일 객체든, 복수 객체든 무슨 차이가 있는지?
싱글톤과 메타클래스
- 메타클래스는 클래스의 클래스, 즉 클래스는 자신의 메타클래스의 인스턴스다.
- 이미 정의된 클래스가 있다면 메타클래스를 생성해 기존 클래스를 재정의 할 수 있다.
# 메타클래스 예시
print(type(5)) # <class 'int'>
print(type(int)) # <class 'type'>
print(type(type(5))) # <class 'type'>
"""
숫자 5의 타입은 int이며, int의 타입은 type이다.
즉, 'type'은 'int'의 메타클래스이다.
"""
- 구현 코드
class MetaSingleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances: # 인스턴스가 있으면 생성하지 않음
cls._instances[cls] = super(MetaSingleton,\
cls).__call__(*args, **kwargs)
return cls._instances[cls]
def show(self): # 상속 확인용 메서드
print("상속확인")
class Logger(metaclass=MetaSingleton):
pass
logger1 = Logger()
logger2 = Logger()
print(logger1, logger2)
print(logger1 is logger2)
"""
결과값. 한 개의 인스턴스를 생성한다.
<__main__.Logger object at 0x1028f7e80> <__main__.Logger object at 0x1028f7e80>
True
"""
print(logger1.show())
"""
결과값. 메타클래스는 메서드 상속이 안됨
Traceback (most recent call last):
File "*********", line 30, in <module>
print(logger1.show())
AttributeError: 'Logger' object has no attribute 'show'
"""
- 참고로 메타클래스는 상속과 달리 메서드를 상속하지 않는다.
함께 읽으면 좋은 글
파이썬 메타클래스 쉽고 깊게 이해하기, Python Metaclass A to Z
인스턴스 생성 내부동작
- __new__, __init__, __call__
어떤 클래스의 인스턴스가 생성되는 과정을 이해하기 위해서는 __new__, __init__, __call__ 에 대해서 알아야한다.
- __new__ : 클래스 인스턴스를 생성 (메모리 할당)
- __init__ : 생성된 인스턴스 초기화
- __call__ : 인스턴스 실행
흔히들, __init__ 이 생성자라고 생각하는데, 아니다. 생성은 __new__에서 한다. __init__에서는 생성된 인스턴스를 초기화를 하는 것이다. 대부분의 경우, 인스턴스의 생성 그 자체에는 관여하지 않기 때문에 해당 메스드를 재사용하지 않는다. 반면에, 개발자가 구현하고자하는 내용에 따라 초기화해줘야하는 변수들은 다양하므로 __init__ 메소드는 재사용하는 경우가 많다. 때문에, __init__이 생성자라고 착각하는 경우가 많은데, 사실은 그렇지 않은 것이다.
그러니까, 굳이 순서를 따지자면, __new__ -> __init__ -> __call__ 순이 되는 것이다.
데이터베이스 기반 싱글톤 사례
- 여러 서비스가 한 개의 DB를 공유하는 구조
- 데이터베이스의 일관성을 보존해야 한다. (충돌 방지)
- 다수의 DB 연산을 처리하려면 메모리와 CPU를 효율적으로 사용해야 한다.
- 구현 코드
import sqlite3
class MetaSingleton(type):
_instances = {}
def __call__(cls, *args, **kwargs): # 싱글톤 생성 로직
if cls not in cls._instances:
cls._instances[cls] = super(MetaSingleton,\
cls).__call__(*args, **kwargs)
return cls._instances[cls]
class Database(metaclass=MetaSingleton):
connection = None
def connect(self):
if self.connection is None:
self.connection = sqlite3.connect("db.sqlite3")
self.cursorobj = self.connection.cursor()
return self.cursorobj
db1 = Database().connect()
db2 = Database().connect()
print(f"{db1=}")
print(f"{db2=}")
"""
결과값. 동일한 커서 객체를 사용한다.
db1=<sqlite3.Cursor object at 0x1054045c0>
db2=<sqlite3.Cursor object at 0x1054045c0>
"""
- 앱이 DB연산을 요청할 때마다 Database 클래스를 호출하지만 인스턴스는 한 개만 생성된다.
- 동기화가 보장된다.
- 메모리와 cpu사용량을 최소화 한다.
- 단점
- 여러 앱이 같은 DB에 접속하는 상황이라면 각기 싱글톤을 생성하기 때문에 싱글톤 패턴에 적합하지 않다. DB 동기화가 어렵고 리소스 사용이 많아진다. 이런 때에는 연결 풀링 기법을 사용하는 것이 좋다.
인프라 상태 확인 싱글톤 사례
- 서버의 상태를 확인하는 구조
- 목록에 있는 서버의 상태를 체크한다.
- 만약 목록이 변동되면 바뀐 목록을 참조한다.
- 구현 코드
class HealthCheck:
_instance = None
def __new__(cls, *arg, **kwargs):
if not HealthCheck._instance:
HealthCheck._instance = super(HealthCheck, \
cls).__new__(cls, *arg, **kwargs)
return HealthCheck._instance
def __init__(self):
self._servers = []
def add_servers(self):
print("서버를 추가합니다.")
self._servers.append("Server 1")
self._servers.append("Server 2")
self._servers.append("Server 3")
self._servers.append("Server 4")
def del_server(self):
print(self._servers.pop()+" 서버를 삭제합니다.")
hc1 = HealthCheck()
hc2 = HealthCheck()
hc1.add_servers()
for server in hc1._servers:
print(f"hc1={server}")
"""
서버를 추가합니다.
hc1=Server 1
hc1=Server 2
hc1=Server 3
hc1=Server 4
"""
hc2.del_server()
hc2.del_server()
"""
Server 4 서버를 삭제합니다.
Server 3 서버를 삭제합니다.
"""
for server in hc1._servers:
print(f"hc1={server}")
print(hc1 is hc2)
"""
hc1=Server 1
hc1=Server 2
True
"""
- hc1 과 hc2는 동일 객체로 같은 상태를 공유하기 때문에 hc2로 서버를 삭제하면 hc1도 알 수 있다.
싱글톤 패턴의 단점
- 전역 변수의 값이 실수로 변경된 것을 모르고 사용될 수 있다.
- 같은 객체에 대한 여러 참조자가 생길 수 있다.
- 전역 변수에 종속적인 모든 클래스 간 상호관계가 복잡해진다.
- 전역 변수 수정이 의도치 않게 다른 클래스에도 영향을 줄 수 있다.
정리
- 싱글톤은 스레드 풀과 캐시, 대화 상자, 레지스트리 설정 등 한 개의 객체만 필요한 경우에 사용하기 적합하다.
- 싱글톤은 글로벌 액세스 지점을 제공하는, 단점이 거의 없는 검증된 패턴이다.
- 싱글톤 패턴의 단점은 전역 변수가 의도치 않게 다른 클래스에게 영향을 줄 수 있고, 리소스를 많이 사용하는 구조가 될 수 있다.
반응형
'나는 이렇게 본다 > 파이썬 디자인 패턴' 카테고리의 다른 글
퍼사드 패턴 (0) | 2022.05.31 |
---|---|
팩토리 메소드, 추상 팩토리 패턴 (0) | 2022.05.30 |
심플 팩토리 패턴 (0) | 2022.05.27 |
싱글톤 패턴, 게으른 초기화, 모듈 싱글톤 (0) | 2022.05.24 |
디자인 패턴 개요 (0) | 2022.05.23 |