Development environment

[FastAPI] FastAPI 제대로 이해하기 : 코드와 개념으로

hjjummy 2025. 8. 20. 15:53

FastAPI를 처음 접하면 “파이썬으로 API 서버 만드는 도구” 정도로만 이해하기 쉽다. 그런데 막상 실제 프로젝트 코드와 연결해 보면, 라우팅은 어떻게 되는지, 라우팅이 어떻게 동작하는지, async가 어떤 상황에서 빛을 발하는지, Depends는 왜 필요한지, Pydantic은 어디서 힘을 발휘하는지 헷갈리기 마련이다.

그래서 이번 글에서는 FastAPI를 단순히 소개하는 수준을 넘어서,  작성한 코드와 1:1로 매칭해 보면서 개념이 실제로 어떻게 녹아들어 있는지를 짚어보려고 한다.

 

이전글에서도 간단하게  FastAPI를 언급한적은 있었지만 오늘은 FastAPI에 대해서만 중점적으로 다뤄보려고 한다. 

2025.08.20 - [프로젝트 & 대외활동] - FastAPI · Docker · EC2로 보는 백엔드 개발 흐름

 

FastAPI · Docker · EC2로 보는 백엔드 개발 흐름

지난번 프로젝트에서는 내가 FastAPI 코드를 작성하고 Docker 환경을 구축하는 작업을 맡았었다. 하지만 서버에 직접 배포하는 단계까지는 가지 못했고, 실제로 EC2에 올려 서비스를 운영하는 부분

hjjummy.tistory.com

 

 

글의 흐름은 다음과 같다.

  • FastAPI가 무엇인지
  • FastAPI 핵심 개념 4가지(Router, Async, Depends, Pydantic)
  • 실제 코드에서 이 개념들이 어떻게 적용되었는지
  • 운영에서 바로 써먹을 보완/개선 팁

1. FastAPI란?

"FastAPI는 Python 기반의 웹 프레임워크이다."

 

이렇게만 말하면 감이 안올 수도 있는 분들을 위해 “웹 프레임워크”라는 말을 풀어보자면,,

브라우저나 앱이 서버에 GET /users?limit=10 같은 요청을 보내면, 서버는 그 요청을 읽고 DB에서 데이터를 꺼내 JSON으로 응답한다. 이 과정에서 반복적으로 필요한 작업들 — 라우팅, 요청 파싱, 검증, 에러 처리, 자동 문서화 — 을 일일이 붙이는 대신, 프레임워크가 기본기를 대신 제공한다.

 

FastAPI  = 파이썬으로 “타입 안전 + 비동기 + 자동 문서화” 기반의 API 서버를 빠르게 만드는 프레임워크

 

주로 REST API 서버AI inference API를 빠르게 만들 때 많이 쓰인다. Flask와 비슷하지만 다음 이유로 최근 프로젝트에서 특히 선호된다.

  • 빠름: Starlette(ASGI) + uvicorn 조합으로 비동기 I/O에 최적화됨. 대기 많은 작업(외부 API, DB I/O)에 강함.
  • 안전함: 타입 힌트 기반 검증(Pydantic)으로 요청/응답 스키마가 명확, 버그를 초기에 잡기 쉬움.
  • 자동 문서화: /docs(Swagger UI), /redoc(ReDoc) 자동 생성. 백엔드-프론트 협업이 편해짐.
  • 개발자 경험: IDE에서 자동 완성·정적 분석이 잘 됨. 유지보수 용이.

 

 

아래는 정말 간단한 fastapi 코드이고 /hello로 GET 요청하면 JSON 반환한다.

FastAPI 공식 링크

1-1. “Hello”부터

from fastapi import FastAPI
app = FastAPI()

@app.get("/hello")
def hello():
    return {"msg": "hi"}  # /hello로 GET 요청 시 JSON 반환


실행하는 방법은 

uvicorn main:app --reload

 

  • 브라우저에서 http://127.0.0.1:8000/hello → {"msg":"hi"}
  • http://127.0.0.1:8000/docs → 자동 문서(Swagger UI)

 

1-2. 비동기 라우팅

from fastapi import FastAPI
import httpx

app = FastAPI()

@app.get("/weather")
async def weather():
    async with httpx.AsyncClient() as client:
        r = await client.get("https://api.example.com/weather")
    return r.json()

 

 


2. FastAPI 핵심 개념 4가지

FastAPI를 이해하는 데 꼭 알아야 할 개념은 다음 네 가지다.

(1) 라우터(Router)

클라이언트 요청 경로와 함수를 연결한다. 규모가 커지면 APIRouter로 모듈화한다.

from fastapi import APIRouter, FastAPI
router = APIRouter(prefix="/api")

@router.get("/ping")
def ping():
    return {"ok": True}

app = FastAPI()
app.include_router(router)

 

(2) 비동기 처리(async/await)

외부 API/DB 대기 중에도 이벤트 루프가 다른 요청을 처리할 수 있다. 

@app.get("/data")
async def get_data():
    result = await some_async_func()  # 대기 중에도 이벤트 루프는 다른 작업 수행
    return result

주의: 동기 라이브러리(psycopg2 등) 를 async def 안에서 직접 호출하면 이벤트 루프가 막힌다. 임시로는 스레드풀 우회, 장기적으로는 asyncpg 같은 비동기 드라이버를 고려.

 

(3) 의존성 주입(Depends)

요청마다 필요한 공통 리소스(DB 연결, 인증 정보 등)를 함수 인자로 주입한다.

from fastapi import Depends

def get_db():
    conn = db_pool.getconn()
    try:
        yield conn
    finally:
        db_pool.putconn(conn)  # 반드시 반납 보장

@app.get("/items")
def read_items(conn=Depends(get_db)):
    # conn으로 쿼리 수행
    return {"count": 42}

의존성은 캐싱/스코프/예외 처리까지 일관적으로 다룰 수 있어 유지보수성 ↑

(4) 모델 검증(Pydantic)

요청/응답 스키마를 타입으로 선언하면, FastAPI가 자동으로 검증+문서화를 해준다.

from pydantic import BaseModel

class Message(BaseModel):
    user: str
    text: str

@app.post("/send")
def send_message(msg: Message):
    return {"status": "ok", "echo": msg.text}

잘못된 JSON 형식이면  422 Unprocessable Entity로 자동 에러를 돌려준다.


 

3. 코드 속에서 FastAPI 개념 찾기

여기까지가 FastAPI의 핵심 개념이었다.
이제는 실제 코드를 보면서, 앞에서 정리한 Router / Async / Depends / Pydantic 네 가지가 어떻게 쓰이고 있는지 하나씩 짚어보자.

 

3-1. Router — 엔드포인트 구조

가장 먼저 눈에 띄는 건 라우터이다. 규모가 커질수록 모든 엔드포인트를 main.py에 다 적어 넣을 수는 없기 때문에 보통 APIRouter를 활용한다.

예를 들어 대화 API를 담당하는 chat_router가 있다고 하자.

from fastapi import APIRouter

router = APIRouter(prefix="/api/chat")

@router.get("/ping")
def ping():
    return {"ok": True}

 

그리고 이 라우터를 메인 앱에 등록한다.

from fastapi import FastAPI
from .routers import chat_router

app = FastAPI()
app.include_router(chat_router.router)

→ 이렇게 하면 /api/chat/ping 같은 엔드포인트가 살아난다.
즉, 라우터는 코드의 구조를 쪼개고 API를 모듈화하는 핵심 축이라고 볼 수 있다.

 

 

3-2. Async — 외부 자원과의 연결

내 코드에서 async def가 쓰인 부분은 대부분 DB 조회나 외부 API 호출이다.

예를 들어 날씨 API를 불러오는 경우

import httpx
from fastapi import APIRouter

router = APIRouter(prefix="/api")

@router.get("/weather")
async def get_weather():
    async with httpx.AsyncClient() as client:
        r = await client.get("https://api.example.com/weather")
    return r.json()

여기서 핵심은 I/O가 끝날 때까지 멈추지 않는다는 점이다.
즉, await client.get(...)이 날씨 서버 응답을 기다리는 동안 이벤트 루프는 다른 요청을 동시에 처리할 수 있다.

→ 운영 환경에서는 DB도 같은 방식으로 접근하는 게 이상적이다. 만약 아직 동기 드라이버(psycopg2 등)를 쓰고 있다면, 적어도 스레드풀 우회나 asyncpg 같은 비동기 드라이버로 옮겨가는 게 필요하다.

 

3-3. Depends — 공통 리소스 관리

다음은 의존성 주입이다.
예를 들어 DB 커넥션을 매번 직접 열고 닫는 코드를 엔드포인트마다 넣으면 금방 중복 투성이가 된다. FastAPI의 Depends는 이 문제를 깔끔히 해결해 준다.

from fastapi import Depends

def get_db():
    conn = db_pool.getconn()
    try:
        yield conn
    finally:
        db_pool.putconn(conn)

@router.get("/items")
def read_items(conn=Depends(get_db)):
    return {"count": 42}

read_items 함수는 단순히 conn이라는 인자를 받는 것처럼 보이지만, 실제로는 FastAPI가 요청이 들어올 때마다 get_db()를 실행하고, 끝날 때는 자원 반납까지 보장한다.

→ 이 덕분에 DB 연결 누수 없이, 깔끔하게 자원을 관리할 수 있다.

 

3-4. Pydantic — 데이터 검증과 문서화

대화 API를 예로 들면, 보통 이런 요청 바디를 받게 된다.

{
  "user": "alice",
  "text": "안녕하세요!"
}

이를 Pydantic 모델로 정의해 두면, 자동 검증 + 문서화가 동시에 이루어진다.

from pydantic import BaseModel

class Message(BaseModel):
    user: str
    text: str

@router.post("/send")
def send_message(msg: Message):
    return {"status": "ok", "echo": msg.text}

만약 user를 숫자로 보내거나, text 필드를 누락하면?
FastAPI가 자동으로 422 Unprocessable Entity 에러를 돌려준다.

→ 덕분에 비즈니스 로직은 “들어온 데이터가 유효하다”는 전제 하에서만 작성하면 된다.

 

4. get_conversation_history()의 맥락

이제 실제로 작성했던 함수인 get_conversation_history()를 보자.

이 함수는 특정 유저의 대화 내역을 DB에서 불러오는 역할을 한다. 단순히 보면 “DB에서 select 하고 리턴”이지만, 그 안을 뜯어보면 앞에서 말한 FastAPI 개념들이 다 녹아 있다.

from fastapi import Depends
from pydantic import BaseModel

class ConversationRequest(BaseModel):
    user_id: str

class ConversationResponse(BaseModel):
    history: list[str]

async def get_conversation_history(
    req: ConversationRequest,
    conn=Depends(get_db)
) -> ConversationResponse:
    rows = await conn.fetch("SELECT text FROM conversations WHERE user_id=$1", req.user_id)
    return ConversationResponse(history=[row["text"] for row in rows])

여기서 보면

  • Router: /api/chat/history 같은 엔드포인트로 묶임
  • Async: await conn.fetch(...) → DB I/O 대기 중에도 다른 요청 처리
  • Depends: conn=Depends(get_db) → 커넥션 풀 자동 관리
  • Pydantic: 요청(ConversationRequest)과 응답(ConversationResponse) 모두 검증/문서화

즉, get_conversation_history()는 단순 함수가 아니라, FastAPI의 네 가지 개념이 한데 모여 동작하는 집합체이다.


5. 운영에서 바로 써먹을 보완/개선 팁

마지막으로, 실제 운영 환경에서 고려해볼 만한 개선 포인트들을 정리해 본다.

  • DB 연결 최적화: Depends(get_db) + 커넥션 풀 필수
  • 에러 핸들링: try/except로 잡지 말고 HTTPException을 활용해 상태 코드 명확히 반환
  • 스키마 버전 관리: ConversationResponseV1, ConversationResponseV2 같은 식으로 API 변경 대비
  • 캐싱: 최근 대화 내역은 Redis 같은 캐시 계층을 두면 응답 속도 개선
  • 로깅/모니터링: logging + Prometheus/Grafana 같은 모니터링 → 운영 중 병목 구간 바로 파악

 

정리하자면, Router / Async / Depends / Pydantic은 FastAPI의 네 가지 기둥이고, 실제 코드 안에서는 이 네 가지가 서로 맞물려 돌아간다.

특히 get_conversation_history() 같은 함수를 보면, 단순히 데이터만 꺼내오는 게 아니라, 비동기 처리 + DB 연결 관리 + 요청/응답 검증이 동시에 적용되어 있다는 걸 확인할 수 있다.

이런 연결고리를 잡아두면, 예제 따라 하기 수준에서 벗어나 실제 운영 환경에서도 안정적이고 확장 가능한 FastAPI 서버를 설계할 수 있다.

 

FastAPI를 공부하면서 자연스럽게 따라오는 주제가 바로 도커(Docker)인데.
FastAPI는 가볍고 빠른 웹 프레임워크이지만, 실제 운영 환경에서 안정적으로 배포하려면 애플리케이션이 돌아가는 환경(파이썬 버전, 패키지, OS 설정 등)을 일관되게 유지하는 것이 중요하다. 여기서 도커가 핵심 역할을 하게 되므로 다음글에서는 도커에 대해서 알아보려고 한다.