나는 이렇게 본다/파이썬 디자인 패턴

모노스테이트, 메타클래스, 싱글톤 구현예시

daco2020 2022. 5. 25. 17:52
반응형

모노스테이트 싱글톤 패턴

  • 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

인스턴스 생성 내부동작

  1. __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도 알 수 있다.





싱글톤 패턴의 단점

  • 전역 변수의 값이 실수로 변경된 것을 모르고 사용될 수 있다.
  • 같은 객체에 대한 여러 참조자가 생길 수 있다.
  • 전역 변수에 종속적인 모든 클래스 간 상호관계가 복잡해진다.
  • 전역 변수 수정이 의도치 않게 다른 클래스에도 영향을 줄 수 있다.



정리

  • 싱글톤은 스레드 풀과 캐시, 대화 상자, 레지스트리 설정 등 한 개의 객체만 필요한 경우에 사용하기 적합하다.
  • 싱글톤은 글로벌 액세스 지점을 제공하는, 단점이 거의 없는 검증된 패턴이다.
  • 싱글톤 패턴의 단점은 전역 변수가 의도치 않게 다른 클래스에게 영향을 줄 수 있고, 리소스를 많이 사용하는 구조가 될 수 있다.





반응형