본문으로 바로가기

Python Dependency Injection

category Coding/설계 | 경험 2019. 11. 9. 19:39
반응형

오늘은 SOLID원칙 중 하나인 Inversion of Control(의존성 역전)에 대해 설명하겠다. 의존성 역전을 구현하려면 Dependency Injection라는 기법을 사용해야 한다. 관련 정보를 인터넷에 검색하면 대부분 자바의 스프링 프레임워크 예제가 나온다. 그만큼 스프링에서는 DI를 사용하는것이 일반적이며 널리 알려진 방법중에 하나이다. 하지만 파이썬에 관한 자료는 정말 찾기 힘들다. 인터넷에 공개되어있는 코드를 봐도 DI를 적용한 예제는 현재까지 본적이 없다. 그나마 몇가지 라이브러리들이 존재하긴 하는데, 실제로 내부에서 어떠한 형태로 돌아가는지 파악하기가 힘들어서 직접 구현해보고 그 과정을 이렇게 포스팅으로 남긴다. 먼저 다음과 같은 코드가 있다고 가정해보자.

 

class Repo:
    def get(self):
        return session.query(User).get(1)


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

 

소스를 보면 Usecase 클래스의 생성자에서 Repo객체를 생성하고 있음을 확인할 수 있다. 이러한 경우 Repo클래스에 변경이 생기면, Usecase 클래스의 동작에도 영향을 미친다. 따라서 UsecaseRepo는 서로 의존성을 가지고 있다고 볼 수 있다. 이러한 걸 강한 결합이라고도 표현하는데, 모든 객체사이는 느슨한 결합(Loose Coupling)이 이루어져야 한다. (느슨하게 결합되어 있다는 것은 서로 상호작용하지만 서로의 세부 내용에 대해 알 지 못해도 정상적으로 동작하는 관계이다) 강한 결합으로 묶여있는 상태라면 하나의 객체에 수정이 가해졌을 때 묶여있는 타 객체에도 영향을 미치기 때문이다. 이러한 상황은 리팩토링 시 가장 강력하게 불편사항으로 발생한다. (주관적인 의견)

 

그렇다면 의존성 주입이란 무엇일까? 위 예제의 상황에서 생각해보자면, 먼저 Repo의 인터페이스 객체를 하나 생성하고 세부 구현체도 따로 만들어준다. 그리고 Usecase는 Repo의 세부 내용을 알지 못하도록 인터페이스 객체를 바라보도록 만든다. 그리고 실제 사용할때는 구현체가 사용될 수 있도록 해주면 된다.

import abc


class Repo:
    __metaclass__ = abc.ABCMeta

    @abc.abstractmethod
    def get(self):
        pass


class MySQLRepo(Repo):
    def get(self):
        return session.query(User).get(1)


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

 

파이썬의 ABC모듈을 사용하여 인터페이스 클래스인 Repo를 생성했다. 그리고 인터페이스를 구현한 실 구현체 클래스인 MySQLRepo에서 인터페이스를 상속받고 세부 내용을 구현했다. 그리고 Usecase는 Repo모듈을 바라보도록 만들었다. 이제 Usecase를 사용할 때 MySQLRepo를 생성자에 넘겨주면 될까? 많은 방법이 있겠지만, 공개된 라이브러리 중 inject라는 라이브러리가 있다. 해당 라이브러리의 사용법을 한번 살펴보자.

import abc
import inject


class Repo:
    __metaclass__ = abc.ABCMeta

    @abc.abstractmethod
    def get(self):
        pass


class MySQLRepo(Repo):
    def get(self):
        return session.query(User).get(1)


class Usecase:
    @inject.autoparams()
    def __init__(self, repo: Repo):
        self.repo = repo
        
        
if __name__ == '__main__':
    usecase = Usecase()
    usecase.repo.get()

위처럼 의존성을 주입시킬 함수위에 @inject.autoparams()라는 데코레이터를 입력해주면, 함수의 type annotation을 파싱하여 사용자가 미리 정의해둔 규칙에 따라 의존성을 주입시켜준다. (위 예제에서는 미리 Repo에 MySQLRepo를 주입시키도록 규칙을 설정해놨다고 가정한다) 본 포스팅에서도 위처럼 annotation에 인터페이스 클래스를 명시해두고 실제 객체 생성때에는 아무런 인자를 넘기지 않아도 자동으로 DI가 되도록 동작하는 데코레이터를 만들어 볼 것이다.

import abc
import inject


class Repo:
    __metaclass__ = abc.ABCMeta

    @abc.abstractmethod
    def get(self):
        pass


class MySQLRepo(Repo):
    def get(self):
        print('MySQLRepo')


providers = {
    Repo: MySQLRepo,
}

먼저 위처럼 객체 2개를 생성하고 딕셔너리에 저장시킨다.

 

import inspect
from functools import wraps


def inject(func):
	@wraps(func)
	def wrapper(*args, **kwargs):
		annotations = inspect.getfullargspec(func).annotations

        for k, v in annotations.items():
	        if v in providers:
    		    kwargs[k] = providers[v]()
        return func(*args, **kwargs)
    return wrapper

그리고 위처럼 데코레이터를 작성한다. inspect의 getfullargspec이라는 함수를 사용하면 주어진 객체의 인자에 관한 정보를 추출할 수 있는데, annotations안에는 우리가 지정한 type annotation 정보들이 들어가있다. 위 코드를 하나하나 설명해보자면 다음과 같다.

 

1. 객체의 annotation들을 모두 추출한다.

2. 추출한 annotation중 우리가 DI를 하도록 지정한 규칙에 맞는 annotation이 존재한다면 해당 인자에 주입시킬 객체를 넣어준다.

3. 실제 함수를 실행하며 데코레이터를 종료한다.

 

이해하기 어려운 부분이 없을 정도로 깔끔한 코드이다. 완성된 코드는 다음과 같다.

import abc
import inspect
from functools import wraps


class Repo:
    __metaclass__ = abc.ABCMeta

    @abc.abstractmethod
    def get(self):
        pass


class MySQLRepo(Repo):
    def get(self):
        print('MySQLRepo')


providers = {
    Repo: MySQLRepo,
}


def inject(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        annotations = inspect.getfullargspec(func).annotations

        for k, v in annotations.items():
            if v in providers:
                kwargs[k] = providers[v]()
        return func(*args, **kwargs)

    return wrapper


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


if __name__ == '__main__':
    usecase = Usecase()
    usecase.repo.get()

main을 보면 Usecase 객체를 생성할 때 repo인자에 아무런 값도 주지 않았다. 하지만 실제 위 코드를 구동시켜보면, MySQLRepo 즉, 실 구현체의 get() 함수가 실행되어 MySQLRepo라는 값을 출력하는것을 확인할 수 있다.

 

위 예제에서는 DI에 관해 자세히 설명하지 않았다. 기존에도 수많은 관련 정보들이 많기 때문이다. 그렇기 때문에 이론적인 부분보다는 실제 파이썬 코드를 통해 구현하는 과정에 대해 서술했다. 예전에 Dependency Injection이라는 개념을 처음 접했을 때 이해하기 정말 힘들었다. 왜 사용하는지 의문이 들었고 파이썬에 관한 예제를 살펴보며 이해를 하고 싶었는데, 관련 예제가 없어서 힘들었던 경험이 있다. 본 포스팅을 통해 같은 고충을 겪고 있는 사람들에게 조금이나마 도움이 되기를 바란다.

 

소스 : https://github.com/teamhide/pythondi

반응형