Fastapi

fastapi 의존성

Prower 2022. 8. 7. 01:31
728x90
반응형
  • fastapi를 공부하면서 의존성을 주입할 때 어떻게 처리되는지 궁금하여 공부한 내용을 정리한다.

의존성 관리

  • 기존에 내가 사용하던 nestjs 프레임워크는 프레임워크 자체에서 DI container를 내장하고 있었고, provider를 통해 container 등록하여 사용하던 방식이었다.
  • fastapi는 조금 다른것으로 보인다. 따로 DI container는 존재하지 않으며, 요청이 들어오면 요청에 대한 처리를 위해 필요한 의존성만 주입받는다.
  • 의존성 주입이 가능한 객체는 callable한 객체이며, 실행을 통해 리소스를 반환해야 한다.

기본 작동 원리

main.py

from typing import Union
from fastapi import FastAPI,Depends
from service import Service
​
app = FastAPI()
​
@app.get("/")
async def main(service: Service = Depends(Service)):
    return service.getFoo()

service.py

class Service:
    def __init__(self):
        print("init service")
        self.foo = "foo"
        pass
​
    def getFoo(self):
        print("getFoo")
        return self.foo
  • 간단한 예시로 확인해본다 Service 클래스를 구현하고 controller에서 의존한다.
  • 따로 구현하지 않았지만, Service 클래스에 __call__ 함수가 존재하며, 이는 Service의 생성자를 호출해 인스턴스를 생성하고 이를 반환한다.
  • 요청이 들어오면 의존성 주입을 통해 __call__ 함수가 실행되며 Service 의 인스턴스가 주입된다.

실행 결과

init service
getFoo

호출 횟수

  • fastapi 는 의존성을 주입하는 시점에 __call__ 메서드가 실행되며, 이는 하나의 request context 내에서 한번만 실행된다.
  • 즉, 하나의 request context 내에서 생성된 인스턴스는 싱글턴으로 작동한다는 의미도 된다.

main.py

from typing import Union
from fastapi import FastAPI,Depends
from repository import Repository
from service import Service
​
app = FastAPI()
​
@app.get("/")
async def main(
    service: Service = Depends(Service),
    repo: Repository = Depends(Repository)
):
    return service.getFoo()

service.py

from fastapi import Depends
from repository import Repository
​
class Service:
    def __init__(self, repo: Repository = Depends(Repository)):
        print("init service")
        self.repo = repo
        pass
​
    def getFoo(self):
        print("getFoo")
        return self.repo.getQuery()

repository.py

class Repository:
    def __init__(self):
        print("init repository")
        pass
​
    def getQuery(self):
        print("getQuery")
        return "query"
  • (이렇게 사용할 일은 없겠지만) Repository 클래스를 정의하고, 이를 Service 와 controller에서 주입했다.

실행 결과

init repository
init service
getFoo
getQuery
  • 요청을 실행하면 repository의 생성자가 한번만 실행되는 것을 볼 수 있다.
  • 주목할 점은 Service 생성자를 호출하기 전에 Repository 의 생성자를 를 먼저 실행하는 것을 볼 수 있다.
  • 위 예시에서는 당연하게도 Service 인스턴스를 생성하려면 먼저 Repository 의 인스턴스가 생성되어야 한다.

메서드를 통한 주입

  • 의존성을 주입할 때 callable한 객체를 주입해야 한다고 했다.
  • 메서드 또한 callable하며, 이를 통해 의존성을 주입할 수 있다.

provider.py

class Connection:
    def runQuery(self):
        print("runQuery")
        return "query"

def connectionProvider():
    print("connectionProvider")
    return Connection()

repository.py

from fastapi import Depends
from provider import Connection, connectionProvider

class Repository:
    def __init__(self, con: Connection = Depends(connectionProvider)):
        print("init repository")
        self.con = con
        pass

    def getQuery(self):
        print("getQuery")
        return self.con.runQuery()

main.py

from typing import Union
from fastapi import FastAPI,Depends
from provider import Connection, connectionProvider
from repository import Repository
from service import Service

app = FastAPI()

@app.get("/")
async def main(
    service: Service = Depends(Service),
    repo: Repository = Depends(Repository),
    con: Connection = Depends(connectionProvider),
):
    return service.getFoo()
  • 위 예시에서 3가지만 변경되었다. connectionProvider 라는 메서드를 통해 Connection 인스턴스를 생성하여 반환한다.
  • controller와 RepositoryconnectionProvider 를 통해 생성된 인스턴스를 주입받는다.

실행 결과

connectionProvider
init repository
init service
getFoo
getQuery
runQuery
  • 마찬가지로, 메서드를 통한 방법도 하나의 request context 내에서 한번만 실행되며, 실행되는 순서도 클래스를 통한 방법과 동일하다.

Scope

  • 사실 이 글을 쓰는 가장 주된 목적이다.
  • 이전에 사용했던 는 3개의 injection scope가 제공되었다.
    • DEFAULT: 전체 어플리케이션 실행 동안 한번만 생성된다
      • ex) 비즈니스 로직을 실행하는 service
    • REQUEST: 하나의 request context 내에서 한번만 생성된다.
      • ex) context를 사용하는 로거, connection session
    • TRANSIENT: 주입 될 때 마다 새로 생성된다.
      • 이 scope는 사용해 본 적이 없어서 예시를 모르겠다. 실제로 사용할 일이 있는지도 모르겠다.
  • injection scope
  • nestjs
  • 지금까지 알아본 바로 fastapi에서는 위에서 REQUEST scope만 제공되는 것으로 보인다.
  • TRANSIENT는 제쳐두고, scope를 DEFAULT로 설정이 가능한 방법이 있는지 생각해 보았다.

DEFAULT scope

  • 보통 전체 어플리케이션에서 한번만 생성하여 공통적으로 사용하는 케이스는 전체 어플리케이션의 설정값 정도가 있겠다.
  • fastapi를 실행하는 시점에 모든 모듈이 평가된다. 평가되는(혹은 application bootstrap되는) 시점에 인스턴스를 생성(혹은 싱글턴으로 생성)하여 해당 인스턴스를 주입한다.

config.py

class Config:
    def __init__(self):
        self.appConfig = {}
​
config = Config()
​
def bootstrap():
    config.appConfig["appkey"] = "testing"

service.py

from configs import Config, config
from fastapi import Depends
from repository import Repository
​
class Service:
    def __init__(
        self,
        repo: Repository = Depends(Repository),
        config: Config = Depends(lambda: config)
    ):
        print("init service")
        print(config.appConfig)
        self.repo = repo
        pass
​
    def getFoo(self):
        print("getFoo")
        return self.repo.getQuery()

main.py

from configs import bootstrap
from fastapi import FastAPI,Depends
from provider import Connection, connectionProvider
from repository import Repository
from service import Service
​
bootstrap()
​
app = FastAPI()
​
@app.get("/")
async def main(
    service: Service = Depends(Service),
    repo: Repository = Depends(Repository),
    con: Connection = Depends(connectionProvider),
):
    return service.getFoo()
  • 조금 조잡하지만, Config 가 추가되고 이를 Service 에서 주입 받는 상황을 가정한다
  • app 실행시 Config 는 선언과 동시에 모듈을 평가하는 과정에서 인스턴스가 생성되고, bootstrap 과정에서 필드가 초기화 된다.
  • 이렇게 생성된 Config 인스턴스는 Service 에 주입된다.
  • 생성 과정에서 다른 의존성 주입을 통하지 않은 모듈 평가 과정에서 생성된 인스턴스 이기 때문에 어플리케이션 전체 context에서 한번만 생성이 보장된다.
728x90
반응형