Borg 패턴

  • 싱글턴 패턴은 하나의 인스턴스만 생성되고 사용되지만 Borg 패턴은 여러 인스턴스가 생성되지만 공유되는 하나의 상태(state)를 가진다.
  • 같은 클래스의 모든 인스턴스는 하나의 상태를 공유한다는 뜻이다.

실제 코드를 통해 알아보자.

class BaseFetcher:
    _attributes = {}

    def __init__(self, source):
       self.source = source

    def set_attribute(self, child, source):
       child.__dict__ = self.__class__._attributes


class TagFetcher(BaseFetcher):
  def __init__(self, source):
    super().set_attribute(self, source)
    super().__init__(source)

  def pull(self):
    print(self.source, "태그에서 풀")
    return f"Tag = {self.source}"


class BranchFetcher(BaseFetcher):
  def __init__(self, source):
    super().set_attribute(self, source)
    super().__init__(source)

  def pull(self):
    print(self.source, "브랜치에서 풀")
    return f"Branch = {self.source}"


if __name__ == "__main__":
  f1 = TagFetcher(0.1)
  f1_id = id(f1.source)
  f2 = BranchFetcher(0.2)
  f2_id = id(f2.source)
  print("f1", f1_id)
  print("f2", f2_id)
  f1.pull()
  f2.pull()


//f1 4330054256
//f2 4330345264
//0.2 태그에서 풀
//0.2 브랜치에서 풀

결과를 살펴보자면 f1 과 f2 는 서로 다른 class 로 생성되었으며 id 값을 통해 별개의 객체임이 확인되었다. 하지만 pull 했을 경우 똑같은 값이 출력된다. 어떻게 된 것인지 자세히 살펴보자.

우선 f1.__dict__ 와 f2.__dict__ 를 출력했을 때 {“source” : 0.2} 가 저장되어있는 것을 확인할 수 있다.

그렇다면 각 __dict__ 의 id 값은 어떨까?

image

실제 결과값을 보면 f1, f2 는 서로 다른 객체이지만 같은 __dict__ 를 가지고 있는 것을 확인할 수 있다. 그렇다면 __dict__ 는 어디에 위치해 있는 것인가??

image

결론부터 말하자면 f1, f2 의 __dict__ 는 BaseFetcher의 __dict__ 가 아니라 __dict__[“_attributes”] 이다. 이는 BaseFetcher 의 set_attribute() 에서 f1, f2 의 __dict__ 를 BaseFetcher 클래스의 _attributes 와 매핑시켰기 때문이다. 그렇기 때문에 마지막에 출력하는 BaseFetcher.__dict__[“_attributes”] 의 주소값이 동일하게 나오는 것을 확인할 수 있다.

결국 f1, f2 는 서로 다른 객체이지만 각 __dict__ 의 주소를 BaseFetcher 의 _attributes 에 모두 매핑시켜 동기화가 되는 것이다.

이처럼 서로 다른 객체이지만 같은 상태를 공유하는 것이 Borg 패턴이다.

다시 실행 코드를 정리해보자면

  1. F1 이 생성되고 F1 의 __dict__ 는 BaseFetcher 의 _attribute 에 매핑되어 source = 0.1 이 저장된다.
  2. F2 가 생성되고 F2 의 __dict__ 역시 BaseFetcher 의 _attribute 에 매핑되어 해당 source = 0.2 를 저장한다.
  3. 이때 BaseFetcher._attribute = { “source” : 0.2 } 이렇게 저장되어있고 F1, F2 가 해당 주소를 가르키고 있기 때문에 F1.pull(), F2.pull() 모두 BaseFetcher._attribute 에서 source 값을 가져오기에 동일한 값이 나올 수 밖에 없다.

Borg Pattern with Mixin

Borg 패턴을 Mixin 과 결합해보자.

import logging


class SharedAllMixin:
    def __init__(self, *args, **kwargs):
        try:
            __class__._attributes
        except AttributeError:
            __class__._attributes = {}
            self.__dict__ = __class__._attributes
            super().__init__(*args, **kwargs)


class BaseFetcher:
    def __init__(self, source):
        self.source = source


class TagFetcher(SharedAllMixin, BaseFetcher):
    def pull(self):
        logging.info("%s 태그에서 풀", self.source)
        return f"Tag = {self.source}"


class BranchFetcher(SharedAllMixin, BaseFetcher):
    def pull(self):
        logging.info("%s 브랜치에서 풀", self.source)
        return f"Branch = {self.source}"


if __name__ == "__main__":
    f1 = TagFetcher(0.1)
    print(f1.pull())
    f2 = BranchFetcher(0.2)
    print(f1.pull())
    print(f2.pull())
    print(f1.__dict__)
    print(f2.__dict__)

위의 결과값을 예상해보자면 f1 과 f2 는 서로 다른 id 값을 가진 별개의 객체이지만 각 __dict__ 의 id 값은 동일할 것이다. 그 이유는 SharedAllMixin 의 생성자에 서 _attributes 를 생성하고 self.__dict__ 를 매핑한다. f1 인 TagFetcher 가 생성되지만 다중 상속 을 받고 있기 때문에 SharedAllMixin 생성자가 먼저 생성되고 BaseFetcher 의 생성자가 바로 동작 한다. 이후 source 값들이 업데이트 되지만 f1.__dict__ 는 이미 SharedAllMixin 의 _attributes 를 가르키고 있다.

image

결과를 확인해보면 예상과 동일하게 f1, f2 의 id 는 다르지만 첫 예시와 동일하게 각 __dict__ 는 부모클래스의 _attribute 주소와 동일한 것을 확인할 수 있다.

++

위 코드를 진행하던 중 두 가지 의문이 들었다.

  1. Mixin 에서 self.__dict__ = __class__._attributes 매핑하였고, 처음 Borg 패턴에서는 child.__dict__ = self.__class__._attributes 로 매핑했다는 점
  2. 다중 상속 시에 SharedAllMixin 에서 super 를 사용하는 점이다.

1. Python Self

간단하게 말하자면 Python 의 self 는 현재 인스턴스를 나타낸다. 즉, 처음 Borg 패턴에서 굳이 child 로 인스턴스를 넘겨주지 않더라도 self.__dict__ 에 매핑한다면 똑같은 결과를 얻을 수 있다.

image

실제 모든 init 함수 내에서 print(self) 를 했을 경우 모두 동일한 인스턴스를 반환하는 것을 확인할 수 있다.

2. super()

super 는 Python 이기에 사용한다. 다중 상속하는 경우에는 MRO 에 따라서 다음 클래스를 가르킬 때 super 를 사용한다. 여기서는 SharedAllMinxin 과 BaseFetcher 를 상 속받고 있고 처음에 SharedAllMinxin 를 상속 받은 이후 다음 BaseFetcher 를 상속 받기 위해서는 super() 으로 다음 클래스를 가르켜야한다는 것이다. 즉, super().__init__ 은 BaseFetcher.__init__ 을 호출한다.

추가 두 가지 사항에 대해서는 Python 글에서 더 자세히 다루도록 하겠다.

업데이트:

댓글남기기