Python/FastAPI / / 2023. 1. 28. 15:11

FastAPI 알아보기 - 클래스로서 의존, Dependency Injection, Dependency Injector

What is Dependency Injector?

Dependency Injector는 파이썬의 Dependency Injection(이하, DI)을 위한 프레임워크다. 먼저 소개글을 보겠다.

Originally dependency injection pattern got popular in languages with static typing like Java. Dependency injection is a principle that helps to achieve an inversion of control. A dependency injection framework can significantly improve the flexibility of a language with static typing. Implementation of a dependency injection framework for a language with static typing is not something that one can do quickly. It will be a quite complex thing to be done well. And will take time.

기본적으로 의존성주입 패턴은 자바같은 정적 타 언어에서 유명해진 패턴입니다. 의존성주입은 Inversion of Control(IoC)를 성취하기 위한 원칙이다.  의존성 주입 프레임워크는 정적 타입과 같은 언어에서 유연성에 상당한 상승을 기여한다. 정적 타입 언어를 위한 의존성주입 프레임워크 실행은 누군가 빨리 할 수 있는 것은 아니다. 이를 잘하기 위해서는 꽤나 복잡한 일이다. 그리고 시간도 걸린다.

Python is an interpreted language with dynamic typing. There is an opinion that dependency injection doesn’t work for it as well as it does for Java. A lot of the flexibility is already built-in. Also, there is an opinion that a dependency injection framework is something that Python developer rarely needs. Python developers say that dependency injection can be implemented easily using language fundamentals.

파이선은 동적 타이핑을 하는 인터프리터 언어이다. 자바에서 하는 만큼 의존성 주입이 잘 작동하지 않는 의견도 있다. 대부분의 유연성이 이미 빌트인 되어있기 때문이다. 또한 파이썬 개발자에게 의존성 주입 프레임워크는 거의 필요하지 않다는 의견도 있다. 파이썬 개발자들은 의존성 주입은 언어의 기본을 잘 사용하면 쉽게 할 수 있다고 한다.

 

그리고 Dependency Injection에 대해서 설명하고 있는데, 이에 대해 괜찮은 이야기 인것 같아서 다시 한 번 이야기하고 넘어가려고 한다.

Coupling(커플링)과 Cohesion(응집력)

커플링과 응집력은 컴포넌트들이 얼마나 강하게 묶여 있는지에 대한 이야기이다.
- High Coupling(높은 커플링) : 커플링이 높으면, 강력 접착제같은 것을 사용한 것과 같다. 다시 떼기가 힘들다.
- High Cohension(높은 응집력) : 응집력이 높으면, 커플링과 반대로 분리하고 다시 붙이기 쉽다.

결론

- 유연성 : 컴포넌트들은 약하게 커플링된다. 당신은 쉽게 시스템의 기능을 컴포넌트를 다른 방식으로 엮어서 확장하거나 바꿀 수 있다. 
- 시험 가능성 : Mock을 API나 데이터베이스같은 실제 오브젝트 대신에 주입 가능하기에, 테스트가 더 쉬워진다.
- 명확함과 유지 보수성 : 의존성주입은 의존성을 드러나게 도와준다. 즉, 애매함을 명확하게 해준다. 그리고 PEP20 - 파이썬의 선에서 나온 것처럼, "애매한 것보다 명확한게 낫다"를 실현 가능하다. 컨테이너 속 컴포넌트와 의존성을 명확하게 정의 가능하다. 이는 곧 전체 청사진과 앱 구조를 제공한다. 쉽게 이해 가능하고 바꿀 수 있다. 

Classes as Dependencies

from fastapi import Depends, FastAPI

app = FastAPI()


async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100):
    return {"q": q, "skip": skip, "limit": limit}


@app.get("/items/")
async def read_items(commons: dict = Depends(common_parameters)):
    return commons


@app.get("/users/")
async def read_users(commons: dict = Depends(common_parameters)):
    return commons

 

무엇이 의존성을 만드는가?

async def common_parameters(
    q: Union[str, None] = None, skip: int = 0, limit: int = 100
):

위의 코드를 보면 의존들이 함수로 정의된 것을 알 수 있다. 하지만 이것이 의존성을 선언하는 유일한 방법은 아니다. 중요한 요소는 바로 의존이 "callable(호출 가능성)"이 되어야 한다는 것이다. 그래서 만약 어떤 오브젝트 something을 호출한다고 하면 다음과 같을 것이다.

something()
something(some_argument, some_keyword_argument="foo")

그러면 한번 이를 클래스로 만들어보자.

class Cat:
    def __init__(self, name: str):
        self.name = name


fluffy = Cat(name="Mr Fluffy")

이 상황에서는 fluffy는 Cat 클래스의 인스턴스이다. fluufy를 만드는 것을 통해 Cat을 부르게 된다. 그리고 FastAPI에서 파이썬 클래스를 의존성으로 사용 가능하다. FastAPI에서 실질적으로 체크하는 것은 "호출 가능성(함수, 클래스 혹은 아무거나)"이다. 만약 "callable"들을 FastAPI의 의존으로 넘긴다면, FastAPI는 파라미터를 callble을 위해 분석하고, "Path 실행함수"처럼 동일하게 처리한다.

from typing import Union

from fastapi import Depends, FastAPI

app = FastAPI()


fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]


class CommonQueryParams:
    def __init__(self, q: Union[str, None] = None, skip: int = 0, limit: int = 100):
        self.q = q
        self.skip = skip
        self.limit = limit


@app.get("/items/")
async def read_items(commons: CommonQueryParams = Depends(CommonQueryParams)):
    response = {}
    if commons.q:
        response.update({"q": commons.q})
    items = fake_items_db[commons.skip : commons.skip + commons.limit]
    response.update({"items": items})
    return response

위의 코드를 자세하게 분석해 보겠다. 위의 코드에서 클래스의 __init__은 사실 함수에서 정의하면 다음과 같다.

def __init__(self, q: Union[str, None] = None, skip: int = 0, limit: int = 100):
async def common_parameters(q: Union[str, None] = None, skip: int = 0, limit: int = 100):

위으 파라미터들이 FastAPI가 의존성을 풀기위해 사용하는 것이다. 그리고 이를 사용하면 다음과 같다.

async def read_items(commons: CommonQueryParams = Depends(CommonQueryParams)):

 

타입 힌팅 VS Depends

commons: CommonQueryParams = Depends(CommonQueryParams)
... = Depends(CommonQueryParams)
commons: CommonQueryParams ...
commons = Depends(CommonQueryParams)

위의 코드에서는 CommonQueryParams를 힌팅과 파라미터로 동시에 사용했다. 하지만 Depends(CommonQueryParma)가 실질적으로 의존성을 푸는 인자이며 FastAPI가 실질적으로 호출하는 것이다. 따라서 타입힌팅을 쓸 필요 없이,

commons = Depends(CommonQueryParams)

위의 코드처럼 쓰면 된다. 그 외에도 타입힌팅을 사용해서

commons: CommonQueryParams = Depends()

와 같이 써도 상관없다.

 

다음 글에서는 PathParameter에 대해서 알아보겠다.

728x90
반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유