이미지 로딩 중...

바닥부터 만드는 ChatGPT FastAPI 웹 서버 구축 완벽 가이드 - 슬라이드 1/9
A

AI Generated

2025. 11. 12. · 6 Views

바닥부터 만드는 ChatGPT FastAPI 웹 서버 구축 완벽 가이드

FastAPI를 활용하여 ChatGPT 기반 웹 서버를 처음부터 구축하는 방법을 다룹니다. 비동기 처리, OpenAI API 통합, 프로젝트 구조 설계까지 실무에 바로 적용할 수 있는 내용을 제공합니다.


목차

  1. FastAPI 프로젝트 초기 설정
  2. FastAPI 기본 라우터 구성
  3. OpenAI API 통합
  4. 비동기 처리로 성능 최적화
  5. 요청 검증과 Pydantic 모델
  6. CORS 설정과 보안
  7. 환경 변수 관리
  8. 에러 핸들링과 예외 처리

1. FastAPI 프로젝트 초기 설정

시작하며

여러분이 AI 기반 웹 서비스를 만들려고 할 때, 어떤 프레임워크를 선택해야 할지 고민한 적 있나요? Django는 너무 무겁고, Flask는 비동기 처리가 불편하고, 타입 체크도 번거롭습니다.

이런 문제는 실제 개발 현장에서 자주 발생합니다. 특히 OpenAI API처럼 외부 API를 호출하는 서비스는 동기 처리로는 성능 병목이 생기기 쉽고, 타입 체크가 없으면 런타임 에러가 빈번합니다.

바로 이럴 때 필요한 것이 FastAPI 프로젝트 초기 설정입니다. FastAPI는 비동기 처리와 타입 힌트를 기본으로 지원하여 현대적인 웹 서버를 빠르게 구축할 수 있게 해줍니다.

개요

간단히 말해서, FastAPI 프로젝트 초기 설정은 Python 가상환경을 만들고 필요한 라이브러리를 설치하여 개발 환경을 구축하는 과정입니다. 왜 이 과정이 필요한지 실무 관점에서 설명하자면, 프로젝트마다 서로 다른 버전의 라이브러리를 사용할 수 있기 때문입니다.

예를 들어, 프로젝트 A에서는 FastAPI 0.100을 쓰고 프로젝트 B에서는 0.110을 쓸 때 가상환경이 없다면 버전 충돌이 발생합니다. 기존에는 pip로 전역에 패키지를 설치했다면, 이제는 venv나 poetry로 프로젝트별 독립 환경을 만들 수 있습니다.

핵심 특징은 첫째, 프로젝트 격리로 의존성 충돌 방지, 둘째, requirements.txt나 pyproject.toml로 재현 가능한 환경 제공, 셋째, 개발/운영 환경 일관성 유지입니다. 이러한 특징들이 팀 협업과 배포 시 문제를 최소화합니다.

코드 예제

# 가상환경 생성 및 활성화
python -m venv venv
source venv/bin/activate  # Windows: venv\Scripts\activate

# FastAPI와 필수 패키지 설치
pip install fastapi uvicorn[standard] openai python-dotenv pydantic

# requirements.txt 생성하여 의존성 기록
pip freeze > requirements.txt

# main.py 파일 생성 - 기본 FastAPI 앱
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
    return {"message": "ChatGPT Server is running"}

설명

이것이 하는 일: FastAPI 프로젝트를 시작하기 위한 기본 환경을 구축합니다. 첫 번째로, python -m venv venv 명령으로 가상환경을 생성합니다.

이렇게 하는 이유는 시스템 전역 Python 환경과 분리하여 프로젝트별 독립적인 패키지 관리를 하기 위해서입니다. venv 폴더 안에 Python 인터프리터와 pip가 복사되어 격리된 환경이 만들어집니다.

그 다음으로, source venv/bin/activate로 가상환경을 활성화하고 pip install 명령으로 필요한 패키지들을 설치합니다. fastapi는 웹 프레임워크 코어이고, uvicorn은 ASGI 서버로 FastAPI 앱을 실행하는 데 필요합니다.

openai는 ChatGPT API를 호출하기 위한 공식 클라이언트 라이브러리이며, python-dotenv는 환경 변수를 .env 파일에서 읽어오는 도구입니다. 마지막으로, pip freeze > requirements.txt로 설치된 패키지 목록을 파일로 저장합니다.

이 파일이 있으면 다른 개발자나 서버에서 pip install -r requirements.txt 한 번으로 동일한 환경을 재현할 수 있습니다. 여러분이 이 설정을 완료하면 일관된 개발 환경에서 작업할 수 있고, 배포 시에도 같은 버전의 라이브러리를 사용하여 "내 컴퓨터에서는 되는데" 문제를 방지할 수 있습니다.

기본 FastAPI 앱을 만드는 코드는 매우 간단합니다. FastAPI() 인스턴스를 생성하고 @app.get("/") 데코레이터로 루트 경로를 처리하는 함수를 등록하면 끝입니다.

uvicorn main:app --reload로 서버를 실행하면 localhost:8000에서 API가 동작합니다.

실전 팁

💡 가상환경 이름은 venv 외에 .venv로 해도 좋습니다. .venv는 숨김 폴더로 인식되어 Git에 실수로 커밋될 가능성이 줄어듭니다.

💡 requirements.txt 대신 poetry를 사용하면 의존성 해결과 버전 관리가 더 편리합니다. poetry init으로 프로젝트를 시작하고 poetry add fastapi uvicorn으로 패키지를 추가하세요.

💡 개발용 패키지(pytest, black 등)는 별도로 관리하세요. pip install pytest --dev나 poetry add --group dev pytest로 개발 의존성을 분리하면 운영 서버에 불필요한 패키지가 설치되지 않습니다.

💡 .gitignore에 venv/, pycache/, .env를 반드시 추가하세요. 가상환경 폴더와 환경 변수 파일은 Git에 올리면 안 됩니다.

💡 uvicorn main:app --reload의 --reload 옵션은 코드 변경 시 자동으로 서버를 재시작해줍니다. 개발할 때는 필수지만, 운영 환경에서는 제거해야 합니다.


2. FastAPI 기본 라우터 구성

시작하며

여러분이 API를 설계할 때 모든 엔드포인트를 main.py 하나에 다 넣으면 어떻게 될까요? 100줄, 200줄로 늘어나면서 코드를 찾기도 힘들고 유지보수가 악몽이 됩니다.

이런 문제는 실제 개발 현장에서 자주 발생합니다. API가 많아질수록 하나의 파일에서 관리하기 어려워지고, 팀원들이 동시에 수정하면 Git 충돌도 빈번해집니다.

바로 이럴 때 필요한 것이 FastAPI 라우터 구성입니다. APIRouter를 사용해 기능별로 엔드포인트를 분리하면 코드 구조가 깔끔해지고 협업이 수월해집니다.

개요

간단히 말해서, FastAPI 라우터는 관련된 API 엔드포인트들을 그룹으로 묶어 별도 파일로 관리하는 기능입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하자면, 대규모 프로젝트에서는 수십 개의 엔드포인트가 생기기 때문입니다.

예를 들어, /chat, /chat/history, /chat/clear 같은 채팅 관련 API들을 하나의 chat_router.py에 모아두면 관리가 훨씬 쉽습니다. 기존에는 main.py에 @app.get(), @app.post()를 모두 작성했다면, 이제는 APIRouter를 만들고 app.include_router()로 연결할 수 있습니다.

핵심 특징은 첫째, 기능별 라우터 분리로 코드 가독성 향상, 둘째, prefix를 사용해 URL 경로를 일괄 관리, 셋째, tags로 API 문서를 자동 그룹핑합니다. 이러한 특징들이 API 설계를 체계적으로 만들고 문서화를 자동화합니다.

코드 예제

# routers/chat.py - 채팅 관련 라우터
from fastapi import APIRouter

router = APIRouter(
    prefix="/chat",  # 모든 경로에 /chat 접두사 추가
    tags=["Chat"]    # API 문서에서 Chat 그룹으로 표시
)

@router.post("/message")
async def send_message(content: str):
    # ChatGPT API 호출 로직 (다음 섹션에서 구현)
    return {"response": f"Echo: {content}"}

@router.get("/history")
async def get_history():
    return {"messages": []}

# main.py - 라우터 등록
from fastapi import FastAPI
from routers import chat

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

설명

이것이 하는 일: 관련된 API 엔드포인트들을 모듈화하여 관리합니다. 첫 번째로, routers 폴더를 만들고 chat.py 파일에 APIRouter 인스턴스를 생성합니다.

prefix="/chat"을 설정하면 이 라우터의 모든 경로 앞에 자동으로 /chat이 붙습니다. 따라서 @router.post("/message")는 실제로 /chat/message 경로가 됩니다.

이렇게 하는 이유는 URL 구조를 일관되게 유지하고, 나중에 prefix를 바꾸면 모든 경로가 한 번에 변경되기 때문입니다. 그 다음으로, tags=["Chat"]을 설정하면 FastAPI가 자동 생성하는 Swagger 문서(localhost:8000/docs)에서 이 라우터의 엔드포인트들이 "Chat" 그룹으로 묶여 표시됩니다.

이는 API 문서를 체계적으로 만들어 프론트엔드 개발자나 API 사용자가 쉽게 찾을 수 있게 합니다. 마지막으로, main.py에서 app.include_router(chat.router)로 라우터를 등록합니다.

FastAPI 앱에 여러 라우터를 추가할 수 있고, 각 라우터는 독립적으로 개발하고 테스트할 수 있습니다. 여러분이 이 구조를 사용하면 chat.py, user.py, admin.py 등으로 기능을 분리하여 팀원들이 동시에 작업할 수 있고, Git 충돌도 줄어듭니다.

또한 새로운 기능을 추가할 때 기존 코드를 건드리지 않고 새 라우터 파일만 만들면 되므로 유지보수가 쉽습니다. 실무에서는 라우터마다 의존성(dependency)을 설정하여 인증, 로깅, 속도 제한 등을 일괄 적용하기도 합니다.

예를 들어, APIRouter(dependencies=[Depends(verify_token)])로 라우터 전체에 토큰 검증을 적용할 수 있습니다.

실전 팁

💡 라우터 파일명과 prefix를 일치시키세요. chat.py는 prefix="/chat", user.py는 prefix="/user"처럼 규칙을 정하면 파일 위치를 쉽게 찾을 수 있습니다.

💡 라우터가 많아지면 routers/init.py에서 한 번에 import하세요. from .chat import router as chat_router처럼 별칭을 사용하면 main.py에서 app.include_router(chat_router)로 간결하게 등록할 수 있습니다.

💡 버전 관리가 필요하면 prefix="/api/v1/chat"처럼 버전 번호를 포함하세요. API 변경 시 v2를 만들어 하위 호환성을 유지할 수 있습니다.

💡 FastAPI의 자동 문서(localhost:8000/docs)를 활용하세요. 코드만 작성하면 Swagger UI가 자동 생성되어 API를 테스트하고 공유하기 편합니다.

💡 라우터마다 별도의 테스트 파일을 만드세요. tests/test_chat.py처럼 구조를 맞추면 어떤 기능을 테스트하는지 명확해집니다.


3. OpenAI API 통합

시작하며

여러분이 ChatGPT를 내 서비스에 붙이려고 할 때, 공식 문서만 보고 바로 구현하기 어려웠던 경험 있나요? API 키 설정, 요청 형식, 응답 파싱까지 신경 써야 할 게 많습니다.

이런 문제는 실제 개발 현장에서 자주 발생합니다. OpenAI API를 잘못 호출하면 429 에러(rate limit)가 나거나, 비용이 예상보다 많이 나가거나, 응답이 느려서 사용자 경험이 나빠집니다.

바로 이럴 때 필요한 것이 OpenAI API 통합입니다. Python 공식 클라이언트를 사용하면 간단한 코드로 ChatGPT를 호출하고, 스트리밍 응답까지 처리할 수 있습니다.

개요

간단히 말해서, OpenAI API 통합은 openai 라이브러리를 사용해 FastAPI 서버에서 ChatGPT 모델을 호출하는 작업입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하자면, 사용자가 채팅 메시지를 보낼 때마다 ChatGPT의 응답을 받아와야 하기 때문입니다.

예를 들어, 고객 지원 챗봇, 코드 리뷰 도구, 학습 도우미 같은 서비스에서 OpenAI API는 핵심 기능입니다. 기존에는 requests 라이브러리로 직접 HTTP 요청을 만들었다면, 이제는 openai.ChatCompletion.create()로 간단히 호출할 수 있습니다.

핵심 특징은 첫째, 공식 클라이언트로 안정적인 API 호출, 둘째, 비동기 지원으로 성능 최적화, 셋째, 스트리밍으로 실시간 응답 제공입니다. 이러한 특징들이 사용자 경험을 크게 개선하고 서버 리소스를 효율적으로 사용하게 합니다.

코드 예제

import os
from openai import AsyncOpenAI
from dotenv import load_dotenv

# 환경 변수에서 API 키 로드
load_dotenv()
client = AsyncOpenAI(api_key=os.getenv("OPENAI_API_KEY"))

# FastAPI 엔드포인트
@router.post("/message")
async def send_message(content: str):
    # ChatGPT API 호출
    response = await client.chat.completions.create(
        model="gpt-3.5-turbo",  # 또는 gpt-4
        messages=[
            {"role": "system", "content": "You are a helpful assistant."},
            {"role": "user", "content": content}
        ],
        max_tokens=500,
        temperature=0.7
    )

    # 응답 추출
    answer = response.choices[0].message.content
    return {"response": answer}

설명

이것이 하는 일: FastAPI 서버에서 OpenAI ChatGPT 모델을 호출하고 응답을 반환합니다. 첫 번째로, load_dotenv()로 .env 파일에서 OPENAI_API_KEY를 읽어옵니다.

이렇게 하는 이유는 API 키를 코드에 하드코딩하면 Git에 올라가 보안 문제가 생기기 때문입니다. .env 파일에 OPENAI_API_KEY=sk-...처럼 저장하고 .gitignore에 추가하면 안전합니다.

그 다음으로, AsyncOpenAI 클라이언트를 생성합니다. AsyncOpenAI는 비동기 메서드를 제공하여 await으로 호출하는 동안 다른 요청을 처리할 수 있습니다.

동기 OpenAI 클라이언트를 쓰면 API 응답을 기다리는 동안 서버가 블로킹되어 성능이 떨어집니다. client.chat.completions.create() 메서드로 ChatGPT를 호출합니다.

model 파라미터는 사용할 모델(gpt-3.5-turbo, gpt-4 등)을 지정하고, messages는 대화 히스토리를 전달합니다. system 메시지는 AI의 행동을 정의하고, user 메시지는 사용자 입력입니다.

max_tokens는 응답 길이를 제한하여 비용을 관리하고, temperature는 창의성을 조절합니다(0에 가까우면 결정적, 1에 가까우면 랜덤). 마지막으로, response.choices[0].message.content로 AI의 답변을 추출합니다.

OpenAI API는 여러 응답을 반환할 수 있지만 보통 첫 번째만 사용합니다. 여러분이 이 코드를 사용하면 사용자가 POST /chat/message로 메시지를 보낼 때마다 ChatGPT가 답변하는 챗봇을 만들 수 있습니다.

실무에서는 대화 히스토리를 데이터베이스에 저장하고, 이전 대화를 messages에 포함시켜 문맥을 유지합니다. 비동기 처리 덕분에 여러 사용자가 동시에 요청해도 서버가 빠르게 응답하고, 스트리밍을 사용하면 타이핑 효과처럼 실시간으로 답변을 보여줄 수 있습니다.

실전 팁

💡 대화 히스토리를 세션이나 데이터베이스에 저장하세요. messages에 이전 대화를 포함하면 ChatGPT가 문맥을 이해하여 더 자연스러운 대화가 가능합니다.

💡 max_tokens를 적절히 설정하여 비용을 관리하세요. gpt-4는 비싸므로 짧은 응답이 필요하면 max_tokens=200처럼 제한하세요.

💡 에러 처리를 반드시 추가하세요. try-except로 openai.error.RateLimitError, openai.error.APIError 등을 잡아 사용자에게 친절한 메시지를 보여주세요.

💡 스트리밍 응답을 사용하면 UX가 좋아집니다. stream=True로 설정하고 async for chunk in response로 부분 응답을 받아 실시간으로 전송하세요.

💡 모델 선택은 비용과 성능을 고려하세요. 간단한 질문은 gpt-3.5-turbo로 충분하고, 복잡한 추론이 필요하면 gpt-4를 쓰세요.


4. 비동기 처리로 성능 최적화

시작하며

여러분이 웹 서버를 운영할 때 동시 사용자가 늘어나면서 응답이 점점 느려진 경험 있나요? 한 사용자가 OpenAI API 응답을 기다리는 동안 다른 사용자들도 대기해야 한다면 큰 문제입니다.

이런 문제는 실제 개발 현장에서 자주 발생합니다. 동기 코드는 I/O 작업(API 호출, 데이터베이스 쿼리)이 끝날 때까지 블로킹되어 서버 처리량이 급격히 떨어집니다.

바로 이럴 때 필요한 것이 비동기 처리입니다. async/await 패턴을 사용하면 I/O 대기 중에 다른 요청을 처리하여 서버 성능을 몇 배로 높일 수 있습니다.

개요

간단히 말해서, 비동기 처리는 async def로 함수를 정의하고 await으로 I/O 작업을 호출하여 블로킹 없이 동시성을 확보하는 기법입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하자면, OpenAI API는 응답에 1-3초 걸리는데 이 시간 동안 서버가 멈춰 있으면 안 되기 때문입니다.

예를 들어, 동기 코드에서 10명이 동시에 요청하면 10번째 사용자는 30초를 기다리지만, 비동기 코드에서는 모두 3초 안에 응답을 받습니다. 기존에는 threading이나 multiprocessing으로 동시성을 구현했다면, 이제는 asyncio와 FastAPI의 내장 비동기 지원으로 간단히 처리할 수 있습니다.

핵심 특징은 첫째, I/O 바운드 작업에서 높은 동시성, 둘째, 단일 스레드에서 수천 개의 요청 처리, 셋째, 코드가 직관적이고 디버깅이 쉽습니다. 이러한 특징들이 서버 비용을 줄이고 사용자 경험을 개선합니다.

코드 예제

import asyncio
from fastapi import FastAPI
from openai import AsyncOpenAI

app = FastAPI()
client = AsyncOpenAI(api_key=os.getenv("OPENAI_API_KEY"))

# 비동기 엔드포인트
@app.post("/chat")
async def chat_endpoint(message: str):
    # await으로 비동기 API 호출 - 블로킹 없음
    response = await client.chat.completions.create(
        model="gpt-3.5-turbo",
        messages=[{"role": "user", "content": message}]
    )
    return {"answer": response.choices[0].message.content}

# 여러 API를 동시에 호출하는 예제
@app.post("/multi-chat")
async def multi_chat(messages: list[str]):
    # asyncio.gather로 병렬 처리
    tasks = [
        client.chat.completions.create(model="gpt-3.5-turbo", messages=[{"role": "user", "content": msg}])
        for msg in messages
    ]
    responses = await asyncio.gather(*tasks)
    return {"answers": [r.choices[0].message.content for r in responses]}

설명

이것이 하는 일: I/O 바운드 작업을 비블로킹 방식으로 처리하여 서버 처리량을 극대화합니다. 첫 번째로, async def chat_endpoint로 비동기 함수를 정의합니다.

FastAPI는 async 함수를 자동으로 이벤트 루프에서 실행하여 동시 요청을 효율적으로 처리합니다. 일반 def 함수는 스레드 풀에서 실행되므로 동시성이 제한적입니다.

그 다음으로, await client.chat.completions.create()로 OpenAI API를 호출합니다. await 키워드는 "이 작업이 끝날 때까지 기다리되, 그동안 다른 요청을 처리해"라는 의미입니다.

API 응답을 기다리는 동안 이벤트 루프가 다른 chat_endpoint 호출을 실행하므로 동시에 수백 명의 사용자를 처리할 수 있습니다. multi_chat 예제는 asyncio.gather를 사용하여 여러 API 호출을 병렬로 실행합니다.

3개의 메시지를 순차적으로 보내면 9초(3초×3)가 걸리지만, gather로 동시에 보내면 3초만에 모든 응답을 받습니다. 마지막으로, await asyncio.gather(*tasks)로 모든 태스크가 완료될 때까지 기다립니다.

각 태스크는 독립적으로 실행되어 하나가 느려도 다른 것들은 영향을 받지 않습니다. 여러분이 이 패턴을 사용하면 서버 인스턴스 하나로 훨씬 많은 사용자를 처리할 수 있고, 클라우드 비용을 절감할 수 있습니다.

실무에서는 데이터베이스 쿼리, 외부 API 호출, 파일 I/O 등 모든 I/O 작업을 비동기로 만들어 최대 성능을 뽑아냅니다. 비동기 코드는 CPU 바운드 작업(복잡한 계산)에는 효과가 없지만, 웹 서버는 대부분 I/O 바운드이므로 비동기가 항상 유리합니다.

실전 팁

💡 비동기 라이브러리를 사용하세요. requests 대신 httpx, pymongo 대신 motor처럼 비동기 버전을 쓰면 전체 시스템이 비동기로 동작합니다.

💡 await 없이 async 함수를 호출하면 안 됩니다. result = async_function()은 코루틴 객체만 반환하고 실행되지 않으므로 result = await async_function()으로 써야 합니다.

💡 블로킹 코드(time.sleep, requests.get)는 비동기 함수 안에서 쓰면 안 됩니다. 이벤트 루프를 막아 성능이 떨어지므로 asyncio.sleep, httpx.get처럼 비동기 대체제를 쓰세요.

💡 에러 처리는 각 태스크에 개별적으로 하세요. asyncio.gather의 return_exceptions=True를 쓰면 일부 태스크가 실패해도 나머지는 계속 실행됩니다.

💡 FastAPI의 백그라운드 태스크를 활용하세요. 응답을 먼저 보내고 로깅이나 알림을 나중에 처리하려면 BackgroundTasks를 쓰면 사용자 응답 시간이 단축됩니다.


5. 요청 검증과 Pydantic 모델

시작하며

여러분이 API를 만들 때 사용자가 잘못된 데이터를 보내면 어떻게 처리하나요? 문자열이 와야 하는데 숫자가 오거나, 필수 필드가 빠지면 서버가 에러를 뱉거나 심지어 크래시될 수 있습니다.

이런 문제는 실제 개발 현장에서 자주 발생합니다. 수동으로 if not message or len(message) > 1000 같은 검증 코드를 쓰면 코드가 지저분해지고, 빠뜨린 검증은 보안 취약점이 됩니다.

바로 이럴 때 필요한 것이 Pydantic 모델입니다. 타입 힌트로 데이터 구조를 정의하면 FastAPI가 자동으로 검증하고, 잘못된 요청은 친절한 에러 메시지와 함께 거부합니다.

개요

간단히 말해서, Pydantic 모델은 Python 클래스로 데이터 스키마를 정의하고 자동으로 타입 검증, 변환, 직렬화를 수행하는 라이브러리입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하자면, API가 받는 데이터의 형식을 명확히 정의하고 신뢰할 수 있게 만들어야 하기 때문입니다.

예를 들어, 채팅 메시지가 최소 1자 이상, 최대 1000자 이하여야 한다는 규칙을 Pydantic으로 선언하면 모든 요청이 자동으로 검증됩니다. 기존에는 if isinstance(message, str)처럼 수동 검증을 했다면, 이제는 message: str = Field(min_length=1, max_length=1000)로 선언적으로 규칙을 정의할 수 있습니다.

핵심 특징은 첫째, 타입 안전성으로 런타임 에러 방지, 둘째, 자동 검증으로 보안 강화, 셋째, API 문서 자동 생성입니다. 이러한 특징들이 코드 품질을 높이고 개발 속도를 빠르게 합니다.

코드 예제

from pydantic import BaseModel, Field, validator
from typing import Optional

# 요청 데이터 모델
class ChatRequest(BaseModel):
    message: str = Field(..., min_length=1, max_length=1000, description="User's chat message")
    user_id: Optional[str] = Field(None, description="Optional user ID for tracking")
    temperature: float = Field(0.7, ge=0.0, le=2.0, description="ChatGPT creativity level")

    # 커스텀 검증
    @validator('message')
    def message_not_empty(cls, v):
        if not v.strip():
            raise ValueError('Message cannot be empty or whitespace only')
        return v.strip()

# FastAPI 엔드포인트에서 사용
@router.post("/chat")
async def chat(request: ChatRequest):
    # request.message는 이미 검증되고 strip() 처리됨
    response = await client.chat.completions.create(
        model="gpt-3.5-turbo",
        messages=[{"role": "user", "content": request.message}],
        temperature=request.temperature
    )
    return {"answer": response.choices[0].message.content}

설명

이것이 하는 일: API 요청 데이터의 구조와 검증 규칙을 선언적으로 정의합니다. 첫 번째로, BaseModel을 상속한 ChatRequest 클래스를 정의합니다.

각 필드는 타입 힌트와 Field로 세부 규칙을 설정합니다. message: str = Field(..., min_length=1, max_length=1000)은 "message는 문자열이고 1자 이상 1000자 이하"라는 의미입니다.

...은 필수 필드를 나타내고, Optional[str]은 선택 필드입니다. 그 다음으로, @validator 데코레이터로 커스텀 검증을 추가합니다.

message_not_empty는 메시지가 공백만 있는지 확인하고, 문제가 있으면 ValueError를 발생시킵니다. FastAPI는 이를 자동으로 422 Unprocessable Entity 에러로 변환하여 사용자에게 반환합니다.

FastAPI 엔드포인트에서 async def chat(request: ChatRequest)처럼 파라미터 타입을 Pydantic 모델로 지정하면, FastAPI가 요청 JSON을 자동으로 파싱하고 검증합니다. 검증이 통과하면 request 객체가 생성되고, 실패하면 자동으로 에러 응답이 전송됩니다.

마지막으로, request.message, request.temperature처럼 필드에 안전하게 접근할 수 있습니다. IDE는 자동완성을 제공하고, 타입 체커(mypy)는 컴파일 시점에 타입 오류를 잡아냅니다.

여러분이 이 패턴을 사용하면 수동 검증 코드를 작성할 필요가 없고, API 사용자는 localhost:8000/docs에서 어떤 필드가 필요한지, 각 필드의 제약 조건은 무엇인지 자동 생성된 문서로 확인할 수 있습니다. 실무에서는 응답 모델도 Pydantic으로 정의하여 response_model=ChatResponse처럼 지정하면 응답 데이터도 검증되고 문서화됩니다.

실전 팁

💡 Field의 description을 꼭 작성하세요. API 문서에 표시되어 프론트엔드 개발자가 이해하기 쉽습니다.

💡 @validator 대신 Pydantic v2의 @field_validator를 쓰면 더 강력합니다. mode='before'로 변환 전 검증도 가능합니다.

💡 response_model을 설정하면 응답에서 불필요한 필드를 자동으로 제거합니다. 예를 들어, 데이터베이스 모델을 그대로 반환하면 password 필드까지 노출되지만, response_model을 쓰면 안전합니다.

💡 Config 클래스로 모델 동작을 커스터마이즈하세요. class Config: str_strip_whitespace = True로 모든 문자열 필드의 공백을 자동 제거할 수 있습니다.

💡 중첩 모델을 활용하세요. class ChatRequest(BaseModel): user: User로 복잡한 데이터 구조도 깔끔하게 정의할 수 있습니다.


6. CORS 설정과 보안

시작하며

여러분이 React나 Vue로 프론트엔드를 만들고 FastAPI 백엔드를 호출했을 때 "CORS policy" 에러를 본 적 있나요? localhost:3000에서 localhost:8000을 호출하면 브라우저가 차단합니다.

이런 문제는 실제 개발 현장에서 자주 발생합니다. 브라우저는 보안상 다른 origin(도메인, 포트)으로의 요청을 기본적으로 막기 때문에, 백엔드에서 명시적으로 허용해야 합니다.

바로 이럴 때 필요한 것이 CORS 설정입니다. FastAPI의 CORSMiddleware를 추가하면 프론트엔드와 백엔드를 안전하게 연결할 수 있습니다.

개요

간단히 말해서, CORS(Cross-Origin Resource Sharing)는 브라우저가 다른 origin의 리소스에 접근할 수 있도록 서버가 허용하는 메커니즘입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하자면, 프론트엔드와 백엔드가 다른 포트나 도메인에서 실행되는 경우가 대부분이기 때문입니다.

예를 들어, 개발 환경에서는 프론트엔드가 localhost:3000, 백엔드가 localhost:8000이고, 운영 환경에서는 app.example.com과 api.example.com처럼 분리됩니다. 기존에는 nginx나 Apache에서 CORS 헤더를 설정했다면, 이제는 FastAPI 미들웨어로 간단히 처리할 수 있습니다.

핵심 특징은 첫째, origin별 접근 제어로 보안 유지, 둘째, 개발/운영 환경별 설정 분리, 셋째, preflight 요청 자동 처리입니다. 이러한 특징들이 프론트엔드 개발을 원활하게 하면서도 보안을 유지합니다.

코드 예제

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import os

app = FastAPI()

# CORS 설정 - 환경에 따라 다르게 설정
if os.getenv("ENVIRONMENT") == "production":
    origins = [
        "https://app.example.com",  # 운영 프론트엔드 도메인
    ]
else:
    origins = [
        "http://localhost:3000",     # React 개발 서버
        "http://localhost:5173",     # Vite 개발 서버
    ]

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,           # 허용할 origin 목록
    allow_credentials=True,          # 쿠키 포함 허용
    allow_methods=["*"],             # 모든 HTTP 메서드 허용
    allow_headers=["*"],             # 모든 헤더 허용
)

# 이제 프론트엔드에서 fetch("http://localhost:8000/chat") 가능

설명

이것이 하는 일: 특정 origin에서 오는 브라우저 요청을 허용하여 프론트엔드와 백엔드를 연결합니다. 첫 번째로, FastAPI 앱에 CORSMiddleware를 추가합니다.

allow_origins는 요청을 허용할 origin 목록입니다. origins = ["*"]로 모든 origin을 허용하면 편하지만 보안상 위험하므로, 실제 프론트엔드 URL만 명시하는 것이 좋습니다.

그 다음으로, 환경 변수로 개발/운영 환경을 구분합니다. 개발 환경에서는 localhost의 여러 포트를 허용하고, 운영 환경에서는 실제 도메인만 허용합니다.

이렇게 하는 이유는 공격자가 자신의 사이트에서 내 API를 호출하는 것을 막기 위해서입니다. allow_credentials=True는 쿠키나 인증 헤더를 포함한 요청을 허용합니다.

로그인 기능이 있다면 필수입니다. allow_methods=[""]는 GET, POST, PUT, DELETE 등 모든 HTTP 메서드를 허용하고, allow_headers=[""]는 Content-Type, Authorization 같은 헤더를 허용합니다.

마지막으로, 브라우저는 실제 요청 전에 OPTIONS 메서드로 preflight 요청을 보내 서버가 CORS를 지원하는지 확인합니다. CORSMiddleware가 자동으로 응답하므로 개발자는 신경 쓸 필요가 없습니다.

여러분이 이 설정을 완료하면 프론트엔드에서 fetch() 또는 axios로 백엔드 API를 자유롭게 호출할 수 있고, 브라우저 콘솔에 CORS 에러가 사라집니다. 실무에서는 API 게이트웨이나 CDN에서 CORS를 설정하기도 하지만, FastAPI에서 직접 설정하면 개발 환경 구축이 빠르고 간단합니다.

실전 팁

💡 운영 환경에서는 절대 allow_origins=["*"]를 쓰지 마세요. 모든 사이트에서 내 API를 호출할 수 있어 CSRF 공격에 취약해집니다.

💡 allow_methods와 allow_headers는 필요한 것만 허용하세요. 예를 들어, GET과 POST만 쓴다면 allow_methods=["GET", "POST"]로 제한하세요.

💡 프론트엔드가 여러 개면 origins 리스트에 모두 추가하세요. 관리자 페이지, 사용자 페이지가 따로 있다면 각각의 URL을 허용해야 합니다.

💡 개발 중에 CORS 에러가 나면 브라우저 개발자 도구의 Network 탭을 확인하세요. preflight 요청이 실패하는지, 응답 헤더가 올바른지 확인할 수 있습니다.

💡 쿠키 기반 인증을 사용한다면 allow_credentials=True와 함께 프론트엔드에서 credentials: 'include'를 설정해야 합니다.


7. 환경 변수 관리

시작하며

여러분이 OpenAI API 키를 코드에 직접 넣고 Git에 푸시한 적 있나요? 몇 분 안에 봇이 탐지하여 API 키가 도용되고, 수백 달러의 요금이 청구될 수 있습니다.

이런 문제는 실제 개발 현장에서 자주 발생합니다. API 키, 데이터베이스 비밀번호, 암호화 키 같은 민감 정보를 코드에 하드코딩하면 보안 사고로 이어집니다.

바로 이럴 때 필요한 것이 환경 변수 관리입니다. .env 파일과 python-dotenv를 사용하면 민감 정보를 코드와 분리하고 안전하게 관리할 수 있습니다.

개요

간단히 말해서, 환경 변수 관리는 설정 값을 코드 외부(.env 파일)에 저장하고 런타임에 읽어오는 방식입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하자면, 개발/스테이징/운영 환경마다 다른 설정을 사용해야 하기 때문입니다.

예를 들어, 개발 환경에서는 테스트용 API 키를 쓰고, 운영 환경에서는 실제 결제되는 키를 쓰는데, 코드에 하드코딩하면 환경별 관리가 불가능합니다. 기존에는 config.py에 상수로 정의했다면, 이제는 .env 파일에 KEY=value 형식으로 저장하고 os.getenv()로 읽어올 수 있습니다.

핵심 특징은 첫째, 민감 정보를 Git에서 분리하여 보안 강화, 둘째, 환경별 설정을 쉽게 전환, 셋째, 12-factor app 원칙 준수입니다. 이러한 특징들이 안전하고 확장 가능한 애플리케이션을 만듭니다.

코드 예제

# .env 파일 (Git에 커밋하지 말 것!)
OPENAI_API_KEY=sk-proj-abcd1234...
DATABASE_URL=postgresql://user:pass@localhost/dbname
ENVIRONMENT=development
MAX_TOKENS=500
TEMPERATURE=0.7

# main.py - 환경 변수 로드
import os
from dotenv import load_dotenv
from pydantic_settings import BaseSettings

# .env 파일 로드
load_dotenv()

# Pydantic으로 타입 안전한 설정 관리
class Settings(BaseSettings):
    openai_api_key: str
    database_url: str
    environment: str = "development"
    max_tokens: int = 500
    temperature: float = 0.7

    class Config:
        env_file = ".env"

settings = Settings()

# 사용 예시
client = AsyncOpenAI(api_key=settings.openai_api_key)

설명

이것이 하는 일: 설정 값을 코드 외부에서 관리하여 보안과 유연성을 확보합니다. 첫 번째로, 프로젝트 루트에 .env 파일을 만들고 KEY=value 형식으로 설정을 작성합니다.

.env 파일은 반드시 .gitignore에 추가하여 Git에 올라가지 않도록 해야 합니다. 대신 .env.example 파일을 만들어 필요한 키 목록을 공유하되 실제 값은 비웁니다.

그 다음으로, load_dotenv()를 호출하여 .env 파일의 내용을 환경 변수로 로드합니다. 이후 os.getenv("OPENAI_API_KEY")로 값을 읽어올 수 있습니다.

하지만 os.getenv()는 타입이 str | None이므로 매번 체크해야 합니다. 더 나은 방법은 pydantic-settings의 BaseSettings를 사용하는 것입니다.

Settings 클래스에 필드를 정의하면 자동으로 환경 변수를 읽어오고, 타입 검증까지 수행합니다. openai_api_key: str로 정의하면 OPENAI_API_KEY 환경 변수를 읽어 문자열로 변환하고, 없으면 에러를 발생시킵니다.

마지막으로, settings 객체를 싱글톤처럼 사용하여 전역에서 접근합니다. settings.openai_api_key처럼 타입 안전하게 설정값을 사용할 수 있고, IDE의 자동완성도 지원됩니다.

여러분이 이 패턴을 사용하면 API 키가 Git에 노출될 위험이 사라지고, 환경별로 다른 .env 파일(.env.dev, .env.prod)을 만들어 쉽게 전환할 수 있습니다. 실무에서는 운영 환경에서 .env 파일 대신 AWS Secrets Manager, Azure Key Vault 같은 비밀 관리 서비스를 사용하여 더 높은 수준의 보안을 확보합니다.

실전 팁

💡 .env.example을 만들어 필요한 키 목록을 공유하세요. 팀원이 프로젝트를 클론하면 .env.example을 .env로 복사하고 자신의 키를 채워 넣으면 됩니다.

💡 환경별로 파일을 분리하세요. .env.dev, .env.staging, .env.prod를 만들고 load_dotenv(".env.dev")처럼 로드하면 환경 전환이 쉽습니다.

💡 기본값을 제공하세요. temperature: float = 0.7처럼 기본값을 설정하면 .env에 없어도 동작하여 개발이 편합니다.

💡 필수 값은 타입에 기본값을 주지 마세요. openai_api_key: str처럼 기본값 없이 정의하면 .env에 없을 때 시작 시점에 에러가 발생하여 런타임 문제를 방지합니다.

💡 Docker를 쓴다면 docker-compose.yml에서 env_file: .env로 환경 변수를 주입하세요. 컨테이너 내부에서도 동일한 방식으로 설정을 관리할 수 있습니다.


8. 에러 핸들링과 예외 처리

시작하며

여러분이 서비스를 운영할 때 사용자가 이상한 입력을 넣거나 OpenAI API가 다운되면 어떻게 대응하나요? 서버가 500 에러를 뱉고 크래시되면 사용자는 무슨 일이 일어났는지 알 수 없습니다.

이런 문제는 실제 개발 현장에서 자주 발생합니다. 예외 처리 없이 코드를 작성하면 예상치 못한 상황에서 서버가 멈추고, 디버깅도 어렵습니다.

바로 이럴 때 필요한 것이 에러 핸들링입니다. try-except와 FastAPI의 HTTPException을 사용하면 문제를 안전하게 처리하고 사용자에게 친절한 메시지를 보낼 수 있습니다.

개요

간단히 말해서, 에러 핸들링은 예외가 발생할 수 있는 코드를 try-except로 감싸고, 적절한 HTTP 상태 코드와 메시지로 응답하는 것입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하자면, 외부 API 호출, 데이터베이스 쿼리, 파일 I/O 같은 작업은 언제든 실패할 수 있기 때문입니다.

예를 들어, OpenAI API가 rate limit을 초과하면 429 에러를 반환하는데, 이를 처리하지 않으면 사용자는 "Internal Server Error"만 보고 당황합니다. 기존에는 if error: return {"error": "..."}처럼 수동으로 에러를 반환했다면, 이제는 raise HTTPException(status_code=429, detail="...")으로 표준화된 에러 응답을 만들 수 있습니다.

핵심 특징은 첫째, 예외를 안전하게 처리하여 서버 안정성 확보, 둘째, 사용자에게 명확한 에러 메시지 제공, 셋째, 로깅으로 디버깅 지원입니다. 이러한 특징들이 운영 가능한 서비스를 만듭니다.

코드 예제

from fastapi import HTTPException, status
from openai import AsyncOpenAI, OpenAIError, RateLimitError, APIError
import logging

# 로거 설정
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

@router.post("/chat")
async def chat(request: ChatRequest):
    try:
        # OpenAI API 호출
        response = await client.chat.completions.create(
            model="gpt-3.5-turbo",
            messages=[{"role": "user", "content": request.message}],
            temperature=request.temperature
        )
        return {"answer": response.choices[0].message.content}

    except RateLimitError:
        # Rate limit 초과 - 재시도 안내
        logger.warning(f"Rate limit exceeded for user {request.user_id}")
        raise HTTPException(
            status_code=status.HTTP_429_TOO_MANY_REQUESTS,
            detail="API rate limit exceeded. Please try again in a minute."
        )

    except APIError as e:
        # OpenAI API 에러 - 상세 로깅
        logger.error(f"OpenAI API error: {e}")
        raise HTTPException(
            status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
            detail="AI service is temporarily unavailable. Please try again later."
        )

    except Exception as e:
        # 예상치 못한 에러 - 일반 에러 메시지
        logger.exception(f"Unexpected error: {e}")
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail="An unexpected error occurred."
        )

설명

이것이 하는 일: 예외 발생 시 서버가 크래시하지 않고 사용자에게 유용한 에러 정보를 제공합니다. 첫 번째로, try 블록 안에 실패할 수 있는 코드를 넣습니다.

OpenAI API 호출은 네트워크 문제, rate limit, API 장애 등 다양한 이유로 실패할 수 있습니다. try 없이 호출하면 예외가 발생했을 때 FastAPI가 500 에러를 반환하고 사용자는 원인을 알 수 없습니다.

그 다음으로, except 블록에서 특정 예외를 처리합니다. RateLimitError는 OpenAI API가 요청 제한을 초과했을 때 발생하므로, 429 상태 코드로 응답하고 "잠시 후 다시 시도하세요"라고 안내합니다.

logger.warning으로 로그를 남기면 나중에 rate limit을 늘려야 하는지 판단할 수 있습니다. APIError는 OpenAI 서버 측 문제로, 503 Service Unavailable로 응답합니다.

이는 일시적인 문제이므로 사용자에게 재시도를 권장합니다. logger.error로 상세 정보를 남기면 OpenAI 상태 페이지와 비교하여 원인을 파악할 수 있습니다.

마지막으로, 예상치 못한 예외는 Exception으로 잡아 500 에러로 처리합니다. logger.exception은 스택 트레이스까지 기록하여 디버깅에 유용합니다.

사용자에게는 상세 에러를 노출하지 않고 일반적인 메시지만 보여 보안을 유지합니다. 여러분이 이 패턴을 사용하면 서비스가 안정적으로 운영되고, 에러 발생 시 빠르게 원인을 파악하고 대응할 수 있습니다.

실무에서는 Sentry 같은 에러 추적 도구를 연동하여 모든 예외를 자동으로 수집하고, 심각도별로 알림을 받습니다.

실전 팁

💡 구체적인 예외부터 먼저 처리하세요. except RateLimitError를 except Exception보다 위에 두어야 정확한 처리가 가능합니다.

💡 사용자에게 너무 상세한 에러를 보여주지 마세요. 스택 트레이스나 내부 정보는 로그에만 남기고, 사용자에게는 간단한 메시지만 제공하세요.

💡 재시도 로직을 추가하세요. tenacity 라이브러리로 @retry(stop=stop_after_attempt(3))를 적용하면 일시적인 네트워크 문제를 자동으로 해결할 수 있습니다.

💡 커스텀 예외를 만들어 비즈니스 로직을 명확히 하세요. class InvalidMessageError(Exception)를 정의하고 적절히 raise하면 코드 가독성이 높아집니다.

💡 health check 엔드포인트를 만드세요. @app.get("/health")로 서버와 OpenAI API 연결 상태를 확인할 수 있게 하면 모니터링이 쉬워집니다.

이상 "바닥부터 만드는 ChatGPT FastAPI 웹 서버 구축 완벽 가이드"였습니다. 각 개념을 차근차근 따라 하면 실무에서 바로 사용할 수 있는 AI 웹 서버를 구축할 수 있습니다!


#Python#FastAPI#OpenAI#AsyncAPI#WebServer#ai

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.