콘텐츠로 이동

Lifespan 이벤트

애플리케이션이 시작하기 전에 실행되어야 하는 로직(코드)을 정의할 수 있습니다. 이는 이 코드가 한 번만 실행되며, 애플리케이션이 요청을 받기 시작하기 전에 실행된다는 의미입니다.

마찬가지로, 애플리케이션이 종료될 때 실행되어야 하는 로직(코드)을 정의할 수 있습니다. 이 경우, 이 코드는 한 번만 실행되며, 여러 요청을 처리한 후에 실행됩니다.

이 코드는 애플리케이션이 요청을 받기 시작하기 전에 실행되고, 요청 처리를 끝낸 직후에 실행되기 때문에 전체 애플리케이션의 수명(lifespan)을 다룹니다(잠시 후 "lifespan"이라는 단어가 중요해집니다 😉).

이는 전체 앱에서 사용해야 하는 자원을 설정하고, 요청 간에 공유되는 자원을 설정하고, 그리고/또는 이후에 정리하는 데 매우 유용할 수 있습니다. 예를 들어, 데이터베이스 연결 풀 또는 공유 머신러닝 모델을 로드하는 경우입니다.

사용 사례

먼저 사용 사례 예시로 시작한 다음, 이를 어떻게 해결할지 살펴보겠습니다.

요청을 처리하는 데 사용하고 싶은 머신러닝 모델이 있다고 상상해 봅시다. 🤖

동일한 모델이 요청 간에 공유되므로, 요청마다 모델이 하나씩 있거나 사용자마다 하나씩 있는 등의 방식이 아닙니다.

모델을 로드하는 데 상당한 시간이 걸린다고 상상해 봅시다, 왜냐하면 모델이 디스크에서 많은 데이터를 읽어야 하기 때문입니다. 그래서 모든 요청마다 이를 수행하고 싶지는 않습니다.

모듈/파일의 최상위에서 로드할 수도 있지만, 그러면 단순한 자동화된 테스트를 실행하는 경우에도 모델을 로드하게 되고, 테스트가 코드의 독립적인 부분을 실행하기 전에 모델이 로드될 때까지 기다려야 하므로 느려집니다.

이것이 우리가 해결할 문제입니다. 요청을 처리하기 전에 모델을 로드하되, 코드가 로드되는 동안이 아니라 애플리케이션이 요청을 받기 시작하기 직전에만 로드하겠습니다.

Lifespan

FastAPI 앱의 lifespan 매개변수와 "컨텍스트 매니저"를 사용하여 시작종료 로직을 정의할 수 있습니다(컨텍스트 매니저가 무엇인지 잠시 후에 보여드리겠습니다).

예제로 시작한 다음 자세히 살펴보겠습니다.

yield를 사용해 비동기 함수 lifespan()을 다음과 같이 생성합니다:

from contextlib import asynccontextmanager

from fastapi import FastAPI


def fake_answer_to_everything_ml_model(x: float):
    return x * 42


ml_models = {}


@asynccontextmanager
async def lifespan(app: FastAPI):
    # Load the ML model
    ml_models["answer_to_everything"] = fake_answer_to_everything_ml_model
    yield
    # Clean up the ML models and release the resources
    ml_models.clear()


app = FastAPI(lifespan=lifespan)


@app.get("/predict")
async def predict(x: float):
    result = ml_models["answer_to_everything"](x)
    return {"result": result}

여기서는 yield 이전에 (가짜) 모델 함수를 머신러닝 모델이 들어 있는 딕셔너리에 넣어 모델을 로드하는 비용이 큰 시작 작업을 시뮬레이션합니다. 이 코드는 애플리케이션이 요청을 받기 시작하기 전, 시작 동안에 실행됩니다.

그리고 yield 직후에는 모델을 언로드합니다. 이 코드는 애플리케이션이 요청 처리를 마친 후, 종료 직전에 실행됩니다. 예를 들어 메모리나 GPU 같은 자원을 해제할 수 있습니다.

shutdown은 애플리케이션을 중지할 때 발생합니다.

새 버전을 시작해야 할 수도 있고, 그냥 실행하는 게 지겨워졌을 수도 있습니다. 🤷

Lifespan 함수

먼저 주목할 점은 yield를 사용하여 비동기 함수를 정의하고 있다는 것입니다. 이는 yield를 사용하는 의존성과 매우 유사합니다.

from contextlib import asynccontextmanager

from fastapi import FastAPI


def fake_answer_to_everything_ml_model(x: float):
    return x * 42


ml_models = {}


@asynccontextmanager
async def lifespan(app: FastAPI):
    # Load the ML model
    ml_models["answer_to_everything"] = fake_answer_to_everything_ml_model
    yield
    # Clean up the ML models and release the resources
    ml_models.clear()


app = FastAPI(lifespan=lifespan)


@app.get("/predict")
async def predict(x: float):
    result = ml_models["answer_to_everything"](x)
    return {"result": result}

함수의 첫 번째 부분, 즉 yield 이전의 코드는 애플리케이션이 시작되기 전에 실행됩니다.

그리고 yield 이후의 부분은 애플리케이션이 종료된 후에 실행됩니다.

비동기 컨텍스트 매니저

확인해 보면, 함수는 @asynccontextmanager로 데코레이션되어 있습니다.

이는 함수를 "비동기 컨텍스트 매니저"라고 불리는 것으로 변환합니다.

from contextlib import asynccontextmanager

from fastapi import FastAPI


def fake_answer_to_everything_ml_model(x: float):
    return x * 42


ml_models = {}


@asynccontextmanager
async def lifespan(app: FastAPI):
    # Load the ML model
    ml_models["answer_to_everything"] = fake_answer_to_everything_ml_model
    yield
    # Clean up the ML models and release the resources
    ml_models.clear()


app = FastAPI(lifespan=lifespan)


@app.get("/predict")
async def predict(x: float):
    result = ml_models["answer_to_everything"](x)
    return {"result": result}

파이썬에서 컨텍스트 매니저with 문에서 사용할 수 있는 것입니다. 예를 들어, open()은 컨텍스트 매니저로 사용할 수 있습니다:

with open("file.txt") as file:
    file.read()

최근 버전의 파이썬에는 비동기 컨텍스트 매니저도 있습니다. 이를 async with와 함께 사용합니다:

async with lifespan(app):
    await do_stuff()

위와 같은 컨텍스트 매니저 또는 비동기 컨텍스트 매니저를 만들면, with 블록에 들어가기 전에 yield 이전의 코드를 실행하고, with 블록을 벗어난 후에는 yield 이후의 코드를 실행합니다.

위의 코드 예제에서는 직접 사용하지 않고, FastAPI에 전달하여 FastAPI가 이를 사용하도록 합니다.

FastAPI 앱의 lifespan 매개변수는 비동기 컨텍스트 매니저를 받으므로, 새 lifespan 비동기 컨텍스트 매니저를 전달할 수 있습니다.

from contextlib import asynccontextmanager

from fastapi import FastAPI


def fake_answer_to_everything_ml_model(x: float):
    return x * 42


ml_models = {}


@asynccontextmanager
async def lifespan(app: FastAPI):
    # Load the ML model
    ml_models["answer_to_everything"] = fake_answer_to_everything_ml_model
    yield
    # Clean up the ML models and release the resources
    ml_models.clear()


app = FastAPI(lifespan=lifespan)


@app.get("/predict")
async def predict(x: float):
    result = ml_models["answer_to_everything"](x)
    return {"result": result}

대체 이벤트(사용 중단)

경고

시작종료를 처리하는 권장 방법은 위에서 설명한 대로 FastAPI 앱의 lifespan 매개변수를 사용하는 것입니다. lifespan 매개변수를 제공하면 startupshutdown 이벤트 핸들러는 더 이상 호출되지 않습니다. lifespan만 쓰거나 이벤트만 쓰거나 둘 중 하나이지, 둘 다는 아닙니다.

이 부분은 아마 건너뛰셔도 됩니다.

시작종료 동안 실행될 이 로직을 정의하는 대체 방법이 있습니다.

애플리케이션이 시작되기 전에 또는 애플리케이션이 종료될 때 실행되어야 하는 이벤트 핸들러(함수)를 정의할 수 있습니다.

이 함수들은 async def 또는 일반 def로 선언할 수 있습니다.

startup 이벤트

애플리케이션이 시작되기 전에 실행되어야 하는 함수를 추가하려면, "startup" 이벤트로 선언합니다:

from fastapi import FastAPI

app = FastAPI()

items = {}


@app.on_event("startup")
async def startup_event():
    items["foo"] = {"name": "Fighters"}
    items["bar"] = {"name": "Tenders"}


@app.get("/items/{item_id}")
async def read_items(item_id: str):
    return items[item_id]

이 경우, startup 이벤트 핸들러 함수는 "database"(그냥 dict) 항목을 일부 값으로 초기화합니다.

여러 개의 이벤트 핸들러 함수를 추가할 수 있습니다.

그리고 모든 startup 이벤트 핸들러가 완료될 때까지 애플리케이션은 요청을 받기 시작하지 않습니다.

shutdown 이벤트

애플리케이션이 종료될 때 실행되어야 하는 함수를 추가하려면, "shutdown" 이벤트로 선언합니다:

from fastapi import FastAPI

app = FastAPI()


@app.on_event("shutdown")
def shutdown_event():
    with open("log.txt", mode="a") as log:
        log.write("Application shutdown")


@app.get("/items/")
async def read_items():
    return [{"name": "Foo"}]

여기서 shutdown 이벤트 핸들러 함수는 텍스트 한 줄 "Application shutdown"log.txt 파일에 기록합니다.

정보

open() 함수에서 mode="a"는 "append"(추가)를 의미하므로, 기존 내용을 덮어쓰지 않고 파일에 있던 내용 뒤에 줄이 추가됩니다.

이 경우에는 파일과 상호작용하는 표준 파이썬 open() 함수를 사용하고 있습니다.

따라서 I/O(input/output)가 포함되어 있어 디스크에 기록되는 것을 "기다리는" 과정이 필요합니다.

하지만 open()asyncawait를 사용하지 않습니다.

그래서 이벤트 핸들러 함수는 async def 대신 표준 def로 선언합니다.

startupshutdown을 함께

시작종료 로직은 연결되어 있을 가능성이 높습니다. 무언가를 시작했다가 끝내거나, 자원을 획득했다가 해제하는 등의 작업이 필요할 수 있습니다.

로직이나 변수를 함께 공유하지 않는 분리된 함수에서 이를 처리하면, 전역 변수에 값을 저장하거나 비슷한 트릭이 필요해져 더 어렵습니다.

그 때문에, 이제는 위에서 설명한 대로 lifespan을 사용하는 것이 권장됩니다.

기술적 세부사항

호기심 많은 분들을 위한 기술적인 세부사항입니다. 🤓

내부적으로 ASGI 기술 사양에서는 이것이 Lifespan Protocol의 일부이며, startupshutdown이라는 이벤트를 정의합니다.

정보

Starlette lifespan 핸들러에 대해서는 Starlette의 Lifespan 문서에서 더 읽어볼 수 있습니다.

또한 코드의 다른 영역에서 사용할 수 있는 lifespan 상태를 처리하는 방법도 포함되어 있습니다.

서브 애플리케이션

🚨 이 lifespan 이벤트(startup 및 shutdown)는 메인 애플리케이션에 대해서만 실행되며, 서브 애플리케이션 - Mounts에는 실행되지 않음을 유의하세요.