
접근 방식: 스레드가 아니라 태스크를 기준으로 보기
asyncio는 한 스레드에서 여러 요청을 번갈아 실행할 수 있어. 그래서 스레드 로컬에 저장한 값은 다른 요청이 덮어쓸 위험이 있어.
ContextVar 값은 현재 실행 컨텍스트에 속해. await로 실행권이 바뀌어도 각 태스크는 자신의 컨텍스트를 다시 사용해.
| 방식 | 구분 기준 | 비동기 요청 격리 |
|---|---|---|
| 함수 인자 | 호출 경로 | 명시적으로 보장 |
threading.local() |
스레드 | 같은 스레드에서는 부족 |
ContextVar |
실행 컨텍스트 | 태스크별 유지 |
코드 예제: 미들웨어에서 설정하고 로그에 주입하기
순수 ASGI 미들웨어로 요청 전체를 감싸면 응답 처리와 FastAPI 백그라운드 작업까지 같은 컨텍스트에서 실행돼. 종료할 때는 토큰으로 이전 값을 복원해야 해.
- 헤더의 요청 ID를 읽거나 새 UUID를 생성해.
ContextVar.set()이 반환한 토큰을 보관해.- 요청이 끝나면
reset()으로 컨텍스트를 복원해.
import asyncio
import logging
from contextvars import ContextVar
from uuid import uuid4
from fastapi import FastAPI
request_id_var: ContextVar[str] = ContextVar(
"request_id",
default="-",
)
class RequestIdMiddleware:
def __init__(self, app):
self.app = app
async def __call__(self, scope, receive, send):
if scope["type"] != "http":
await self.app(scope, receive, send)
return
headers = dict(scope["headers"])
request_id = headers.get(b"x-request-id")
if request_id is None:
request_id = str(uuid4())
else:
request_id = request_id.decode("utf-8")
token = request_id_var.set(request_id)
try:
await self.app(scope, receive, send)
finally:
request_id_var.reset(token)
class RequestIdFilter(logging.Filter):
def filter(self, record: logging.LogRecord) -> bool:
record.request_id = request_id_var.get()
return True
handler = logging.StreamHandler()
handler.addFilter(RequestIdFilter())
handler.setFormatter(
logging.Formatter(
"%(asctime)s %(levelname)s "
"[request_id=%(request_id)s] %(message)s"
)
)
logger = logging.getLogger("app")
logger.setLevel(logging.INFO)
logger.addHandler(handler)
app = FastAPI()
app.add_middleware(RequestIdMiddleware)
@app.get("/orders/{order_id}")
async def read_order(order_id: int):
logger.info("주문 조회 시작")
await asyncio.sleep(0.1)
logger.info("주문 조회 완료")
return {"order_id": order_id}
asyncio.create_task()는 생성 순간의 컨텍스트를 복사해. 태스크를 만든 뒤 부모의 값을 바꿔도 이미 생성된 태스크에는 반영되지 않아.
async def worker():
logger.info("백그라운드 실행")
token = request_id_var.set("request-A")
task = asyncio.create_task(worker())
request_id_var.set("request-B")
await task # worker에는 request-A가 기록돼
request_id_var.reset(token)
실전 체크리스트와 선택 기준
요청 수명 안에서 실행되는 작업은 ContextVar와 잘 맞아. 큐에 넣거나 요청 종료 뒤 독립 실행되는 작업이라면 request_id를 메시지 데이터로 명시하는 편이 안전해.
set()의 토큰을 반드시finally에서 복원해.- 태스크 생성 시점에 어떤 컨텍스트가 복사되는지 확인해.
- 새 스레드나 외부 작업 큐로 값이 자동 전파된다고 가정하지 마.
- 로그 포맷이 요구하는 필드는 필터에서 항상 기본값까지 채워.
- 동시 요청 테스트로 서로 다른 ID가 섞이지 않는지 검증해.
함수의 핵심 계약에 필요한 값은 명시적 인자로 전달하는 게 좋아. 로깅·트레이싱처럼 호출 계층 전체에 걸친 부가 정보는 contextvars가 코드 오염을 줄여줘.
'개발 > Python 스터디' 카테고리의 다른 글
| [Python] 예외를 다시 던졌는데 원래 traceback이 사라지는 이유 (0) | 2026.06.11 |
|---|---|
| [Python] 문자열 메소드로 데이터 클리닝 마스터하기: 간결한 코드로 복잡한 문제 해결 (0) | 2024.12.01 |
| [Python] `assert`로 조건 확인하기: 디버깅과 테스트를 간편하게! (0) | 2024.11.30 |
| [Python] 함수 기본값으로 가변 객체 사용 시 주의점: 방심하면 생기는 복잡한 버그들! (1) | 2024.11.29 |
| [Python] `any`와 `all`로 조건 확인하기: 모든 조건이 만족되나요? (0) | 2024.11.28 |