본문 바로가기
Coding/설계 | 경험

파이썬 오픈소스 라이브러리 개발기

by Hide­ 2019. 12. 6.
반응형

1. 개요

지금으로부터 약 3주 전, pythondi(https://pypi.org/project/pythondi/) 라는 라이브러리를 개발하여 pypi에 오픈소스로 배포하였다. 그 과정에 대해 간단하게나마 후기를 남긴다.

 

2. 뭐하는 라이브러리인데?

pythondi는 무엇을 하는 라이브러리일까. 이에 대해 설명하기 위해서는 먼저 객체 지향 5대 원칙에 대해서 알아야한다. 이를 앞글자만 따서 SOLID라고 부르는데, 각 스펠링마다의 의미는 다음과 같다. (본 포스팅은 SOLID원칙에 대한 포스팅이 아니므로 간략하게만 설명한다)

 

S: Single Responsibility Principle(단일 책임 원칙) 

O: Open Closed Principle(개방 폐쇄 원칙)

L: Liskov Substitution Principle(리스코프 치환 원칙)

I: Interface Segregation Principle(인터페이스 분리 원칙)

D: Dependency Inversion Principle(의존성 역전 원칙)

 

이렇게 총 5개의 원칙 중 D에 해당하는 Dependency Inversion Principle 즉, 의존성 역전에 대한 것이다. 의존성 역전을 위해서는 일반적으로 Dependency Injection이라는 기술을 사용하는데, 이는 말 그대로 의존성을 주입해주는 것이다. 아마 자바 스프링 프레임워크를 사용하던 사람이라면 매우 익숙할 것이다. 하지만 파이썬의 경우 DI를 사용하는 예제를 쉽사리 찾아볼 수 없었고, 메이저하게 사용되는 라이브러리도 별로 없었다. 그러다보니 사용하면서 문제도 몇번 발생하였고, 내부적으로 어떻게 구성되어있는지 제대로 파악하지 못한채로 계속 사용하는것은 무리가 있다고 생각했다. 따라서 직접 라이브러리를 만들어보기로 결심했다.

 

3. 어떻게 만듬ㅋ

먼저 기존에 공개되어있던 라이브러리들을 Reference하여 비교분석하였다. 기본적인 사용법을 어떻게하면 편리하게 만들 수 있을까 생각하다가, 파이썬의 Type annotation을 이용하는 방법을 보았고 가장 편리하고 좋은 방법이라는 생각이 들었다. (또한 IDE차원에서의 자동 완성 기능도 얻을 수 있기 때문)

파이썬은 동적 언어이기 때문에 일반적으로 개발을 할 때 타입을 명시하지 않는다. 하지만 회사에서 일을 할 때 처럼 혼자 개발하는것이 아닌 동료들과 협업하는 경우에는 변수의 타입과 리턴 타입을 함께 명시해주는 것이 좋다. 그렇기에 나는 개발할 때 typing을 항상 사용하고 있었는데, 마침 잘됐다 싶었다. 기본적인 실행 아이디어는 다음과 같았다.

 

3번에 대해 간단하게 설명하기 위해 먼저 다음과 같이 Repo, SQLRepo 두개의 클래스가 있다고 가정해보자.

 

import abc


class Repo:
    """Interface class"""
    __metaclass__ = abc.ABCMeta

    @abc.abstractmethod
    def get(self):
        pass


class SQLRepo(Repo):
    """Impl class"""
    def get(self):
        print('SQLRepo')

Repo 클래스는 Interface 클래스이며 SQLRepo는 Interface를 상속받은 실 구현체이다. 

 

from pythondi import inject
from app.repositories import Repo


class Usecase:
    @inject()
    def __init__(self, repo: Repo):
        self.repo = repo

위 부분은 실제로 DI를 적용한 부분인데, 생성자 부분에 선언되어있는 repo인자의 type annotation에 실 구현체인 SQLRepo가 아닌 Interface객체인 Repo를 써놓은 모습을 볼 수 있다. 그리고 함수 윗 부분에 @inject() 데코레이터를 달아주는데, 위 데코레이터가 실 구현체 객체를 주입해준다.

한마디로 Usecase 입장에서는 실 구현체가 아닌 인터페이스에 의존하고 있고 외부에서 Usecase로 실 구현체를 주입시켜주는 형태이다. 따라서 위에서 설명했던 의존성 역전 법칙이 성립한다 . 라이브러리가 어떻게 동작하는지에 대해서는 조금 더 자세하게 설명하자면, 아래와 같은 흐름을 가진다.

 

provider = Provider()
provider.bind(Repo, SQLRepo)

1. Provider에 인터페이스 객체와 실 구현체 클래스를 각각 binding시킨다.

 

configure(provider=provider)

2. configure메소드를 사용하여 Container에 Provider를 등록한다.

 

from pythondi import inject


class Usecase:
    @inject()
    def __init__(self, repo: Repo):
        self.repo = repo

3. inject데코레이터를 사용하여 Container에 등록되어있는 Provider를 통해 객체를 주입시킨다.

 

특별하게 어려운 부분은 없지만 개발하면서 발생했던 몇가지 특이사항을 말해보자면,

 

class Container:
    """Singleton container class"""
    _instance = None
    _provider = None

    def __new__(cls, *args, **kwargs):
        if not Container._instance:
            Container._instance = super().__new__(cls, *args, **kwargs)
        return Container._instance

1. Container 클래스는 Singleton으로 구현해야한다. 위에 흐름에서 설명했던 것 처럼, Provider에 각각의 클래스를 바인딩하고 이를 Container에 등록시키는데, 각각의 다른 모듈에서 inject를 사용하는 경우에도 동일한 Provider를 사용해야하기 때문이다.

-> 어차피 클래스 변수로 선언하면 생성되는 모든 클래스에서 동일한 값으로 접근할 수 있으므로 싱글톤은 필요하지 않다. Borg pattern과 유사.

 

import threading
from typing import Optional, NoReturn

_LOCK = threading.RLock()


def configure(provider: Provider) -> Optional[NoReturn]:
    """Configure provider to container"""
    with _LOCK:
        if Container.get():
            raise InjectException(msg='Already injected')

    with _LOCK:
        Container.set(provider=provider)

2. 개인적으로 파이썬에서의 병렬처리는 GIL때문에 피하려고 하는 편이지만, 혹시나 모를 Race condition에 대처하기 위해 RLock을 통해 Thread-safe를 구현했다.

 

추가적으로 가장 핵심적인 부분인 inject 데코레이터를 한번 살펴보자.

 

def inject(**params):
def inner_func(func):
@wraps(func)
def wrapper(*args, **kwargs):
_provider = Container.get()

# Case of auto injection
if not params:
annotations = inspect.getfullargspec(func).annotations
for k, v in annotations.items():
if v in _provider.bindings:
kwargs[k] = _provider.bindings[v]()
# Case of manual injection
else:
for k, v in params.items():
kwargs[k] = v()
func(*args, **kwargs)
return wrapper
return inner_func

pythondi는 Provider를 통해 바인딩된 객체를 자동으로 주입해주는 방법과 @inject(repo=Repo) 처럼 주입 당시에 주입시킬 객체를 정의하는 두가지 방법을 제공한다. 첫번째 if문이 자동으로 주입하는 경우이고 else문이 직접 주입할 객체를 주입 당시에 정의하는 2가지 경우이다. 

잘 살펴보면, 데코레이터의 인자로 받은 함수의 type annotations를 뽑아와서 해당 annotation들 중 우리가 정의한 규칙에 맞는 클래스가 존재한다면, 해당 인자에 실 구현체 객체를 넣어주는 일을 한다. 이해하기 쉬운 내용이라 크게 더 설명할 건 없을 것 같다.

 

4. 배포하자

만들고 나서 보니 타 라이브러리들보다 더 직관적인 것 같고 사용법 또한 간단하다고 생각했다. 그럼 차라리 이걸 오픈소스로 배포하는게 좋지 않을까? 혹시나 누가 더 나은 코드를 포함한 PR을 날려준다면 그 또한 많은 공부가 되지 않을까? 뭐 이런저런 생각들이 머릿속을 스쳐갔고 오픈소스로 배포하기로 결정했다. 

기존에 필요한 프로그램들을 만들어도 오픈소스, pypi에 배포해본적은 없다. 그래서 조금 헤맸었는데, 직접 해보니 크게 어려운 부분은 없었다. pypi에 배포하는 방법은 https://hides.tistory.com/1054 에 자세하게 올려놨으니 참고하도록 하자.

 

5. 문서화는?

나는 제대로 문서화가 되지 않은 라이브러리를 굉장히 싫어한다. 사람들에게 편리함을 제공해주기 위해 오픈소스로 배포하였다면, 문서화까지 제대로 진행하여 사용하는데에 문제가 없도록 가이드해주는 것은 제작자의 당연한 의무이며 프로젝트의 마무리라고 생각하기 때문이다. 따라서 도큐먼트를 한번만 읽어도 바로 이해하고 자신들의 프로젝트에 적용할 수 있도록 해주고 싶었다.

찾아보니 readthedocs.org를 사용하면 따로 호스팅을 할 필요없이 도큐먼트 사이트를 구축할 수 있었다. 또한 그냥 사이트가 아닌 가장 직관적이고 이쁘게 만들고 싶었는데, 찾아보니 굉장히 마음에 드는 테마 또한 발견하였다. 문서화에 대한 방법 또한 https://hides.tistory.com/1055 에 올려놨다.

 

6. 후기

나는 내가 만든 프로그램이 다른 사람들에게 사용될 때 희열을 느낀다. 대상이 개발자이든 아니든 상관없다. 그냥 내가 타인에게 도움이 됐을 때, 그 순간이 가장 짜릿한 순간인 것 같다. 이번에 오픈소스로 프로그램을 제작하며, 타 오픈소스도 많이 분석해보았고 어떠한 부분에서는 많은 실력 향상도 가져올 수 있었다고 생각한다. 앞으로도 괜찮은 아이디어가 있다면 지속적으로 오픈소스에 기여하고 싶은 생각이다.  라이브러리의 소스는 깃허브에 올려놓았다. Pull request는 언제든지 환영한다!

 

Github - https://github.com/teamhide/pythondi

Pypi - https://pypi.org/project/pythondi/

Document - https://pythondi.readthedocs.io/en/latest/