하위 의존성

하위 의존성 (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) 쿼리 파라미터 qstr로 선언하고, 그냥 그 값을 반환해요.

아주 단순해서(그렇게 유용하지도 않고요) 오히려 하위 의존성이 어떻게 동작하는지에 집중하기 좋답니다.

두 번째 의존성 "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)만 선언하고 있어요.

그런데 FastAPIquery_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(보안) 챕터에서 이게 얼마나 유용한지 보게 될 거예요.

그리고 얼마나 많은 코드를 아껴 주는지도요.

더 알아보기 (Learn more)