싱글톤

지난 포스트에서 가상 환경 친화적 앱이 어떤 특성을 가져야 하는지 설명했다. 그 글에서 마지막에 소개한 Application Singleton을 이제 구현해보려고 한다.

싱글톤을 만드는 방법부터 시작하자.

주석

이 글은 파이썬 3.7.1 이 나온 시점에 쓰고있고, 파이썬 3.4 이상의 버전을 대상으로한다.

Singleton

위키피디아의 싱글턴 패턴 페이지를 인용하면, 싱글톤은 이런 의미다.

소프트웨어 디자인 패턴에서 싱글턴 패턴(Singleton pattern)을 따르는 클래스는, 생성자가 여러 차례 호출되더라도 실제로 생성되는 객체는 하나이고 최초 생성 이후에 호출된 생성자는 최초의 생성자가 생성한 객체를 리턴한다.

가령 싱글턴 클래스를 App 이라고 한다면 App() is App()True 가 된다는 뜻이다.

여기서 생성자를 문자 그대로 해석할 필요는 없다. 인스턴스를 만드는 팩토리 메서드도 생성자로 취급될 수 있기 때문에, 스타일이 좀 망가지기는 하지만 App() 대신 App.get_instance() 같은 방식을 사용해도 상관없다. 하지만 다음에 보이듯이 파이썬에서는 App() 스타일을 고수하면서도 싱글톤을 구현할 수 있다.

__new__()

생성자가 여러 번 호출되더라도 최초의 인스턴스를 만드는 가장 간단한 방법은 App 을 클래스가 아니라 팩토리 함수로 만드는 것이다. 어딘가 전역 변수에 인스턴스를 보관하는 자리를 만들어두고, 처음에는 인스턴스를 만들어 돌려주지만, 두 번째부터는 새로 만들지 않고 앞서 만들었던 인스턴스를 돌려주면 된다:

>>> class _App:
...     _instance = None

>>> def App():
...     if _App._instance is None:
...         _App._instance = _App()
...     return _App._instance

이렇게 하면 App() is App() 표현식은 항상 참이 된다.

하지만, 이 방법은 App 이 클래스가 아니라는 문제가 있다.

App 이 클래스가 되도록 유지하려면, App 의 생성자를 조작하는 대안을 고려해보자. 생성자라고 하면 __init__()를 떠올리고, 실제로도 그렇게 부르고는 있지만, __init__() 는 인스턴스를 만드는 주체는 아니고, 인스턴스를 초기화하는 역할만 맡고 있다. __init__() 가 호출될 때는 이미 만들어진 self 가 전달되고, __init__() 가 무슨 짓을 하건 그 인스턴스는 그대로 유지된다. 실제 인스턴스가 만들어지는 곳은 __new__() 다:

class App:
    _instance = None

    def __new__(cls):
        if App._instance is None:
            App._instance = super().__new__(cls)
        return App._instance

이제 App() is App() 은 항상 True 가 된다.

하지만, __init__() 가 여러 번 호출될 수 있다는 문제가 있다:

>>> App._instance = None
>>> class MyApp(App):
...     count = 0
...     def __init__(self):
...         super().__init__()
...         self.count += 1
...
>>> app1 = MyApp()
>>> app2 = MyApp()
>>> app1 is app2
True
>>> app1.count
2
>>> app3 = MyApp()
>>> app1.count
3
>>> app4 = App()
>>> app1.count
4

주석

App._instance = None 으로 시작한 이유는 앞에서 App 인스턴스를 만든 적이 있다면, 계속 같은 인스턴스를 돌려줘서, 앞으로의 실험이 의미가 없어지기 때문에, 새출발시킨 것이다. 테스트를 위해 App._instance = None 를 반복하는 것은 볼썽사나운 일이다. 이 글에서 설명하는 App 에 몇 가지 기능을 더 추가한 버전이 https://github.com/flowdas/flowdas/blob/master/flowdas/app/app.py 에 있다. 여기에는 메타 클래스에 컨택스트 관리자 프로토콜을 추가해서, 싱글톤을 제한된 컨텍스트 내에서만 기능을 구현하고 있다.

app1.count 가 2라는 뜻은 MyApp.__init__ 가 두 번 호출되었다는 것이다. 비록 인스턴스는 계속 같은 것이 오지만 생성자를 호출할 때마다 MyApp.__init__ 역시 반복적으로 호출된다. 재미있는 것은 마지막에 App() 을 호출해도 MyApp.__init__ 가 호출된다는 것이다.

이 문제를 해결해보자.

메타 클래스

인스턴스를 하나만 만들기 위해 첫 번째로 만들어진 인스턴스를 어딘가에 저장해두고 재활용한다는 아이디어를 반복하면, __init__() 가 실행되었다는 정보를 어딘가에 저장해두고, 두 번째부터는 호출하지 않는 방법을 시도해볼 수 있다.

어디에 저장할 것인가? 앞에서 사용한 것처럼 클래스 변수로 저장해도 좋지만, 이제 인스턴스가 있으니 인스턴스 변수로 저장하는 것도 좋다.

그러면 어떻게 호출하지 않도록 만들 것인가? 모든 __init__ 메서드를 찾아서 수정해주면 된다.

언제 찾고, 어떻게 수정하는가? 데코레이터를 사용할 수도 있지만, 사용자에게 불편을 주는 단점이 있다. 가장 무난한 방법은 특수 메서드 __init_subclass__()를 사용하는 것인데, 파이썬 3.6 에서 추가된 것이라, 이 글처럼 3.4 이상을 대상으로 할 때는 사용하기가 곤란하다. 그러면 마지막 남은 대안은 메타 클래스다:

class AppMeta(type):
    def __new__(mcs, name, bases, attrs):
        if '__init__' in attrs:
            ctor = attrs['__init__']

            def init(self):
                if not self._inited:
                    ctor(self)

            attrs['__init__'] = init
        return super().__new__(mcs, name, bases, attrs)


class App(metaclass=AppMeta):
    _instance = None
    _inited = False

    def __new__(cls):
        if App._instance is None:
            App._instance = super().__new__(cls)
        elif not isinstance(App._instance, cls):
            raise TypeError
        return App._instance

    def __init__(self):
        super().__init__()
        self._inited = True

클래스의 object.__new__() 가 인스턴스를 만들 때 호출되는 것처럼, 클래스를 만들 때는 메타 클래스의 type.__new__() 가 호출된다. (클래스는 메타 클래스의 인스턴스다.) 다만 뒤에 따라붙는 인자가 다른데, 이는 클래스가 만들어질 때(여기서는 클래스를 정의할 때) 메타 클래스의 생성자로 전달되는 인자다. 마지막 attrs 인자로 어트리뷰트와 메서드들이 딕셔너리 형태로 넘어온다. 여기에 __init__ 메서드가 들어있으면 (없을 수도 있다), self._inited 를 검사한 후 원래 __init__ 메서드를 호출하는 init 함수로 바꿔치기한다. init 함수가 호출할 원래 __init__ 는 클로저(closure) 참조(ctor)를 사용한다.

이제 class App(metaclass=AppMeta)를 사용해서 App 의 메타 클래스를 지정한다. 첫 App.__init__ 호출이 있고 난 뒤에 self._inited를 설정해서 더는 App.__init__를 호출하지 않도록 해야 한다. 주의해야 할 점은 App 에서 파생된 모든 서브 클래스들의 __init__ 가 호출되기 전까지는 설정되지 않아야 한다는 것이다. 모든 __init__ 들이 init로 교체되었기 때문에, 이 중 어느 한 곳도 self._inited를 설정하기에 적합하지 않다. 가장 좋은 장소는 베이스 클래스 App.__init__ 인데, App 의 모든 서브 클래스들의 __init__ 가 호출된 후에 마지막으로 호출됨이 보장되기 때문이다. 단 서브 클래스들이 다중 상속을 사용하지 않거나, super().__init__() 호출 규칙을 항상 지켜준다고 가정할 때만 그렇다. (어차피 다중 상속을 쓰면서 이 규칙을 지키지 않는다면 __init__를 한 번만 호출한다는 것은 애당초 불가능하니 심하게 걱정할 일은 아니다.)

App.__new__ 에는 혹시나 App() 을 먼저 호출한 후 서브 클래스의 생성자를 호출하는 일이 있을까 봐, isinstance() 검사를 추가했다. 서브 클래스 생성자가 베이스 클래스의 인스턴스를 돌려주는 것은 계약 위반이니까.

이제 다시 확인해보자:

>>> App._instance = None
>>> class MyApp(App):
...     count = 0
...     def __init__(self):
...         super().__init__()
...         self.count += 1
...
>>> app1 = MyApp()
>>> app2 = MyApp()
>>> app1 is app2
True
>>> app1.count
1
>>> app3 = MyApp()
>>> app1.count
1
>>> app4 = App()
>>> app1.count
1

isinstance() 검사가 잘 동작하는지도 확인해보자:

>>> App._instance = None
>>> app1 = App()
>>> MyApp()
Traceback (most recent call last):
  ...
TypeError

main

이제 싱글톤의 기본적인 기능이 완성되었다. App 에서 파생한 서브 클래스의 인스턴스를 먼저 만든다면, 그 이후로 호출되는 베이스 클래스들의 생성자를 호출하면 언제나 최초의 인스턴스를 돌려주게 된다. 이는 라이브러리를 만들 때 유용한 특성을 제공하게 되는데, 라이브러리에서는 응용 프로그램이 구체적으로 어떤 App 클래스를 사용하는지 알 필요 없이 App() 을 통해 그 인스턴스를 얻을 수 있기 때문이다. 하지만 여기에는 한가지 전제조건이 있다. 최초의 인스턴스를 응용 프로그램이 시작하자마자 만들어야 한다는 것이다. 이 조건을 만족시킬 최적의 장소는 응용 프로그램 구동 스크립트다.

응용 프로그램 구동 스크립트를 제공하는 메서드 main 을 제공한다:

class App(metaclass=AppMeta):
    class Command:
        ...

    ...

    @classmethod
    def main(cls, *, args=None):
        self = cls()
        return self.Command.main(args=args)

App.main 은 클래스 메서드이므로, 응용 프로그램이 정의한 서브 클래스가 cls로 전달된다. 이제 이 클래스의 인스턴스 self를 만든 후에, 뭔가 쓸모있는 작업을 하면 된다. 여기에서 예로든 코드는 명령 줄 인터페이스를 실행하는 것인데, Command 라는 클래스를 통해 제공된다. Command 클래스를 통한 명령행 인터페이스는 다음에 다룬다.

이제 setup.py 에 다음과 같은 방법으로 등록할 수 있다.

가령 응용 프로그램의 App 서브 클래스가 MyApp 이고, 이 클래스가 myapp 이라는 모듈에 정의되어 있고, myapp 이라는 이름의 스크립트를 등록하고 싶다면 이런 코드를 setup.py 에 삽입하면 된다:

entry_points={
    'console_scripts': [
        'myapp=myapp:MyApp.main',
    ],
},

이제 응용 프로그램이 myapp 이라는 스크립트를 통해 실행되는 한, MyApp 인스턴스가 제공되게 된다.

다음에는 표준 라이브러리의 argparse 모듈을 활용해서, 명령 줄 인터페이스를 정의하는 간편한 방법을 마련해보기로 하자.