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와 Repository 는 connectionProvider 를 통해 생성된 인스턴스를 주입받는다.
실행 결과
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는 사용해 본 적이 없어서 예시를 모르겠다. 실제로 사용할 일이 있는지도 모르겠다.
- DEFAULT: 전체 어플리케이션 실행 동안 한번만 생성된다
- 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
반응형