FastAPI 입문

Python으로 REST API 서버를 만드는 프레임워크

FastAPI는 Python의 타입 힌트와 Pydantic을 활용하여 고성능 REST API를 빠르게 구축하는 웹 프레임워크이다. 설치부터 프로젝트 구조까지, FastAPI로 API 서버를 만드는 단계별 개발 과정을 정리한다.

Engineering
저자

Kwangmin Kim

공개

2026년 05월 05일

1 왜 FastAPI인가

Python으로 API 서버를 만드는 프레임워크는 Flask, Django, FastAPI 등 여러 가지가 있다. AI Agent를 서빙하는 용도로 FastAPI가 적합한 이유는 다음과 같다.

기준 Flask Django FastAPI
비동기(async) 지원 제한적 (WSGI) 3.1부터 부분 지원 네이티브 (ASGI)
타입 검증 수동 수동 (DRF로 보완) Pydantic 자동
API 문서 자동 생성 없음 없음 (DRF Swagger 추가) 내장 (OpenAPI)
학습 곡선 낮음 높음 낮음
성능 보통 보통 높음 (Node.js 수준)
AI/ML 생태계 친화 좋음 보통 매우 좋음

LLM 호출은 네트워크 I/O가 대부분이므로 비동기 지원이 중요하다. FastAPI는 async/await를 네이티브로 지원하여 LLM 응답을 기다리는 동안 다른 요청을 처리할 수 있다.

2 FastAPI 개발의 전체 흐름

FastAPI로 API 서버를 만드는 과정은 다음과 같은 단계를 거친다. 이 포스트는 이 순서를 따라간다.

1. 설치 & 첫 실행      — FastAPI 앱을 만들고 uvicorn으로 띄운다
         ↓
2. Pydantic 스키마      — 요청/응답의 데이터 형식을 정의한다
         ↓
3. 라우팅              — URL과 함수를 연결하고, 매개변수와 요청 본문을 받는다
         ↓
4. 라우터 분리          — 엔드포인트가 늘어나면 파일을 모듈로 나눈다
         ↓
5. 의존성 주입          — 인증, 설정 로드 등 공통 로직을 재사용한다
         ↓
6. 비동기 처리          — I/O 작업을 효율적으로 처리한다
         ↓
7. 에러 처리            — 실패를 적절한 HTTP 상태 코드로 전달한다
         ↓
8. 미들웨어             — CORS 등 모든 요청에 공통 적용할 처리를 추가한다
         ↓
9. Lifespan 이벤트      — 서버 시작/종료 시 모델 로딩, 리소스 정리를 수행한다
         ↓
10. 프로젝트 구조        — 파일을 어디에 어떻게 배치하는지 정리한다

각 단계는 이전 단계의 개념 위에 쌓인다.
- 2단계에서 정의한 Pydantic 모델이
- 3단계의 요청 본문과 응답에 사용되고,
- 4단계에서 라우터를 분리할 때 2~3단계의 코드를 모듈로 옮기는 식이다.

3 1단계: 설치와 첫 실행

3.1 설치

pip install fastapi uvicorn
  • fastapi: 웹 프레임워크 본체
  • uvicorn: FastAPI를 구동하는 ASGI 서버. FastAPI 자체는 HTTP 요청을 수신하는 기능이 없으므로 uvicorn이 네트워크 수신 → FastAPI 전달 → 응답 반환의 중간 역할을 한다

ASGI 서버:

  • ASGI(Asynchronous Server Gateway Interface) — Python 비동기 웹 프레임워크와 서버 간의 표준 인터페이스 규격이다.
  • 이전 세대인 WSGI(Synchronous)와의 차이: WSGI는 동기만 지원, ASGI는 비동기(async/await)·웹소켓·HTTP/2까지 지원한다.
  • 역할 구조: 클라이언트 → uvicorn(ASGI 서버, 네트워크 수신·처리) → FastAPI(ASGI 앱, 라우팅·비즈니스 로직) → 응답

uvicorn:

  • 약자가 아니다 — “uvicorn”은 고유 이름이다.
  • 이름의 유래: uvuvloop(Python 비동기 이벤트 루프를 C로 구현한 고성능 라이브러리)에서 따왔고, corn은 unicorn(유니콘, 단일 프로세스 서버)의 전통적 명명 관습에서 왔다. 즉 “uvloop 기반의 유니콘 서버”.
  • 특징: asyncio의 대안인 uvloop를 사용해 일반 Python asyncio 대비 훨씬 빠른 I/O 처리를 제공한다.

3.2 최소 실행 예제

# main.py
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def root():
    return {"message": "Hello, World"}

@app.get("/health")
def health_check():
    return {"status": "ok"}
# 서버 실행
uvicorn main:app --reload --port 8000

# 확인
curl http://localhost:8000/
# {"message":"Hello, World"}

# API 문서 (브라우저에서 접속)
# http://localhost:8000/docs

uvicorn main:app에서 main은 파일명, app은 FastAPI 인스턴스 변수명이다. --reload는 코드 변경 시 자동 재시작한다 (개발용).

port 8000은 관습적 기본값이다. 기술적으로는 0–65535 범위에서 자유롭게 선택 가능하지만, 8000이 자주 쓰이는 이유는 다음과 같다.
- 8000은 관습적 기본값이다 — uvicorn(및 Django 개발서버 등)이 개발용 기본 포트로 자주 쓰기 때문에 예제에 8000을 많이 사용한다.
- 기술적 이유 없음 — TCP 포트는 0–65535 범위에서 자유롭게 선택 가능하므로 기능적으로 8000이어야 할 이유는 없다.
- 실무 고려사항:
- 포트 < 1024는 특권 포트(루트 권한 필요) → 일반적으로 피함.
- 프로덕션에서는 보통 80(HTTP)/443(HTTPS)을 리버스프록시(Nginx)나 로드밸런서가 맡고, 애플리케이션은 내부 포트(예: 8000)에서 실행함.
- 충돌 회피: 이미 사용 중인 포트가 있으면 다른 포트(3000/5000/8080 등)로 바꾼다.
- 바꾸는 법 예시:
- uvicorn main:app --port 5000
- 컨테이너/클라우드에서는 보통 환경변수 PORT를 읽어 설정함.

3.3 이 코드에서 일어나는 일

  1. FastAPI()로 앱 인스턴스를 생성한다
  2. @app.get("/")데코레이터(decorator)로, “GET 요청이 / 경로로 들어오면 아래 함수를 실행하라”는 의미이다
  3. 함수가 dict를 반환하면 FastAPI가 자동으로 JSON으로 직렬화하여 응답한다
  4. 별도 설정 없이 /docs에서 Swagger UI 문서가 자동 생성된다

여기까지가 FastAPI의 가장 단순한 형태이다. 하지만 실제 API를 만들려면 “클라이언트가 보내는 데이터를 어떻게 검증하고, 서버가 돌려주는 데이터 형식을 어떻게 보장하는가?”라는 문제를 풀어야 한다. 이것이 다음 단계인 Pydantic의 역할이다.

4 2단계: Pydantic — 데이터 스키마 정의

4.1 왜 Pydantic이 필요한가

API를 만들면 외부에서 들어오는 데이터를 검증해야 한다. 사용자가 보내는 JSON이 기대한 형식인지, 필수 필드가 있는지, 값의 범위가 올바른지 확인하지 않으면 서버 내부에서 예상치 못한 에러가 발생한다.

Pydantic 없이 검증하면:

def run_agent(data: dict):
    if "text" not in data:
        raise ValueError("text 필드 필요")
    if not isinstance(data["text"], str):
        raise TypeError("text는 문자열이어야 한다")
    if "temperature" in data and not (0 <= data["temperature"] <= 2):
        raise ValueError("temperature는 0~2 사이")
    # ... 필드마다 반복

Pydantic으로 검증하면:

from pydantic import BaseModel, Field

class RunRequest(BaseModel):
    text: str
    temperature: float = Field(default=0.7, ge=0, le=2)

request = RunRequest(**data)  # 검증 + 변환이 한 줄에 끝난다

타입 힌트 자체가 검증 규칙이 된다. 별도 if 문이 필요 없다.

4.2 BaseModel: Pydantic의 핵심

정의: Pydantic BaseModel

BaseModel은 Pydantic의 핵심 클래스이다. 이 클래스를 상속하면 필드의 타입 선언만으로 자동 검증, JSON 직렬화/역직렬화, 스키마 생성이 가능해진다. FastAPI는 이 Pydantic 모델을 내장 의존성으로 사용한다.

from pydantic import BaseModel

class Query(BaseModel):
    text: str                          # 필수 (기본값 없음)
    history: list[dict] = []           # 선택 (기본값: 빈 리스트)
    user_id: str | None = None         # 선택 (기본값: None)

# 생성 (검증 포함)
q = Query(text="RAG란?")
print(q.text)       # "RAG란?"
print(q.history)    # []
print(q.user_id)    # None

# 유효하지 않은 데이터
Query()              # ValidationError: text 필드가 필요하다
Query(text=123)      # ValidationError: text는 str이어야 한다

기본값이 없는 필드는 필수, 있는 필드는 선택이다. str | None = None 패턴은 “문자열이거나 None이고, 기본값은 None”을 의미한다.

4.3 요청 모델과 응답 모델

FastAPI에서 Pydantic은 두 가지 방향으로 사용된다:

  • 요청 모델: 클라이언트가 보내는 JSON의 형식을 정의한다 (입력 검증)
  • 응답 모델: 서버가 돌려주는 JSON의 형식을 정의한다 (출력 보장)
# 요청: 클라이언트 → 서버
class RunRequest(BaseModel):
    text: str
    history: list[dict] = []
    user_id: str | None = None

# 응답 내부: 인용 정보
class Citation(BaseModel):
    source: str
    page: int | None = None
    section: str | None = None

# 응답: 서버 → 클라이언트
class RunResponse(BaseModel):
    text: str
    citations: list[Citation] = []
    run_id: str
    latency_ms: int

이 모델들은 API의 계약(contract)이다. 요청과 응답의 형식이 코드에 명시되어 있으므로, 프론트엔드 개발자는 이 스키마만 보고 API를 호출할 수 있다. Swagger UI(/docs)에도 이 스키마가 자동으로 표시된다.

Pydantic의 심화 내용(Field 제약 조건, validator, Enum, 설정 관리, ABC와의 결합 등)은 Pydantic – 데이터 검증과 직렬화에서 다룬다.

5 3단계: 라우팅 — URL과 함수를 연결한다

1단계에서 @app.get("/")으로 URL과 함수를 연결하는 것을 보았다. 이 섹션에서는 클라이언트로부터 데이터를 받는 세 가지 방법과, 2단계에서 정의한 Pydantic 모델을 활용하여 응답 형식을 보장하는 방법을 다룬다.

5.1 경로 매개변수 (Path Parameters)

URL 경로의 일부를 변수로 받는다. 자원을 식별할 때 사용한다.

@app.get("/agents/{agent_name}")
def get_agent(agent_name: str):
    return {"agent": agent_name}

# GET /agents/qna_chatbot → {"agent": "qna_chatbot"}
# GET /agents/data_standardizer → {"agent": "data_standardizer"}

Python의 타입 힌트 agent_name: str을 FastAPI가 읽어 자동으로 타입 검증한다. int로 선언하면 정수가 아닌 값이 들어왔을 때 자동으로 422 Unprocessable Entity를 반환한다.

@app.get("/users/{user_id}")
def get_user(user_id: int):
    return {"user_id": user_id}

# GET /users/42   → {"user_id": 42}
# GET /users/abc  → 422 Unprocessable Entity (자동 검증)

5.2 쿼리 매개변수 (Query Parameters)

URL 뒤에 ?key=value 형태로 전달한다. 선택적 필터링에 사용한다.

@app.get("/monitoring/metrics")
def get_metrics(agent: str = "all", period: str = "7d", page: int = 1):
    return {"agent": agent, "period": period, "page": page}

# GET /monitoring/metrics                        → 기본값 사용
# GET /monitoring/metrics?agent=qna_chatbot       → agent만 지정
# GET /monitoring/metrics?agent=qna_chatbot&period=30d&page=2

기본값이 있으면 선택 매개변수, 없으면 필수 매개변수가 된다. Pydantic 모델의 기본값 규칙과 동일한 원리이다.

5.3 요청 본문 (Request Body)

  • POST 요청의 데이터는 JSON 본문으로 전달한다.
  • 2단계에서 정의한 Pydantic 모델을 함수 매개변수의 타입으로 지정하면 FastAPI가 자동으로 JSON → Pydantic 객체 변환과 검증을 수행한다.
@app.post("/agents/qna_chatbot/run")
def run_agent(request: RunRequest):
    return {
        "response": {
            "text": f"질문 '{request.text}'에 대한 답변입니다.",
            "run_id": "abc-123"
        }
    }
curl -X POST http://localhost:8000/agents/qna_chatbot/run \
  -H "Content-Type: application/json" \
  -d '{"text": "RAG란?", "user_id": "user_42"}'

FastAPI가 자동으로 수행하는 작업:

  1. JSON 본문을 파싱한다
  2. RunRequest 모델로 타입 검증한다 (text가 누락되면 422 반환)
  3. 기본값을 채운다 (history가 없으면 빈 리스트)
  4. Python 객체로 변환하여 함수에 전달한다
  5. 반환값을 JSON으로 직렬화하여 응답한다

5.4 응답 모델 (Response Model)

요청 검증만으로는 부족하다. 서버가 돌려주는 응답의 형식도 보장해야 한다. response_model 매개변수로 응답 스키마를 지정한다.

class AgentResult(BaseModel):
    text: str
    citations: list[Citation] = []
    run_id: str
    latency_ms: int

class RunResponse(BaseModel):
    response: AgentResult
    experiment_id: str | None = None

@app.post("/agents/qna_chatbot/run", response_model=RunResponse)
def run_agent(request: RunRequest):
    result = agent.run(request)
    return RunResponse(
        response=AgentResult(
            text=result.text,
            citations=result.citations,
            run_id=result.run_id,
            latency_ms=result.latency_ms,
        )
    )

response_model을 지정하면:

  • 필터링: 모델에 정의되지 않은 필드는 응답에서 자동 제거되어 내부 구현이 노출되지 않는다
  • 문서화: Swagger UI에 응답 스키마가 표시되어 프론트엔드 개발자가 참조할 수 있다
  • 검증: 반환값이 모델 형식에 맞는지 확인한다

5.5 매개변수 판별 규칙 요약

함수 매개변수를 선언할 때 FastAPI는 다음 규칙으로 데이터 소스를 판별한다:

매개변수 형태 FastAPI가 읽는 위치 예시
경로에 {name}이 있으면 경로 매개변수 agent_name: str
기본 타입 (str, int 등) 쿼리 매개변수 page: int = 1
Pydantic BaseModel 요청 본문 (JSON) request: RunRequest

6 4단계: 라우터 분리 — 코드를 모듈로 나눈다

엔드포인트가 3~4개를 넘으면 한 파일에 모든 코드를 넣기 어렵다. APIRouter로 엔드포인트를 도메인별 파일로 분리한다.

# routers/qna_chatbot.py
from fastapi import APIRouter

router = APIRouter(prefix="/agents/qna_chatbot", tags=["QnA Chatbot"])

@router.post("/run")
def run(request: RunRequest):
    ...

@router.post("/stream")
def stream(request: RunRequest):
    ...

@router.get("/documents")
def list_documents():
    ...
# main.py
from fastapi import FastAPI
from routers import qna_chatbot, data_standardizer, monitoring

app = FastAPI(title="MINERVA API")

app.include_router(qna_chatbot.router)
app.include_router(data_standardizer.router)
app.include_router(monitoring.router)
  • prefix를 설정하면 라우터 내부에서는 상대 경로만 적으면 된다. @router.post("/run")의 실제 경로는 /agents/qna_chatbot/run이 된다
  • tags는 Swagger UI에서 엔드포인트를 그룹별로 묶어 표시한다
  • main.py는 라우터를 조합하고 미들웨어를 설정하는 허브 역할만 한다. 비즈니스 로직은 각 라우터 파일에 둔다

7 5단계: 의존성 주입 — 공통 로직을 재사용한다

여러 엔드포인트에서 공통으로 필요한 로직(인증, DB 연결, 설정 로드 등)을 Depends로 주입한다.

7.1 기본 패턴

from fastapi import Depends, Header, HTTPException

def get_current_user(token: str = Header(None)):
    if not token:
        raise HTTPException(status_code=401, detail="인증 필요")
    return decode_token(token)

@app.get("/agents/qna_chatbot/documents")
def list_documents(user=Depends(get_current_user)):
    return get_docs_for_user(user)

Depends(get_current_user)는 엔드포인트가 호출될 때마다 get_current_user먼저 실행하고, 그 결과를 user 매개변수에 주입한다. 인증 로직을 매 엔드포인트에 복붙하지 않아도 된다.

7.2 의존성 주입이 해결하는 문제

의존성 주입 없이는 공통 로직을 매 엔드포인트에 반복해야 한다:

# 의존성 주입 없이 — 모든 엔드포인트에 인증 코드 반복
@app.get("/documents")
def list_documents(token: str = Header(None)):
    if not token:
        raise HTTPException(status_code=401, detail="인증 필요")
    user = decode_token(token)
    return get_docs_for_user(user)

@app.get("/metrics")
def get_metrics(token: str = Header(None)):
    if not token:
        raise HTTPException(status_code=401, detail="인증 필요")
    user = decode_token(token)
    return get_metrics_for_user(user)

Depends를 사용하면 이 공통 로직을 한 곳에서 관리하고, 필요한 엔드포인트에 선언만 하면 된다. 인증 방식이 바뀌면 get_current_user 함수 하나만 수정하면 된다.

8 6단계: 비동기 엔드포인트 — I/O 작업을 효율적으로 처리한다

FastAPI는 동기 함수(def)와 비동기 함수(async def)를 모두 지원한다.

동기(synchronous): 요청→처리→응답을 순차적으로 기다리는 방식. 한 작업이 끝나야 다음 작업을 시작함. 처리 중이면 호출자는 블로킹(대기)된다.
- 비유: 계산기에게 문제를 하나씩 내주고 결과를 받을 때까지 다음 문제를 내지 않는 것.
- 장점: 개념이 단순하고 구현·디버깅이 쉬움.
- 단점: I/O(네트워크/디스크) 대기 시간이 많으면 처리량이 낮아짐.

비동기(asynchronous): 작업을 요청하고 결과를 기다리는 동안 다른 작업을 수행할 수 있는 방식. 보통 콜백·프라미스·이벤트루프(async/await)로 구현한다. 호출자는 즉시 제어를 돌려받아 다른 일을 할 수 있다.
- 비유: 식당에서 번호표 받고 기다리는 동안 쇼핑을 하는 것(결과가 준비되면 알림).
- 장점: 많은 I/O 요청을 동시에 효율적으로 처리(높은 동시성).
- 단점: 흐름 제어가 복잡해질 수 있고 디버깅이 어려움(경합·상태관리 주의).

기술적 차이: - 동기 = blocking, 순차 실행
- 비동기 = non-blocking, 동시성(concurrency) 제공 (항상 병렬(parallel)인 건 아님 — 단일 스레드의 이벤트 루프에서 여러 I/O 작업을 중첩 처리할 수 있음)

언제 쓰나: - I/O 대기 많은 서버(LLM 호출, DB 조회 등) → 비동기 권장
- CPU 바운드 작업(대량 계산) → 동기(또는 멀티스레드/멀티프로세스 병렬화)

짧은 예(Python): - 동기: def fetch(): response = requests.get(url) # 블로킹
- 비동기:

# 동기 — CPU 바운드 작업에 적합
@app.get("/health")
def health():
    return {"status": "ok"}

# 비동기 — I/O 바운드 작업에 적합 (LLM 호출, DB 조회)
@app.post("/agents/qna_chatbot/run")
async def run_agent(request: RunRequest):
    result = await llm.ainvoke(request.text)  # LLM 응답 대기 중 다른 요청 처리 가능
    return {"response": result}

8.1 왜 비동기가 중요한가

LLM 호출은 응답까지 수초가 걸린다. 동기 방식에서는 한 요청이 LLM 응답을 기다리는 동안 해당 스레드가 아무것도 하지 않고 멈춰 있다. 비동기 방식에서는 await 지점에서 다른 요청을 처리할 수 있어 동시 처리량이 크게 향상된다.

동기 (def):         요청A [===LLM 대기====] → 요청B [===LLM 대기====]
비동기 (async def): 요청A [=LLM 대기=]
                   요청B [=LLM 대기=]   ← 대기 중에 다른 요청 처리

8.2 언제 어떤 것을 쓰는가

작업 유형 함수 형태 예시
I/O 바운드 (네트워크, DB) async def + await LLM 호출, DB 조회, 외부 API
CPU 바운드 (연산) def 데이터 전처리, 통계 계산
단순 조회 def 또는 async def 헬스체크, 설정 반환

동기 함수를 사용하면 FastAPI가 내부적으로 스레드 풀에서 실행하므로 블로킹 문제는 발생하지 않지만, 최적 성능을 위해서는 I/O 작업에 async def를 사용하는 것이 좋다.

9 7단계: 에러 처리 — 실패를 명확히 전달한다

from fastapi import HTTPException

@app.post("/agents/{agent_name}/run")
def run_agent(agent_name: str, request: RunRequest):
    if agent_name not in AVAILABLE_AGENTS:
        raise HTTPException(
            status_code=404,
            detail=f"에이전트 '{agent_name}'을 찾을 수 없다"
        )

    try:
        result = AVAILABLE_AGENTS[agent_name].run(request)
        return {"response": result}
    except TimeoutError:
        raise HTTPException(
            status_code=503,
            detail="LLM 응답 시간 초과"
        )

HTTPException을 raise하면 FastAPI가 해당 상태 코드와 메시지로 JSON 응답을 자동 생성한다. Pydantic 검증 실패 시에는 FastAPI가 자동으로 422 Unprocessable Entity를 반환한다 (별도 코드 불필요).

9.1 자주 사용하는 에러 패턴

상황 상태 코드 처리 방식
인증 실패 401 Depends에서 raise
존재하지 않는 자원 404 HTTPException(status_code=404)
잘못된 요청 형식 422 Pydantic이 자동 처리
예상치 못한 서버 오류 500 FastAPI가 자동 처리
LLM 타임아웃 503 try-except에서 raise

10 8단계: 미들웨어 — 모든 요청에 공통 처리를 적용한다

  • 미들웨어는 모든 요청과 응답을 가로채어 공통 처리를 수행한다.
  • 5단계의 의존성 주입이 특정 엔드포인트에 적용되는 것과 달리, 미들웨어는 앱 전체에 적용된다.

10.1 CORS 미들웨어

  • 프론트엔드(React, :5173)에서 백엔드(FastAPI, :8000)로 요청하면 다른 출처(origin)이므로 브라우저가 차단한다. CORS 미들웨어로 허용할 출처를 등록한다.
  • 5173은 관습적(사실상 기본값)
    • 요점: Vite(현대적 개발 서버, create-vite로 만든 React 앱)의 기본 개발 포트가 5173
    • 의무는 아님: 기술적으로 아무 포트나 쓸 수 있고 --port/환경변수로 변경 가능
    • 다른 관습 예: Create React App/Next.js는 보통 3000, Angular는 4200, 일반 예시로 8080 등.
    • 실무 팁: 개발 중엔 CORS/허용 오리진을 http://localhost:5173로 맞추고, 프로덕션은 리버스 프록시(80/443) 사용.
from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:5173", "http://localhost:3000"],
    allow_methods=["*"],
    allow_headers=["*"],
)

CORS의 상세 원리는 CORS와 Proxy 포스트에서 다룬다.

10.2 미들웨어의 실행 순서

요청 →  미들웨어 (CORS 검사)  →  의존성 주입 (인증)  →  엔드포인트 함수
응답 ←  미들웨어 (헤더 추가)  ←  의존성 주입 (정리)  ←  반환값 직렬화

미들웨어는 의존성 주입보다 먼저 실행된다. CORS가 허용되지 않으면 인증 검사에 도달하기도 전에 요청이 거부된다.

11 9단계: Lifespan 이벤트 — 서버 시작과 종료

서버 시작/종료 시 실행할 작업을 정의한다. AI Agent에서는 모델 로딩, 벡터 인덱스 초기화 등에 사용한다.

from contextlib import asynccontextmanager

@asynccontextmanager
async def lifespan(app: FastAPI):
    # --- startup: 서버 시작 시 1회 실행 ---
    print("에이전트 초기화 중...")
    agent = QnaChatbotAgent()
    agent.warmup()                    # 벡터 인덱스 로드, LLM 연결 확인
    app.state.agent = agent           # 앱 상태에 저장

    yield  # --- 서버 실행 중 (요청 처리) ---

    # --- shutdown: 서버 종료 시 1회 실행 ---
    print("리소스 정리 중...")

app = FastAPI(lifespan=lifespan)
  • yield 이전이 startup, 이후가 shutdown이다
  • app.state에 저장한 객체는 엔드포인트에서 request.app.state.agent로 접근할 수 있다
  • 이 패턴은 구버전의 @app.on_event("startup")를 대체한다. FastAPI 공식 문서에서 lifespan 방식을 권장한다

11.1 왜 Lifespan이 필요한가

  • Lifespan 없이 에이전트를 엔드포인트 안에서 초기화하면 첫 요청이 수십 초 걸린다 (벡터 인덱스 로드 + LLM 연결).
  • Lifespan에서 미리 초기화하면 서버가 “준비 완료” 상태에서 요청을 받기 시작한다.

12 프로젝트 구조 패턴

위 단계를 모두 적용한 실제 AI Agent API 서버의 파일 구조이다.

src/services/api/
├── main.py              # 앱 생성, lifespan, 미들웨어, 라우터 등록 (9→8→4단계)
├── schemas.py           # Pydantic 요청/응답 모델 (2단계)
└── routers/             # 도메인별 라우터 파일 (3~7단계)
    ├── health.py        # GET /health
    ├── qna_chatbot.py   # POST /agents/qna_chatbot/{run,stream}
    ├── data_standardizer.py
    ├── monitoring.py    # GET /monitoring/metrics
    ├── records.py       # GET /records
    ├── feedback.py      # POST /feedback
    └── ab.py            # GET/POST /experiments
파일 담당 관련 단계
main.py 앱 생성, lifespan, 미들웨어, 라우터 등록 1, 4, 8, 9
schemas.py 모든 Pydantic 모델 (요청/응답) 2
routers/*.py 각 도메인의 엔드포인트 + 의존성 3, 5, 6, 7

각 라우터 파일은 하나의 도메인만 담당한다. main.py는 라우터를 조합하고 미들웨어를 설정하는 허브 역할만 한다. schemas.py에 Pydantic 모델을 모아두면 라우터 간에 공유하기 쉽다.

13 관련 주제

선행 지식

  • API 기초 – REST, HTTP 메서드, JSON, 상태 코드

후속 주제

다른 카테고리 연결

Subscribe

Enjoy this blog? Get notified of new posts by email: