FastAPI의 핵심 아키텍처인 **자원 관리(try-finally ➔ Context Manager ➔ Depends 의존성 주입)**부터 클라이언트 요청-응답 4단계 라이프사이클, 응답 속도를 극대화하는 Background Tasks, 비동기(asyncio)의 본질과 컴퓨터 하드웨어(RAM vs Disk I/O) 속도 차이, 코루틴(Coroutine)과 '세 쌍둥이 밥 먹이기' 이벤트 루프(Event Loop)의 원리, await를 쓰는 2대 필수 조건과 체이닝 실습, 그리고 FastAPI가 동기 함수(def)를 이벤트 루프 블로킹 없이 처리하는 비밀인 스레드 풀(Thread Pool)과 Lifespan 제어까지 총정리한 실무 가이드
- 자원 관리의 진화: try-finally에서 Context Manager까지
- FastAPI 라우팅과 클라이언트 요청-응답 4단계 라이프사이클
- 의존성 주입 (Dependency Injection, DI): with 구문마저 지우는 비결
- Background Tasks (백그라운드 태스크)
- 비동기 프로그래밍 (asyncio)의 본질과 컴퓨터 구조
- 코루틴(Coroutine)과 협력적 멀티태스킹
- await 키워드와 양보(Yield Control)의 미학
- 이벤트 루프(Event Loop)와 블로킹(Blocking) 금지의 법칙
- FastAPI 내부 동작의 비밀: 동기 함수(def)와 스레드 풀(Thread Pool)
- 💡 실전 핵심 요약 치트시트
파이썬의 예외 처리는 프로그램이 예기치 못한 에러로 강제 종료(크래시)되는 것을 막고, 사용한 시스템 자원을 안전하게 반납하기 위한 핵심 문법입니다.

- try: 에러가 발생할 가능성이 있는 코드를 실행. 에러가 나면 즉시 실행을 멈추고 except로 점프.
- except: 발생한 에러를 잡아내어 대처(로그 기록, 사용자 친화적 메시지 출력 등).
- finally: 예외 발생 여부와 상관없이 무조건 마지막에 실행되는 구역. 파일 닫기(f.close()), 네트워크 연결 종료, 데이터베이스 세션 종료(session.close()) 등에 필수적입니다.
- except 생략 가능: 에러 처리는 상위로 넘기되 뒷정리만 확실히 하고 싶다면 try - finally 구조로만 작성할 수 있습니다.
def get_items_handler():
session = SessionFactory()
try:
stmt = select(Item)
items = session.scalars(stmt).all()
return items # ⚠️ 여기서 함수가 즉시 종료됨!
# session.close() 를 여기에 적으면 절대 도달할 수 없음(Dead Code)!
finally:
session.close() # ✅ return이 실행되기 직전에 무조건 실행되어 세션을 안전하게 닫음!
- 데이터베이스 세션을 열었으면 데이터를 다 쓰고 난 뒤 반드시 session.close()로 커넥션을 반납해야 커넥션 풀 누수(Memory Leak)가 발생하지 않습니다.
- 하지만 함수 중간에 return items를 만나면 함수가 그 즉시 끝나버립니다.
- finally 블록을 사용하면 return 문을 만나더라도 파이썬 엔진이 그 직전에 finally 내부 코드를 먼저 실행하고 함수를 빠져나갑니다.
매번 모든 API 엔드포인트마다 session = SessionFactory(), try:, finally: session.close()를 반복해서 적는 것은 코드가 지저분해지고 번거롭습니다.
파이썬의 **컨텍스트 매니저 (Context Manager, with 문)**를 사용하면 이 모든 과정을 한 줄로 축약할 수 있습니다.
def get_items_handler():
# with 문 진입 시 자동으로 session이 열리고, 블록을 벗어날 때 자동으로 session.close() 호출!
with SessionFactory() as session:
stmt = select(Item)
items = session.scalars(stmt).all()
return items
- with SessionFactory() as session:에 진입하는 순간 세션 인스턴스가 생성됩니다 (__enter__).
- 블록 내부 코드가 정상적으로 끝나든, 중간에 return을 만나든, 에러가 발생해 튕겨 나가든 상관없이, 들여쓰기가 끝나는 순간 컨텍스트 매니저가 내부적으로 session.close()를 100% 보장하며 안전하게 호출합니다 (__exit__).
- **라우팅(Routing)**이란:
클라이언트(웹 브라우저, 모바일 앱)가 보낸 요청의 **HTTP 메서드(GET, POST, PUT, DELETE)**와 **URL 경로(/items, /users)**를 조합하여, 서버 내부의 어떤 담당 함수(핸들러)를 실행시킬지 길을 안내하고 연결해 주는 과정입니다. - 도로 표지판 비유:
- GET /items 표지판 ➔ list_items() 함수로 안내 (데이터 조회)
- POST /items 표지판 ➔ create_item() 함수로 안내 (데이터 생성)
- 주소가 같아도 메서드가 다르면 완전히 다른 함수가 실행됩니다!
클라이언트가 버튼을 클릭한 순간부터 화면에 데이터가 렌더링될 때까지 FastAPI 내부에서 일어나는 일입니다:
Unable to render rich display
Parse error on line 11:
...m); session.commit() Handler-->>Form
-----------------------^
Expecting 'SOLID_OPEN_ARROW', 'DOTTED_OPEN_ARROW', 'SOLID_ARROW', 'SOLID_ARROW_TOP', 'SOLID_ARROW_BOTTOM', 'STICK_ARROW_TOP', 'STICK_ARROW_BOTTOM', 'SOLID_ARROW_TOP_DOTTED', 'SOLID_ARROW_BOTTOM_DOTTED', 'STICK_ARROW_TOP_DOTTED', 'STICK_ARROW_BOTTOM_DOTTED', 'SOLID_ARROW_TOP_REVERSE', 'SOLID_ARROW_BOTTOM_REVERSE', 'STICK_ARROW_TOP_REVERSE', 'STICK_ARROW_BOTTOM_REVERSE', 'SOLID_ARROW_TOP_REVERSE_DOTTED', 'SOLID_ARROW_BOTTOM_REVERSE_DOTTED', 'STICK_ARROW_TOP_REVERSE_DOTTED', 'STICK_ARROW_BOTTOM_REVERSE_DOTTED', 'BIDIRECTIONAL_SOLID_ARROW', 'DOTTED_ARROW', 'BIDIRECTIONAL_DOTTED_ARROW', 'SOLID_CROSS', 'DOTTED_CROSS', 'SOLID_POINT', 'DOTTED_POINT', got 'NEWLINE'
For more information, see https://docs.github.com/get-started/writing-on-github/working-with-advanced-formatting/creating-diagrams#creating-mermaid-diagrams
sequenceDiagram
autonumber
actor Client as 💻 클라이언트 (Chrome)
participant FastAPI as ⚡ FastAPI 라우팅 & 사전 검증
participant Handler as ⚙ 비즈니스 핸들러 & DB
participant Formatter as 📦 응답 직렬화기
Client->>FastAPI: 1단계: HTTP 요청 (예: POST /items {"name": "apple", "price": 1000})
Note over FastAPI: 2단계: 사전 검증<br/>a) 라우팅: URL+Method로 함수 탐색<br/>b) 데이터 유효성 검사: Pydantic 타입/길이 검증<br/>c) 검증 통과 시 핸들러 호출
FastAPI->>Handler: 3단계: 함수 코드 실행 & DB 세션 관리
Note over Handler: with SessionFactory() as session:<br/>session.add(new_item); session.commit()
Handler-->>Formatter: 파이썬 Item ORM 객체 반환
Note over Formatter: 4단계: 최종 응답 조립<br/>a) Python ➔ JSON 직렬화<br/>b) 상태 코드(201 Created) 부여
Formatter-->>Client: HTTP 201 응답 전송 (JSON 본문)
- 1단계: 클라이언트의 HTTP 요청 전송: 웹/앱에서 주소를 호출 (localhost:8000은 외부망으로 나가지 않고 내 컴퓨터 내부에서 루프백 통신).
- 2단계: FastAPI 사전 검증 및 라우팅:
- URL과 Method로 대상 함수 매핑
- Path, Query, Request Body 데이터 형식 사전 검사 (실패 시 즉시 422 반환)
- 3단계: 함수 실행 및 DB 세션 관리: 비즈니스 로직 수행 및 안전한 세션 자원 해제.
- 4단계: 최종 응답 처리: 파이썬 데이터를 표준 JSON 텍스트로 자동 직렬화하고 HTTP 상태 코드와 함께 클라이언트로 회신.
- 의존성 주입(Dependency Injection):
함수 내부에서 필요한 공통 자원(DB 세션, 인증 유저 정보, 외부 API 클라이언트 등)을 함수가 직접 만들지 않고, FastAPI 프레임워크가 외부에서 대신 만들어 파라미터로 주입(Inject)해 주는 아키텍처입니다. - 렌탈(대여) 개념:
자동차를 매번 내가 직접 사서 타고 폐차하는 것이 아니라, 렌터카 업체(FastAPI)에서 차를 빌려 타고 반납하면 뒷정리는 업체가 알아서 해주는 것과 같습니다.
# connection.py
from sqlalchemy.orm import Session
from connection import SessionFactory
def get_session():
# 1. 요청이 들어오면 DB 세션을 열고
session = SessionFactory()
try:
# 2. yield로 핸들러 함수에게 세션을 대여(전달)해 줌!
# (함수가 종료될 때까지 get_session은 여기서 일시정지 상태 유지)
yield session
finally:
# 3. 핸들러 함수가 return을 마치는 순간 일시정지가 풀리고 세션을 안전하게 닫음!
session.close()
- yield의 작동 원리:
- return은 값을 돌려주고 함수를 완전히 종료시킵니다.
- 반면 yield는 **값을 반환한 뒤 그 자리에서 일시정지(Pause)**합니다.
- FastAPI 핸들러 함수가 DB 작업을 모두 마치고 나면, 멈춰있던 yield 다음 줄(finally: session.close())이 실행되면서 자원을 완벽하게 회수합니다.
이제 엔드포인트 함수에서는 with 문조차 쓸 필요가 없습니다!
from fastapi import FastAPI, Depends
from sqlalchemy.orm import Session
from sqlalchemy import select
from connection import get_session
app = FastAPI()
@app.get("/items", response_model=list[ItemResponse])
def list_items(session: Session = Depends(get_session)):
# 🌟 with SessionFactory() as session: 을 작성할 필요가 전혀 없음!
stmt = select(Item)
items = session.scalars(stmt).all()
return items
Tip
💡 의존성 주입(DI)의 압도적인 장점:
- 비즈니스 로직에만 집중: 세션을 열고 닫는 반복적인 보일러플레이트 코드가 완전히 사라집니다.
- 테스트 용이성 (Mocking): 테스트 코드 작성 시 실제 DB 세션 대신 가짜(Mock) 세션으로 손쉽게 교체할 수 있습니다.
- 클라이언트에게 HTTP 응답 메시지를 먼저 초고속으로 반환한 뒤, 시간이 걸리는 부가 작업을 백그라운드에서 실행하는 FastAPI 내장 기능입니다.
- 사용자는 화면에서 로딩 스피너를 오래 보지 않고 즉시 "완료되었습니다" 응답을 받으므로 서비스의 체감 속도가 비약적으로 향상됩니다.
import time
from fastapi import FastAPI, BackgroundTasks
app = FastAPI()
# 5초가 걸리는 무거운 이메일 발송 함수
def send_email(email: str):
time.sleep(5) # 실제 이메일 SMTP 서버 통신 시뮬레이션
print(f"📧 [발송 완료] {email} 님에게 환영 메일을 성공적으로 보냈습니다!")
@app.post("/sign-up")
def sign_up(email: str, background_tasks: BackgroundTasks):
# 1. 백그라운드 태스크 큐에 작업 등록 (함수명, 인자)
background_tasks.add_task(send_email, email)
# 2. 이메일이 다 보내질 때까지 기다리지 않고 0.01초 만에 즉시 클라이언트에 응답!
return {"message": "User SignUp Completed"}
- 적합한 작업: 이메일 발송, 푸시 알림 전송, 간단한 통계 로깅, 캐시 갱신 등 1~5초 내외의 가벼운 후처리.
- ❌ 부적합한 작업 (주의!):
- 수 분 이상 걸리는 대용량 동영상 인코딩, 대규모 데이터셋 머신러닝 학습 등.
- FastAPI의 Background Tasks는 동일한 서버 프로세스 안에서 실행되므로, 너무 무거운 CPU 작업을 넣으면 메인 웹 서버의 전체 성능이 떨어집니다.
- 초대형 작업은 Celery + Redis / RabbitMQ 같은 분산 작업 큐 시스템을 사용하는 것이 정석입니다.
- 동기(Synchronous) 방식:
호출자(Caller)가 피호출자(Callee)의 작업이 완전히 끝날 때까지 아무것도 하지 않고 가만히 멈춰서 기다리는 방식.
import time
def hello():
time.sleep(2) # 2초 동안 CPU가 멍하니 대기 (Blocking)
print("hello")
hello()
print("end")
# 총 2초 동안 프로그램 전체가 멈춤!
왜 웹 서버에서는 동기 방식이 치명적일까요? 컴퓨터 하드웨어의 영역 차이를 이해해야 합니다.
Unable to render rich display
Parse error on line 12:
... <-->|물리적 I/O 대기 발생 (Blocking!)| SlowZon
-----------------------^
Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'PS'
For more information, see https://docs.github.com/get-started/writing-on-github/working-with-advanced-formatting/creating-diagrams#creating-mermaid-diagrams
graph LR
subgraph FastZone["⚡ 빛의 속도 (나노초 단위)"]
CPU["CPU (연산 장치)<br/>1 + 1 = 2 계산"]
RAM["RAM (주기억장치)<br/>FastAPI 파이썬 메모리"]
end
subgraph SlowZone["🐢 달팽이 속도 (밀리초~초 단위)"]
DISK["SSD / HDD (보조기억장치)<br/>SQLite test.db 파일"]
NET["외부 네트워크 / DB 서버<br/>API 통신 패킷"]
end
FastZone <-->|물리적 I/O 대기 발생 (Blocking!)| SlowZone
- CPU와 RAM의 연산: 빛처럼 빠릅니다 (나노초 단위로 수억 번 계산).
- 디스크(SSD/HDD)와 네트워크 I/O: 물리적으로 모터가 돌거나 광케이블을 타야 하므로 상대적으로 수백만 배 느립니다.
- 결론:
FastAPI가 DB 조회를 요청할 때, CPU는 0.0001초 만에 요청 명령을 내렸지만 디스크에서 데이터가 RAM으로 올라올 때까지 **수 밀리초 동안 손을 놓고 멍하니 대기(Blocking)**하게 됩니다.
이 아까운 대기 시간 동안 다른 사용자의 요청을 처리하자는 것이 **비동기 프로그래밍(asyncio)**의 핵심입니다!
상황 🛑 동기 (Synchronous) ⚡ 비동기 (Asynchronous)
| 라면 끓이기 | 냄비에 물을 올리고, 물이 끓을 때까지 3분 동안 가스레인지 앞에 가만히 차렷 자세로 서 있기 | 물을 올려두고, 물이 끓는 동안(I/O 대기) 설거지를 하거나 테이블을 닦고 돌아오기 |
| 식당 운영 | 1번 손님의 스테이크 요리가 다 완성되어 서빙될 때까지 2번 손님의 주문 접수를 일절 거부하고 문을 닫아두기 | 1번 손님 주문을 주방에 넘겨두고(조리 대기), 카운터에서 2번, 3번 손님의 주문을 쉬지 않고 연속해서 접수하기 |
- 파이썬에서 비동기 I/O 프로그래밍을 공식 지원하는 내장 라이브러리입니다 (import asyncio).
- 외부 라이브러리(numpy, pytorch 등)와 달리 별도 pip install 없이 즉시 사용 가능합니다.
- CPU를 쓰지 않고 쉬는 I/O 대기 시간(네트워크 요청, DB 쿼리, sleep)에 다른 작업을 교대로 처리하게 해줍니다.
- 코루틴(Coroutine):
co-(함께, 협력하다) + routine(함수, 절차)의 합성어로, 실행 도중에 멈췄다가(양보) 나중에 그 자리에서 다시 이어서 실행할 수 있는 협력적인 함수를 말합니다. - 특정 함수가 CPU를 혼자 독점하지 않고, "나 지금 DB 기다려야 하니까 다른 함수 먼저 일해!" 하고 서로 양보하며 번갈아 실행됩니다.
# 1. async def로 정의된 함수 -> 코루틴 함수 (Coroutine Function)
async def hello():
print("hello")
# 2. 코루틴 함수 호출
coro = hello()
print(coro)
# 출력: <coroutine object hello at 0x1056f3580>
Q. 왜 일반 함수처럼 hello()를 부르면 바로 실행되지 않고 객체만 튀어나올까요?
A. 코루틴은 단독으로 혼자 실행되려고 태어난 것이 아니기 때문입니다!
코루틴의 전제는 **"동시에 진행해야 할 여러 작업들이 있을 때, 대기 시간에 서로 양보하며 협력하는 것"**입니다.
따라서 실행하기 전에 먼저 **작업 지시서(코루틴 객체)**를 만들어 두고, 이벤트 루프에 한꺼번에 올려서 실행해야 합니다.
import asyncio
async def task_a():
print("A 시작")
await asyncio.sleep(2) # 2초 동안 양보하며 대기
print("A 끝")
async def task_b():
print("B 시작")
await asyncio.sleep(2) # 2초 동안 양보하며 대기
print("B 끝")
async def main():
# 1. 코루틴 객체들을 생성하여 asyncio.gather로 한 바구니에 묶음
coro_a = task_a()
coro_b = task_b()
await asyncio.gather(coro_a, coro_b)
# 2. 메인 진입점에서 이벤트 루프 가동
asyncio.run(main())

- 동기 방식 (time.sleep): A가 2초 멈추고 끝난 뒤 B가 2초 멈춤 ➔ 총 4초 소요!
- 비동기 방식 (asyncio.sleep): A가 2초 대기에 들어가며 양보하자마자 B가 즉시 시작되어 함께 2초를 대기 ➔ 총 2초 만에 두 작업 동시 완료!
- 작업이 5개, 10개로 늘어나도 I/O 대기 시간은 여전히 2초로 수렴합니다.
- 코루틴은 I/O를 기다릴 때 양보하지만, 컴퓨터는 언제 I/O 대기가 발생할지 스스로 예측할 수 없습니다.
- 따라서 개발자가 코드에 **await**라는 키워드를 붙여서 "여기가 바로 오래 걸리는 지점이니까, 결과가 올 때까지 다른 코루틴에게 CPU 제어권을 넘겨라!" 하고 명시해 주어야 합니다.
- 반드시 async def로 선언된 비동기 함수(코루틴) 내부에서만 사용 가능!
일반 동기 함수(def)나 파이썬 파일 최상단에서 단독으로 쓰면 문법 에러(SyntaxError: 'await' outside function)가 발생합니다. - 기다릴 수 있는 대상(Awaitable 객체) 앞에만 사용 가능!
아무 함수에나 붙일 수 없으며, 반드시 비동기 작업을 지원하는 대상이어야 합니다.
# ❌ 일반 동기 함수에 await를 붙인 잘못된 코드 (에러 발생!)
async def wrong():
await time.sleep(3) # TypeError: object NoneType can't be used in 'await' expression
# ✅ 비동기 전용 함수에 await를 붙인 올바른 코드
async def correct():
await asyncio.sleep(3) # 정상 동작
- async def로 정의된 함수인가? ➔ 호출 시 코루틴 객체를 반환하므로 100% await 가능.
- 비동기 라이브러리(aiohttp, asyncpg, aiofiles, asyncio 등) 출신인가? ➔ 대부분 await 가능.
- I/O 대기가 발생하는 작업인가? ➔ 비동기 버전 함수라면 await를 붙여야 함.
슬라이드 31~34페이지의 실전 퍼즐 문제입니다.
- 목표 출력 순서:
- C 시작
- A 시작
- A 끝
- B 시작
- C 끝
- B 끝
각 코루틴의 await asyncio.sleep() 시간을 세밀하게 조율하여 타임라인을 맞춥니다.
import asyncio
async def a():
await asyncio.sleep(0.1) # 0.1초 후 A 시작
print("A 시작")
await asyncio.sleep(0.0) # 대기 없이 즉시 A 끝
print("A 끝")
async def b():
await asyncio.sleep(0.2) # 0.2초 후 B 시작
print("B 시작")
await asyncio.sleep(0.2) # 추가 0.2초(누적 0.4초) 후 B 끝
print("B 끝")
async def c():
await asyncio.sleep(0.0) # 0.0초! 가장 먼저 C 시작
print("C 시작")
await asyncio.sleep(0.3) # 추가 0.3초 후 C 끝
print("C 끝")
async def main():
await asyncio.gather(a(), b(), c())
asyncio.run(main())
**이벤트 루프(Event Loop)**는 수많은 코루틴들의 양보와 대기, 실행 재개를 쉴 새 없이 조율하는 중간 총괄 관리자입니다.
Unable to render rich display
Parse error on line 8:
...D1 -->|2. 오물오물 씹는 중 (await)| PARENT
-----------------------^
Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'PS'
For more information, see https://docs.github.com/get-started/writing-on-github/working-with-advanced-formatting/creating-diagrams#creating-mermaid-diagrams
graph TD
PARENT["👨👧👦 이벤트 루프 (부모)"]
KID1["👶 코루틴 A (첫째)"]
KID2["👶 코루틴 B (둘째)"]
KID3["👶 코루틴 C (셋째)"]
PARENT -->|1. 한 숟갈 먹임| KID1
KID1 -->|2. 오물오물 씹는 중 (await)| PARENT
PARENT -->|3. 씹는 동안 둘째 먹임| KID2
KID2 -->|4. 오물오물 씹는 중 (await)| PARENT
PARENT -->|5. 셋째 먹임| KID3
PARENT -->|6. 첫째가 다 씹었으면 다음 숟갈!| KID1
- 비유 분석:
- 부모 = 이벤트 루프: 단 한 명의 부모(싱글 스레드)가 숟가락을 들고 있습니다.
- 아이들 = 코루틴: 아이가 밥을 씹는 시간은 **I/O 대기 시간(await)**입니다.
- 부모는 한 아이가 씹는 동안 멍하니 기다리지 않고 바로 옆 아이에게 숟가락을 가져갑니다.
- 밖에서 보면 세 아이가 동시에 밥을 먹는 것처럼(동시성 / Concurrency) 보입니다!
⚠️ 비동기 세상의 대역죄: Blocking the Event Loop!
만약 코루틴 내부에서 await 없이 time.sleep(10)이나 거대한 while True 연산을 돌려버리면, 이벤트 루프(부모)의 손발이 통째로 묶여버립니다.
그 순간 다른 모든 코루틴의 실행이 정지되어, 접속해 있는 모든 클라이언트의 웹 페이지가 동시에 멈추게 됩니다.
미스터리:
"FastAPI는 비동기(Async) 기반 프레임워크라고 배웠는데, 왜 이전 실습에서는 @app.get("/items") 밑에 async def가 아니라 그냥 def list_items():를 썼을까요? 동기 함수를 쓰면 이벤트 루프가 블로킹되어 서버가 멈춰야 정상 아닌가요?"
비밀은 FastAPI 내부 소스코드(fastapi/routing.py의 run_endpoint_function)에 숨어 있습니다.

- FastAPI의 천재적인 배려:
- 개발자가 def로 동기 함수를 작성하면, FastAPI는 "아! 이 함수는 동기 함수니까 이벤트 루프에서 직접 돌리면 서버가 멈추겠구나!" 하고 판단합니다.
- 그래서 이벤트 루프를 건드리지 않고, 외부의 별도 일꾼 모음인 스레드 풀(Thread Pool)의 작업자 스레드에게 작업을 대신 던져서 실행시킵니다.
- 덕분에 개발자가 실수로 동기 코드를 작성해도 이벤트 루프가 얼어붙지 않는 것입니다!
- 스레드 풀의 한계:
동기 엔드포인트는 **스레드 풀의 개수(기본값: 40개)**가 곧 최대 동시 처리량입니다.
동시 요청이 40개를 넘어가면 41번째 요청부터는 스레드가 날 때까지 대기 큐에서 지연(Lag)이 발생합니다. - 해결책:
최신 FastAPI 표준인 Lifespan 컨텍스트 매니저를 활용해 서버 기동 시 스레드 풀의 크기(total_tokens)를 상향 조정합니다.
from contextlib import asynccontextmanager
import anyio
from fastapi import FastAPI
@asynccontextmanager
async def lifespan(app: FastAPI):
# 1. [서버 시작 직후 실행] AI 모델 로드, 스레드 풀 조정 등
limiter = anyio.to_thread.current_default_thread_limiter()
limiter.total_tokens = 200 # 기본 40개 ➔ 200개로 대폭 상향!
print("🚀 [서버 시작] 스레드 풀 200개 확장 완료")
yield # 서버가 돌아가는 동안 대기
# 2. [서버 종료 직전 실행] DB 커넥션 풀 종료 등 마무리 코드
print("🛑 [서버 종료] 자원 정리 완료")
app = FastAPI(
title="e-Commerce 상품 관리 서버",
lifespan=lifespan
)
┌────────────────────────────────────────────────────────────────────────┐
│ FastAPI 자원 관리 & 비동기(asyncio) & 스레드 풀 치트시트 │
├──────────────────────────────┬─────────────────────────────────────────┤
│ 문법 / 개념 │ 핵심 요약 및 사용 공식 │
├──────────────────────────────┼─────────────────────────────────────────┤
│ try - finally │ return 직전에 session.close() 보장 │
├──────────────────────────────┼─────────────────────────────────────────┤
│ with SessionFactory() as s: │ 들여쓰기 빠져나갈 때 자동 close() │
├──────────────────────────────┼─────────────────────────────────────────┤
│ yield session │ get_session() 제너레이터로 세션 렌탈 │
├──────────────────────────────┼─────────────────────────────────────────┤
│ Depends(get_session) │ FastAPI 의존성 주입 (with 문도 생략) │
├──────────────────────────────┼─────────────────────────────────────────┤
│ BackgroundTasks.add_task() │ 응답 반환 후 백그라운드에서 후처리 실행 │
├──────────────────────────────┼─────────────────────────────────────────┤
│ async def │ 코루틴 함수 정의 (호출 시 코루틴 객체) │
├──────────────────────────────┼─────────────────────────────────────────┤
│ await │ I/O 대기 시 CPU 제어권 양보 키워드 │
├──────────────────────────────┼─────────────────────────────────────────┤
│ await 사용 2대 조건 │ 1) async def 내부 2) Awaitable 객체 │
├──────────────────────────────┼─────────────────────────────────────────┤
│ asyncio.gather(c1, c2) │ 여러 비동기 작업을 묶어 동시 실행 │
├──────────────────────────────┼─────────────────────────────────────────┤
│ 이벤트 루프 (Event Loop) │ 코루틴들의 양보/대기/재개를 총괄 관리 │
├──────────────────────────────┼─────────────────────────────────────────┤
│ 블로킹 금지 │ 이벤트 루프 멈추면 모든 비동기 작업 올스톱│
├──────────────────────────────┼─────────────────────────────────────────┤
│ def (동기 엔드포인트) │ FastAPI가 스레드 풀(기본 40개)에서 실행 │
├──────────────────────────────┼─────────────────────────────────────────┤
│ lifespan (스레드 풀 확장) │ anyio limiter.total_tokens = 200 │
└──────────────────────────────┴─────────────────────────────────────────┘