하위 의존성
하위 의존성 (Sub-dependencies)
하나의 의존성이 또 다른 의존성에 의존할 수 있어요. 그리고 그 의존성이 또 다른 의존성에, 또... 이렇게 **체인(chain)**을 얼마든지 깊게 만들 수 있답니다. FastAPI가 그 해석(solve)을 전부 알아서 해 줘요. 이 글에서는 이 하위 의존성 구조가 어떻게 동작하는지 차근차근 살펴볼게요.
출처: 공식문서
하위 의존성(sub-dependencies)을 가진 의존성을 만들 수 있어요.
필요한 만큼 깊게 만들 수 있고요.
FastAPI가 해석(solving)을 처리해 줍니다.
첫 번째 의존성 "dependable"
이렇게 첫 번째 의존성("dependable")을 만들 수 있어요:
from typing import Annotated
from fastapi import Cookie, Depends, FastAPI
app = FastAPI()
def query_extractor(q: str | None = None):
return q
def query_or_cookie_extractor(
q: Annotated[str, Depends(query_extractor)],
last_query: Annotated[str | None, Cookie()] = None,
):
if not q:
return last_query
return q
@app.get("/items/")
async def read_query(
query_or_default: Annotated[str, Depends(query_or_cookie_extractor)],
):
return {"q_or_cookie": query_or_default}
이건 선택적(optional) 쿼리 파라미터 q를 str로 선언하고, 그냥 그 값을 반환해요.
아주 단순해서(그렇게 유용하지도 않고요) 오히려 하위 의존성이 어떻게 동작하는지에 집중하기 좋답니다.
두 번째 의존성 "dependable"이면서 "dependant"
이번엔 자기 자신이 의존성이면서 동시에 자기만의 의존성을 선언하는 함수("dependant"이기도 한 "dependable")를 만들어 볼게요:
from typing import Annotated
from fastapi import Cookie, Depends, FastAPI
app = FastAPI()
def query_extractor(q: str | None = None):
return q
def query_or_cookie_extractor(
q: Annotated[str, Depends(query_extractor)],
last_query: Annotated[str | None, Cookie()] = None,
):
if not q:
return last_query
return q
@app.get("/items/")
async def read_query(
query_or_default: Annotated[str, Depends(query_or_cookie_extractor)],
):
return {"q_or_cookie": query_or_default}
선언된 파라미터를 눈여겨볼게요:
- 이 함수는 그 자체로 의존성("dependable")이면서도, 다른 의존성을 선언하고 있어요("무언가에 의존한다"는 뜻의 "dependant"죠).
query_extractor에 의존하는데, 그 함수가 반환한 값을q파라미터에 할당해요.
- 또
last_query라는 선택적 쿠키를str로 선언하고 있어요.- 사용자가 쿼리
q를 제공하지 않았다면, 이전에 쿠키에 저장해 둔 마지막 쿼리를 사용해요.
- 사용자가 쿼리
의존성 사용하기
이제 이 의존성을 이렇게 사용할 수 있어요:
from typing import Annotated
from fastapi import Cookie, Depends, FastAPI
app = FastAPI()
def query_extractor(q: str | None = None):
return q
def query_or_cookie_extractor(
q: Annotated[str, Depends(query_extractor)],
last_query: Annotated[str | None, Cookie()] = None,
):
if not q:
return last_query
return q
@app.get("/items/")
async def read_query(
query_or_default: Annotated[str, Depends(query_or_cookie_extractor)],
):
return {"q_or_cookie": query_or_default}
참고
우리는 path operation 함수에 의존성 하나(
query_or_cookie_extractor)만 선언하고 있어요.그런데 FastAPI는
query_extractor를 먼저 풀어서(해석해서) 그 결과를query_or_cookie_extractor를 호출할 때 넘겨줘야 한다는 걸 알아서 처리해 줍니다.
graph TB
query_extractor(["query_extractor"])
query_or_cookie_extractor(["query_or_cookie_extractor"])
read_query["/items/"]
query_extractor --> query_or_cookie_extractor --> read_query
같은 의존성을 여러 번 사용하기
여러 의존성이 공통의 하위 의존성을 가지는 것처럼, 한 의존성이 같은 path operation에서 여러 번 선언되면, FastAPI는 그 하위 의존성을 요청당 한 번만 호출한다는 걸 알아요.
그리고 반환된 값을 "캐시"(계산하거나 만들어 낸 값을 저장해 두었다가, 다시 계산하지 않고 재사용하기 위한 유틸리티/시스템)에 저장해 두었다가, 같은 요청에서 그 값을 필요로 하는 모든 "dependant"에게 전달해 줘요. 같은 요청에서 의존성을 여러 번 호출하는 대신요.
"캐시된" 값을 쓰지 않고, 같은 요청 안의 모든 단계에서(어쩌면 여러 번) 의존성이 호출되길 원하는 고급 시나리오라면, Depends를 쓸 때 use_cache=False 파라미터를 설정하면 돼요:
async def needy_dependency(fresh_value: Annotated[str, Depends(get_value, use_cache=False)]):
return {"fresh_value": fresh_value}
정리
여기서 쓴 멋진 용어들을 빼면, Dependency Injection(의존성 주입) 시스템은 아주 단순해요.
path operation 함수들과 똑같이 생긴 함수들일 뿐이거든요.
그래도 매우 강력해서, 얼마든지 깊게 중첩된 의존성 "그래프"(트리)를 선언할 수 있게 해 줍니다.
팁
이런 단순한 예시로는 이게 그렇게 유용해 보이지 않을 수 있어요.
하지만 security(보안) 챕터에서 이게 얼마나 유용한지 보게 될 거예요.
그리고 얼마나 많은 코드를 아껴 주는지도요.