이미지 로딩 중...
AI Generated
2025. 11. 20. · 3 Views
FastAPI 기반 API 서버 개발 완벽 가이드
현대적인 Python 웹 프레임워크 FastAPI를 사용하여 고성능 API 서버를 구축하는 방법을 배웁니다. 비동기 처리부터 Docker 배포까지, 실무에서 바로 사용할 수 있는 완전한 가이드를 제공합니다.
목차
- FastAPI 프로젝트 구조 설계
- /chat 엔드포인트 구현
- Request/Response 스키마 정의
- 비동기 처리로 동시 요청 처리
- CORS 설정 및 보안 고려사항
- Docker 컨테이너화
1. FastAPI 프로젝트 구조 설계
시작하며
여러분이 API 서버를 개발할 때 코드가 점점 복잡해지고 어디에 무엇을 넣어야 할지 헷갈린 적 있나요? 파일이 너무 많아지거나 반대로 하나의 파일에 모든 코드가 몰려있어서 유지보수가 힘들어지는 경험 말이죠.
이런 문제는 실제 개발 현장에서 자주 발생합니다. 특히 프로젝트가 커질수록 처음의 깔끔했던 구조가 점점 무너지고, 나중에는 어디를 수정해야 할지 찾는 것만으로도 시간이 오래 걸리게 됩니다.
바로 이럴 때 필요한 것이 체계적인 프로젝트 구조입니다. FastAPI 프로젝트를 시작할 때부터 올바른 구조로 설계하면, 나중에 기능을 추가하거나 수정할 때 훨씬 수월해집니다.
개요
간단히 말해서, FastAPI 프로젝트 구조는 마치 책장을 정리하는 것과 같습니다. 소설은 소설 칸에, 만화는 만화 칸에 넣듯이, 코드도 그 역할에 맞는 곳에 배치하는 것이죠.
실무에서는 여러 개발자가 함께 작업하고, 시간이 지나면서 기능이 계속 추가됩니다. 예를 들어, 처음에는 간단한 로그인 기능만 있었는데, 나중에는 결제, 알림, 분석 등 수십 개의 기능이 추가되는 경우가 많습니다.
이럴 때 체계적인 구조가 없다면 코드를 찾고 수정하는 것이 매우 어려워집니다. 전통적인 방법에서는 모든 코드를 하나의 파일에 작성했다면, 이제는 기능별로 폴더와 파일을 나누어 관리할 수 있습니다.
이렇게 하면 특정 기능을 수정할 때 그 부분만 찾아서 작업할 수 있습니다. FastAPI 프로젝트 구조의 핵심 특징은 계층적 분리, 재사용성, 확장성입니다.
라우터는 엔드포인트를 관리하고, 스키마는 데이터 형식을 정의하며, 서비스는 비즈니스 로직을 담당합니다. 이러한 특징들이 코드의 유지보수성을 크게 향상시키고, 팀원들과의 협업을 원활하게 만들어줍니다.
코드 예제
# 프로젝트 구조
# app/
# ├── main.py # 애플리케이션 진입점
# ├── routers/ # API 엔드포인트들
# │ ├── __init__.py
# │ └── chat.py # /chat 엔드포인트
# ├── schemas/ # Request/Response 모델
# │ ├── __init__.py
# │ └── chat.py # 채팅 스키마
# ├── services/ # 비즈니스 로직
# │ ├── __init__.py
# │ └── chat_service.py # 채팅 처리 로직
# └── config.py # 설정 파일
# main.py - 애플리케이션의 시작점
from fastapi import FastAPI
from app.routers import chat
app = FastAPI(title="FastAPI Chat Server")
# 라우터 등록 - /chat 엔드포인트를 앱에 연결
app.include_router(chat.router, prefix="/api", tags=["chat"])
설명
이것이 하는 일: FastAPI 프로젝트 구조는 애플리케이션의 각 부분을 명확하게 분리하여, 개발자가 코드를 쉽게 찾고 수정할 수 있도록 도와줍니다. 첫 번째로, main.py는 애플리케이션의 출발점 역할을 합니다.
마치 건물의 입구처럼, 모든 요청이 이곳을 통해 들어옵니다. 여기서는 FastAPI 인스턴스를 생성하고, 필요한 라우터들을 연결합니다.
이렇게 하는 이유는 main.py를 간결하게 유지하고, 실제 로직은 각 모듈에 위임하기 위함입니다. 그 다음으로, routers 폴더에는 각 기능별 엔드포인트가 정의됩니다.
chat.py에는 채팅 관련 API들이, user.py에는 사용자 관련 API들이 들어갑니다. 각 라우터는 독립적으로 동작하면서도 필요한 서비스를 호출하여 실제 작업을 처리합니다.
schemas 폴더는 API의 입력과 출력 형식을 정의하는데, 이를 통해 데이터 검증이 자동으로 이루어집니다. services 폴더에는 실제 비즈니스 로직이 들어갑니다.
예를 들어, 채팅 메시지를 처리하거나 AI 모델을 호출하는 코드가 여기에 위치합니다. 이렇게 분리하면 라우터는 HTTP 요청만 처리하고, 복잡한 로직은 서비스에 맡길 수 있습니다.
마치 식당에서 주문받는 사람과 요리하는 사람이 분리되어 있는 것과 같습니다. 여러분이 이 구조를 사용하면 코드의 재사용성이 높아지고, 테스트가 쉬워지며, 여러 명이 동시에 작업해도 충돌이 적어집니다.
예를 들어, 같은 채팅 서비스를 웹 API와 웹소켓에서 동시에 사용할 수 있고, 서비스 로직만 테스트하거나 라우터만 테스트하는 것도 가능합니다.
실전 팁
💡 폴더 구조를 너무 복잡하게 만들지 마세요. 프로젝트가 작을 때는 간단하게 시작하고, 커질수록 점진적으로 분리하는 것이 좋습니다.
💡 init.py 파일을 활용하여 자주 사용하는 클래스나 함수를 쉽게 import할 수 있도록 만드세요. 예를 들어, from app.schemas import ChatRequest처럼 간결하게 작성할 수 있습니다.
💡 config.py에는 환경 변수, API 키, 데이터베이스 URL 등 설정 정보를 한곳에 모아두세요. 이렇게 하면 배포 환경이 바뀌어도 이 파일만 수정하면 됩니다.
💡 각 폴더에 README.md를 추가하여 해당 폴더의 역할을 설명해두면, 나중에 다른 개발자가 프로젝트를 이해하기 쉽습니다.
💡 utils나 helpers 폴더를 만들어 여러 곳에서 공통으로 사용하는 유틸리티 함수들을 모아두세요. 날짜 변환, 문자열 처리 등이 여기에 해당합니다.
2. /chat 엔드포인트 구현
시작하며
여러분이 AI 챗봇 서비스를 만들 때 사용자의 메시지를 받아서 응답을 돌려주는 API가 필요한데, 어떻게 구현해야 할지 막막했던 적 있나요? 단순히 함수를 만드는 것은 쉽지만, 실제로는 에러 처리, 입력 검증, 응답 형식 등 고려해야 할 것이 많습니다.
이런 문제는 실제 서비스를 만들 때 반드시 마주치게 됩니다. 사용자가 이상한 데이터를 보내거나, 서버에 문제가 생겼을 때 어떻게 처리할지, 또 응답을 어떤 형식으로 돌려줄지 등을 모두 고려해야 합니다.
바로 이럴 때 필요한 것이 FastAPI의 엔드포인트입니다. FastAPI는 자동으로 입력을 검증해주고, API 문서를 만들어주며, 에러를 적절하게 처리해줍니다.
개요
간단히 말해서, FastAPI 엔드포인트는 클라이언트의 요청을 받아서 처리하고 응답을 돌려주는 함수입니다. 마치 식당의 주문 창구처럼, 고객의 주문을 받아서 주방에 전달하고, 완성된 음식을 다시 고객에게 전달하는 역할이죠.
실무에서는 여러 종류의 API를 만들게 되는데, 채팅 API는 그 중에서도 가장 기본적이면서도 중요한 형태입니다. 예를 들어, 사용자가 "오늘 날씨 어때?"라고 물어보면 AI가 적절한 답변을 생성해서 돌려주는 흐름을 구현해야 합니다.
이런 API는 모바일 앱, 웹사이트, 챗봇 등 다양한 곳에서 사용됩니다. 전통적인 Flask에서는 직접 JSON을 파싱하고 검증하는 코드를 작성해야 했다면, FastAPI에서는 Pydantic 모델을 사용하여 자동으로 검증하고 문서화할 수 있습니다.
FastAPI 엔드포인트의 핵심 특징은 타입 힌트 기반 검증, 자동 문서화, 비동기 처리 지원입니다. 함수 파라미터에 타입을 지정하면 FastAPI가 자동으로 검증해주고, 잘못된 데이터가 들어오면 에러 응답을 보냅니다.
또한 /docs 경로로 접속하면 Swagger UI를 통해 API를 테스트해볼 수 있습니다.
코드 예제
# app/routers/chat.py
from fastapi import APIRouter, HTTPException
from app.schemas.chat import ChatRequest, ChatResponse
from app.services.chat_service import process_chat
# 라우터 생성 - 채팅 관련 엔드포인트를 그룹화
router = APIRouter()
# POST /chat 엔드포인트 정의
@router.post("/chat", response_model=ChatResponse)
async def chat_endpoint(request: ChatRequest):
"""
사용자의 메시지를 받아서 AI 응답을 생성합니다.
- request: 사용자 메시지와 설정을 포함
- return: AI의 응답 메시지
"""
try:
# 실제 채팅 처리는 서비스 레이어에 위임
response = await process_chat(request)
return response
except Exception as e:
# 에러 발생 시 적절한 HTTP 에러 반환
raise HTTPException(status_code=500, detail=str(e))
설명
이것이 하는 일: /chat 엔드포인트는 클라이언트로부터 채팅 요청을 받아서 AI 서비스를 호출하고, 그 결과를 다시 클라이언트에게 전달하는 중간 다리 역할을 합니다. 첫 번째로, APIRouter를 생성하여 관련된 엔드포인트들을 그룹화합니다.
이렇게 하는 이유는 나중에 main.py에서 한 번에 여러 엔드포인트를 등록할 수 있고, 공통 설정(prefix, tags 등)을 적용할 수 있기 때문입니다. 마치 여러 개의 물건을 상자에 담아서 한 번에 옮기는 것과 같습니다.
그 다음으로, @router.post 데코레이터를 사용하여 POST 메서드로 접근하는 엔드포인트를 정의합니다. response_model=ChatResponse를 지정하면 FastAPI가 함수의 반환값을 자동으로 ChatResponse 형식으로 변환해줍니다.
또한 API 문서에도 이 정보가 표시되어, 다른 개발자들이 이 API가 어떤 응답을 주는지 쉽게 알 수 있습니다. 함수 파라미터에 request: ChatRequest를 지정하면, FastAPI가 들어오는 JSON 데이터를 자동으로 ChatRequest 객체로 변환해줍니다.
만약 필수 필드가 빠져있거나 타입이 맞지 않으면, 함수가 실행되기 전에 422 에러가 반환됩니다. 이는 개발자가 직접 검증 코드를 작성할 필요가 없다는 뜻입니다.
여러분이 이 코드를 사용하면 API를 매우 빠르게 개발할 수 있고, 자동으로 생성되는 문서를 통해 프론트엔드 개발자와의 소통도 쉬워집니다. 또한 타입 안정성이 보장되어 런타임 에러가 줄어들고, IDE의 자동완성 기능도 더 잘 작동합니다.
실전 팁
💡 response_model을 항상 지정하세요. 이렇게 하면 함수에서 실수로 잘못된 데이터를 반환해도 FastAPI가 자동으로 필터링하여 보안이 강화됩니다.
💡 HTTPException을 사용하여 명확한 에러 메시지를 클라이언트에게 전달하세요. status_code는 상황에 맞게 선택해야 합니다(400: 잘못된 요청, 404: 찾을 수 없음, 500: 서버 에러).
💡 docstring(""" """)을 작성하면 Swagger UI의 API 문서에 설명이 표시됩니다. 다른 개발자들이 API를 이해하는 데 큰 도움이 됩니다.
💡 async def를 사용하면 비동기 처리가 가능해져 동시 요청을 더 효율적으로 처리할 수 있습니다. I/O 작업(DB, API 호출 등)이 많은 경우 특히 유용합니다.
💡 엔드포인트는 최대한 얇게 유지하고, 실제 로직은 서비스 레이어로 분리하세요. 이렇게 하면 같은 로직을 다른 엔드포인트에서도 재사용할 수 있습니다.
3. Request/Response 스키마 정의
시작하며
여러분이 API를 만들 때 클라이언트가 보내야 하는 데이터 형식이나 서버가 돌려주는 데이터 형식을 일일이 설명해야 했던 적 있나요? 또는 사용자가 이상한 데이터를 보냈을 때 일일이 검증하는 코드를 작성하느라 시간을 낭비한 적이 있으신가요?
이런 문제는 API 개발에서 매우 흔합니다. 필드 이름을 잘못 쓰거나, 필수 값이 빠지거나, 숫자가 들어가야 할 곳에 문자열이 들어오는 등의 문제가 발생하면, 디버깅하는 데 많은 시간이 걸립니다.
또한 API 문서가 코드와 따로 관리되면 둘이 불일치하는 문제도 자주 발생합니다. 바로 이럴 때 필요한 것이 Pydantic 스키마입니다.
스키마를 정의하면 자동으로 검증이 이루어지고, API 문서도 자동으로 생성되며, 타입 안정성도 확보됩니다.
개요
간단히 말해서, Pydantic 스키마는 데이터의 형식을 정의하는 설계도입니다. 마치 건축 도면처럼, 어떤 필드가 있고, 각 필드의 타입은 무엇이며, 어떤 것이 필수이고 어떤 것이 선택인지를 명확하게 정의합니다.
실무에서는 API의 요청과 응답 형식이 명확해야 프론트엔드와 백엔드 개발자가 원활하게 협업할 수 있습니다. 예를 들어, 채팅 API를 만든다면 클라이언트가 message, user_id, temperature 같은 필드를 보내야 하고, 서버는 response, timestamp, token_count 같은 필드를 돌려줍니다.
이런 계약을 코드로 명확하게 정의하는 것이 스키마입니다. 전통적인 방법에서는 딕셔너리를 사용하고 수동으로 검증했다면, Pydantic을 사용하면 클래스로 정의하고 자동 검증이 이루어집니다.
또한 타입 힌트를 활용하여 IDE에서 자동완성도 제공됩니다. Pydantic 스키마의 핵심 특징은 자동 검증, 타입 안정성, 자동 문서화입니다.
Field를 사용하여 기본값, 최소/최대값, 설명 등을 추가할 수 있고, 커스텀 validator를 작성하여 복잡한 검증 로직도 구현할 수 있습니다. 이러한 특징들이 개발 시간을 크게 단축시키고, 버그를 사전에 방지합니다.
코드 예제
# app/schemas/chat.py
from pydantic import BaseModel, Field
from typing import Optional
from datetime import datetime
# 클라이언트가 보내는 요청 형식
class ChatRequest(BaseModel):
# 사용자의 메시지 - 필수 필드
message: str = Field(..., min_length=1, max_length=5000,
description="사용자가 입력한 메시지")
# 사용자 ID - 필수 필드
user_id: str = Field(..., description="사용자 고유 식별자")
# AI 응답의 창의성 조절 - 선택 필드, 기본값 0.7
temperature: Optional[float] = Field(0.7, ge=0.0, le=2.0,
description="응답 창의성 (0~2)")
# 최대 토큰 수 - 선택 필드
max_tokens: Optional[int] = Field(1000, ge=1, le=4000)
# 서버가 돌려주는 응답 형식
class ChatResponse(BaseModel):
# AI의 응답 메시지
response: str = Field(..., description="AI가 생성한 응답")
# 응답 생성 시각
timestamp: datetime
# 사용된 토큰 수
token_count: int
# 처리 상태
status: str = Field(default="success")
설명
이것이 하는 일: Pydantic 스키마는 API의 입출력 데이터 구조를 정의하고, 들어오는 데이터를 자동으로 검증하여 올바르지 않은 데이터가 애플리케이션 내부로 들어오는 것을 방지합니다. 첫 번째로, BaseModel을 상속받아 클래스를 정의합니다.
각 필드는 타입 힌트와 함께 선언되며, Optional을 사용하여 선택적 필드를 표시합니다. Field 함수를 사용하면 더 상세한 검증 규칙을 추가할 수 있습니다.
예를 들어, min_length=1은 빈 문자열을 방지하고, ge=0.0(greater than or equal)은 0보다 작은 값을 거부합니다. 그 다음으로, FastAPI가 이 스키마를 사용하여 요청을 처리할 때 자동으로 JSON을 파싱하고 검증합니다.
만약 message 필드가 빠져있거나, temperature가 2보다 크면, FastAPI는 자동으로 422 Unprocessable Entity 에러를 반환하며, 어떤 필드에 문제가 있는지 자세히 알려줍니다. 이 모든 과정이 여러분이 코드를 작성하기 전에 자동으로 처리됩니다.
응답 스키마인 ChatResponse도 마찬가지로 작동합니다. 엔드포인트 함수에서 이 객체를 반환하면, FastAPI가 자동으로 JSON으로 변환하여 클라이언트에게 보냅니다.
datetime 같은 복잡한 타입도 자동으로 ISO 8601 형식의 문자열로 변환됩니다. 여러분이 이 스키마를 사용하면 데이터 검증 코드를 일일이 작성할 필요가 없고, API 문서가 자동으로 생성되며, IDE의 자동완성 기능이 완벽하게 작동합니다.
또한 프론트엔드 개발자가 Swagger UI를 통해 직접 API를 테스트해볼 수 있어, 소통 비용이 크게 줄어듭니다. 타입스크립트를 사용한다면 pydantic-to-typescript 같은 도구로 스키마를 자동 변환할 수도 있습니다.
실전 팁
💡 Field의 description을 상세히 작성하세요. 이 내용이 Swagger UI에 표시되어 API 사용자에게 큰 도움이 됩니다.
💡 example 파라미터를 추가하여 예시 데이터를 제공하면 API 문서가 더 이해하기 쉬워집니다. 예: Field(..., example="안녕하세요")
💡 Config 클래스를 사용하여 스키마 전체의 동작을 커스터마이즈할 수 있습니다. 예를 들어, orm_mode=True로 설정하면 SQLAlchemy 모델을 직접 Pydantic 모델로 변환할 수 있습니다.
💡 validator 데코레이터를 사용하여 복잡한 검증 로직을 구현하세요. 예를 들어, 이메일 형식을 체크하거나, 두 필드의 관계를 검증하는 등의 작업이 가능합니다.
💡 요청과 응답 스키마를 분리하세요. 같은 엔티티라도 생성할 때와 조회할 때 필요한 필드가 다를 수 있습니다. UserCreate, UserResponse처럼 명확하게 구분하면 혼란이 줄어듭니다.
4. 비동기 처리로 동시 요청 처리
시작하며
여러분의 API 서버에 동시에 100명의 사용자가 접속했는데, 한 명씩 순서대로 처리되어서 나중에 온 사람은 한참을 기다려야 하는 상황을 겪어본 적 있나요? 특히 AI 모델 호출처럼 시간이 오래 걸리는 작업이 있으면, 서버가 하나의 요청을 처리하는 동안 다른 모든 요청이 멈춰있게 됩니다.
이런 문제는 실시간 서비스에서 치명적입니다. 사용자들은 빠른 응답을 기대하는데, 서버가 한 번에 한 명씩만 처리한다면 대기 시간이 기하급수적으로 늘어납니다.
특히 데이터베이스 조회, 외부 API 호출, 파일 읽기 같은 I/O 작업이 많은 경우 문제가 더 심각해집니다. 바로 이럴 때 필요한 것이 비동기 처리입니다.
Python의 asyncio와 FastAPI의 비동기 지원을 활용하면, 하나의 서버가 수천 개의 요청을 동시에 처리할 수 있습니다.
개요
간단히 말해서, 비동기 처리는 여러 작업을 동시에 진행하는 기술입니다. 마치 요리사가 물을 끓이는 동안 다른 재료를 손질하는 것처럼, 서버가 외부 API의 응답을 기다리는 동안 다른 요청을 처리할 수 있습니다.
실무에서는 대부분의 웹 서버가 I/O 대기 시간이 전체 처리 시간의 대부분을 차지합니다. 예를 들어, AI 모델이 응답을 생성하는 데 5초가 걸린다면, 동기 방식에서는 그 5초 동안 서버가 아무것도 하지 못합니다.
하지만 비동기 방식에서는 그 5초 동안 수백 개의 다른 요청을 처리할 수 있습니다. CPU는 거의 사용하지 않으면서 대기만 하고 있었기 때문입니다.
전통적인 동기 방식에서는 쓰레드나 프로세스를 많이 만들어서 병렬 처리를 했다면, 비동기 방식에서는 하나의 쓰레드로도 수천 개의 동시 연결을 처리할 수 있습니다. 이는 메모리 사용량과 컨텍스트 스위칭 오버헤드를 크게 줄여줍니다.
비동기 처리의 핵심 특징은 non-blocking I/O, 이벤트 루프, 효율적인 리소스 사용입니다. async/await 키워드를 사용하여 코드를 작성하면, Python 인터프리터가 자동으로 작업을 스케줄링하여 최적의 성능을 냅니다.
이러한 특징들이 같은 하드웨어로 더 많은 사용자를 처리할 수 있게 해주고, 서버 비용을 절감시킵니다.
코드 예제
# app/services/chat_service.py
import asyncio
import httpx
from app.schemas.chat import ChatRequest, ChatResponse
from datetime import datetime
# 비동기 HTTP 클라이언트 - 외부 API 호출용
async def call_ai_api(message: str, temperature: float) -> str:
"""
외부 AI API를 비동기로 호출합니다.
await를 사용하여 응답을 기다리는 동안 다른 작업 처리 가능
"""
async with httpx.AsyncClient() as client:
response = await client.post(
"https://api.openai.com/v1/chat/completions",
json={"message": message, "temperature": temperature},
timeout=30.0 # 타임아웃 설정으로 무한 대기 방지
)
return response.json()["response"]
# 메인 채팅 처리 함수 - async def로 정의
async def process_chat(request: ChatRequest) -> ChatResponse:
"""여러 비동기 작업을 조합하여 채팅 요청을 처리합니다."""
# 여러 작업을 동시에 실행 - asyncio.gather 사용
ai_response, user_info = await asyncio.gather(
call_ai_api(request.message, request.temperature),
fetch_user_info(request.user_id) # DB 조회도 동시에
)
return ChatResponse(
response=ai_response,
timestamp=datetime.now(),
token_count=len(ai_response.split()),
status="success"
)
설명
이것이 하는 일: 비동기 처리는 I/O 작업이 완료되기를 기다리는 동안 CPU를 놀리지 않고 다른 작업을 처리하여, 서버의 처리량을 극적으로 향상시킵니다. 첫 번째로, async def로 함수를 정의하면 이 함수는 코루틴(coroutine)이 됩니다.
코루틴은 실행 중간에 멈췄다가 다시 시작할 수 있는 특별한 함수입니다. await 키워드를 만나면 해당 작업이 완료될 때까지 다른 코루틴에게 제어권을 넘겨줍니다.
이는 마치 여러 개의 작업을 번갈아가며 조금씩 진행하는 것과 같습니다. 그 다음으로, httpx.AsyncClient를 사용하여 외부 API를 비동기로 호출합니다.
일반적인 requests 라이브러리는 동기 방식이라 응답을 받을 때까지 블로킹되지만, httpx는 비동기 방식을 지원하여 여러 API를 동시에 호출할 수 있습니다. async with 문법을 사용하면 작업이 끝난 후 자동으로 리소스를 정리해줍니다.
asyncio.gather를 사용하면 여러 비동기 작업을 동시에 시작하고, 모두 완료될 때까지 기다릴 수 있습니다. 예를 들어, AI API 호출과 데이터베이스 조회를 동시에 실행하면, 두 작업이 순차적으로 실행될 때보다 훨씬 빠릅니다.
AI API가 5초, DB 조회가 1초 걸린다면, 동기식에서는 6초가 걸리지만 비동기식에서는 5초만 걸립니다. 여러분이 이 코드를 사용하면 같은 서버로 훨씬 더 많은 동시 사용자를 처리할 수 있습니다.
실제로 동기 방식에서 초당 10개 요청을 처리하던 서버가 비동기 방식으로 바꾸면 초당 수백 개를 처리할 수 있게 됩니다. 이는 서버 비용을 크게 절감하고, 사용자 경험도 개선시킵니다.
또한 코드가 동기 방식과 거의 비슷하게 생겨서 이해하기 쉽다는 장점도 있습니다.
실전 팁
💡 모든 I/O 작업에 비동기를 사용하세요. 데이터베이스는 asyncpg나 motor, HTTP 호출은 httpx나 aiohttp를 사용하면 됩니다.
💡 CPU 집약적인 작업(복잡한 계산, 이미지 처리 등)은 비동기로 하면 오히려 느려집니다. 이런 경우는 run_in_executor를 사용하여 별도 쓰레드나 프로세스에서 실행하세요.
💡 비동기 함수 안에서 일반 동기 함수를 호출하면 블로킹이 발생합니다. 예를 들어, time.sleep()은 전체 이벤트 루프를 멈추므로 반드시 asyncio.sleep()을 사용해야 합니다.
💡 asyncio.create_task()를 사용하면 작업을 백그라운드에서 실행하고 즉시 반환할 수 있습니다. 로그 저장이나 알림 전송처럼 응답을 기다릴 필요가 없는 작업에 유용합니다.
💡 타임아웃을 항상 설정하세요. 외부 API가 응답하지 않으면 요청이 영원히 매달릴 수 있습니다. asyncio.wait_for()로 타임아웃을 추가할 수 있습니다.
5. CORS 설정 및 보안 고려사항
시작하며
여러분이 프론트엔드 개발자와 함께 작업하는데, 프론트엔드에서 API를 호출하면 "CORS policy error"라는 에러가 나면서 요청이 차단되는 경험을 해본 적 있나요? 또는 API 키가 코드에 노출되거나, 악의적인 사용자가 서버를 공격하는 것을 막지 못해 고민한 적이 있으신가요?
이런 문제는 웹 애플리케이션 개발에서 매우 흔하게 발생합니다. 브라우저는 보안상의 이유로 다른 도메인으로의 요청을 기본적으로 차단하고, API 서버는 누구나 접근할 수 있도록 열려있어서 보안 공격에 취약합니다.
특히 프로덕션 환경에서는 이런 보안 문제가 큰 사고로 이어질 수 있습니다. 바로 이럴 때 필요한 것이 CORS 설정과 보안 미들웨어입니다.
FastAPI는 간단한 설정만으로 CORS를 허용하고, 다양한 보안 기능을 추가할 수 있습니다.
개요
간단히 말해서, CORS(Cross-Origin Resource Sharing)는 웹 브라우저가 다른 도메인의 리소스를 안전하게 사용할 수 있도록 하는 메커니즘입니다. 마치 아파트 경비실이 방문객의 신원을 확인하고 출입을 허가하는 것처럼, 서버가 어떤 도메인의 요청을 허용할지 제어합니다.
실무에서는 프론트엔드가 localhost:3000에서 실행되고, 백엔드가 localhost:8000에서 실행되는 경우가 많습니다. 또한 프로덕션에서는 www.example.com에서 api.example.com으로 요청을 보내는 식으로 도메인이 다릅니다.
이런 경우 CORS 설정 없이는 브라우저가 요청을 차단합니다. 또한 API 키 관리, 속도 제한, SQL 인젝션 방어 등 다양한 보안 조치가 필요합니다.
전통적인 방법에서는 직접 헤더를 추가하거나 복잡한 미들웨어를 작성해야 했다면, FastAPI에서는 CORSMiddleware를 추가하는 것만으로 간단하게 해결할 수 있습니다. CORS와 보안 설정의 핵심 특징은 도메인 기반 접근 제어, 요청 메서드 제한, 인증 토큰 관리입니다.
허용할 오리진 목록을 명시하고, GET, POST 같은 특정 메서드만 허용하며, JWT 토큰으로 인증을 구현할 수 있습니다. 이러한 특징들이 애플리케이션을 외부 공격으로부터 보호하고, 정당한 사용자만 API를 사용할 수 있게 합니다.
코드 예제
# app/main.py
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import os
app = FastAPI()
# CORS 미들웨어 설정 - 어떤 도메인의 요청을 허용할지
app.add_middleware(
CORSMiddleware,
# 허용할 오리진 목록 - 프로덕션에서는 구체적으로 지정
allow_origins=["http://localhost:3000", "https://yourdomain.com"],
# 인증 정보(쿠키 등)를 포함한 요청 허용 여부
allow_credentials=True,
# 허용할 HTTP 메서드 - 필요한 것만 열기
allow_methods=["GET", "POST", "PUT", "DELETE"],
# 허용할 헤더 - Authorization 등
allow_headers=["*"],
)
# Bearer 토큰 인증 스키마
security = HTTPBearer()
# 인증 검증 함수 - 의존성 주입으로 사용
async def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
"""API 키 또는 JWT 토큰을 검증합니다."""
token = credentials.credentials
# 환경 변수에서 가져온 API 키와 비교
if token != os.getenv("API_KEY"):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication token"
)
return token
# 보호된 엔드포인트 - 인증이 필요함
@app.post("/chat", dependencies=[Depends(verify_token)])
async def protected_chat(request: ChatRequest):
"""인증된 사용자만 접근 가능한 채팅 엔드포인트"""
return await process_chat(request)
설명
이것이 하는 일: CORS 설정은 어떤 웹사이트가 API를 호출할 수 있는지 제어하고, 인증 메커니즘은 정당한 사용자만 API를 사용할 수 있도록 보호합니다. 첫 번째로, CORSMiddleware를 앱에 추가하면 FastAPI가 모든 요청을 가로채서 CORS 헤더를 확인합니다.
allow_origins에 지정된 도메인에서 온 요청만 허용되고, 나머지는 거부됩니다. 개발 환경에서는 ["*"]로 모든 도메인을 허용할 수 있지만, 프로덕션에서는 반드시 구체적인 도메인을 지정해야 합니다.
이렇게 하는 이유는 악의적인 웹사이트가 사용자 브라우저를 통해 API를 호출하는 것을 방지하기 위함입니다. 그 다음으로, HTTPBearer를 사용하여 Bearer 토큰 인증을 구현합니다.
클라이언트는 요청 헤더에 "Authorization: Bearer YOUR_TOKEN"을 포함해야 하고, 서버는 이 토큰을 검증합니다. verify_token 함수는 의존성 주입(Dependency Injection)으로 작동하여, 엔드포인트에 dependencies=[Depends(verify_token)]을 추가하면 자동으로 인증을 확인합니다.
토큰이 잘못되었거나 없으면 401 에러가 반환되어 함수가 실행되지 않습니다. 환경 변수를 사용하여 API 키를 관리하는 것은 매우 중요합니다.
os.getenv("API_KEY")로 환경 변수를 읽어오면, 코드에 민감 정보가 노출되지 않습니다. .env 파일에 API_KEY=your_secret_key를 저장하고, python-dotenv 라이브러리로 로드할 수 있습니다.
또한 .gitignore에 .env를 추가하여 Git에 커밋되지 않도록 해야 합니다. 여러분이 이 코드를 사용하면 프론트엔드와 백엔드를 안전하게 연결할 수 있고, 무단 접근을 차단할 수 있습니다.
실제로 많은 해킹 사고가 CORS 설정 오류나 인증 누락으로 발생하므로, 이런 보안 조치는 필수입니다. 또한 속도 제한(rate limiting)을 추가하여 DDoS 공격을 방어하고, SQL 인젝션 방지를 위해 항상 Pydantic 스키마로 입력을 검증해야 합니다.
실전 팁
💡 프로덕션에서는 allow_origins=["*"]를 절대 사용하지 마세요. 구체적인 도메인 목록을 환경 변수로 관리하는 것이 안전합니다.
💡 JWT(JSON Web Token)를 사용하면 더 안전하고 확장 가능한 인증을 구현할 수 있습니다. python-jose 라이브러리를 사용하여 토큰을 생성하고 검증하세요.
💡 slowapi 라이브러리로 속도 제한을 추가하여, 같은 IP에서 짧은 시간에 너무 많은 요청을 보내는 것을 막으세요. 예: @limiter.limit("5/minute")
💡 HTTPS를 반드시 사용하세요. HTTP로 API 키나 토큰을 전송하면 중간에 가로챌 수 있습니다. Let's Encrypt로 무료 SSL 인증서를 발급받을 수 있습니다.
💡 로깅을 추가하여 모든 API 요청을 기록하세요. 이상한 패턴의 요청이 있는지 모니터링하고, 공격을 빠르게 감지할 수 있습니다.
6. Docker 컨테이너화
시작하며
여러분이 개발한 FastAPI 애플리케이션을 서버에 배포하려고 하는데, Python 버전이 다르거나 필요한 라이브러리가 설치되지 않아서 "내 컴퓨터에서는 잘 되는데..."라는 말을 해본 적 있나요? 또는 여러 개의 서버에 일일이 같은 설정을 반복하느라 시간을 낭비한 적이 있으신가요?
이런 문제는 배포 과정에서 가장 흔하게 발생하는 문제입니다. 개발 환경과 프로덕션 환경이 다르면 예상치 못한 에러가 발생하고, 서버가 여러 대라면 설정을 동기화하는 것도 큰 부담입니다.
또한 의존성이 충돌하거나 버전이 맞지 않으면 디버깅이 매우 어려워집니다. 바로 이럴 때 필요한 것이 Docker 컨테이너화입니다.
Docker를 사용하면 애플리케이션과 모든 의존성을 하나의 이미지로 패키징하여, 어떤 환경에서든 동일하게 실행할 수 있습니다.
개요
간단히 말해서, Docker는 애플리케이션을 컨테이너라는 격리된 환경에서 실행하는 기술입니다. 마치 캠핑용 트레일러처럼, 필요한 모든 것을 담아서 어디든지 가져가서 실행할 수 있습니다.
실무에서는 개발, 스테이징, 프로덕션 환경이 모두 다른 경우가 많습니다. 개발자의 맥북, 테스트 서버의 우분투, 프로덕션의 AWS EC2 등 각각 OS와 설정이 다릅니다.
예를 들어, 개발자는 Python 3.11을 쓰는데 서버는 Python 3.9가 설치되어 있으면 호환성 문제가 발생합니다. Docker를 사용하면 이런 환경 차이를 완전히 없앨 수 있습니다.
전통적인 방법에서는 서버에 직접 SSH로 접속하여 Python을 설치하고 패키지를 설치했다면, Docker에서는 Dockerfile에 모든 설정을 명시하고 이미지를 빌드하여 어디서든 실행할 수 있습니다. Docker 컨테이너화의 핵심 특징은 환경 일관성, 격리성, 이식성입니다.
한 번 빌드한 이미지는 어디서든 동일하게 작동하고, 각 컨테이너는 독립적으로 실행되어 서로 영향을 주지 않으며, 이미지를 다운로드받기만 하면 어떤 서버에서든 즉시 실행할 수 있습니다. 이러한 특징들이 배포를 자동화하고, 확장을 쉽게 하며, 개발 속도를 향상시킵니다.
코드 예제
# Dockerfile - Docker 이미지 빌드 설정
# Python 3.11 공식 이미지를 베이스로 사용
FROM python:3.11-slim
# 작업 디렉토리 설정 - 컨테이너 내부 경로
WORKDIR /app
# 의존성 파일 먼저 복사 - 캐싱 최적화
COPY requirements.txt .
# Python 패키지 설치 - pip 업그레이드 후 설치
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir -r requirements.txt
# 애플리케이션 코드 복사
COPY ./app ./app
# 환경 변수 설정 - Python 출력 버퍼링 비활성화
ENV PYTHONUNBUFFERED=1
# 컨테이너가 리스닝할 포트 명시
EXPOSE 8000
# 컨테이너 시작 시 실행할 명령
# uvicorn으로 FastAPI 앱 실행 - 모든 IP에서 접근 가능하게
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
설명
이것이 하는 일: Dockerfile은 애플리케이션을 실행하는 데 필요한 모든 것을 명시하는 레시피입니다. 이를 바탕으로 Docker 이미지를 만들면, 어떤 환경에서든 동일하게 실행할 수 있는 컨테이너를 생성할 수 있습니다.
첫 번째로, FROM 명령어로 베이스 이미지를 지정합니다. python:3.11-slim은 Python 3.11이 설치된 경량 리눅스 이미지입니다.
slim 버전을 사용하면 이미지 크기가 작아져 다운로드와 배포가 빨라집니다. 이렇게 하는 이유는 불필요한 패키지를 제거하여 보안 취약점을 줄이고 성능을 향상시키기 위함입니다.
그 다음으로, requirements.txt를 먼저 복사하고 패키지를 설치합니다. 그 후에 애플리케이션 코드를 복사하는 순서가 중요한데, 이는 Docker의 레이어 캐싱 때문입니다.
requirements.txt가 바뀌지 않으면 pip install 단계를 캐시에서 가져와서 빌드 시간이 크게 단축됩니다. 애플리케이션 코드는 자주 바뀌지만 의존성은 자주 바뀌지 않으므로, 이런 순서로 배치하는 것이 효율적입니다.
EXPOSE 8000은 컨테이너가 8000번 포트를 사용한다는 것을 문서화하는 것입니다. 실제로 포트를 여는 것은 docker run 명령에서 -p 옵션으로 합니다.
CMD는 컨테이너가 시작될 때 실행할 명령을 지정합니다. uvicorn으로 FastAPI 앱을 실행하는데, --host 0.0.0.0으로 설정하면 외부에서 접근 가능해집니다.
localhost로 하면 컨테이너 내부에서만 접근 가능합니다. 여러분이 이 Dockerfile을 사용하면 docker build -t fastapi-app .으로 이미지를 빌드하고, docker run -p 8000:8000 fastapi-app으로 실행할 수 있습니다.
이미지를 Docker Hub이나 AWS ECR에 푸시하면 다른 서버에서도 즉시 실행할 수 있습니다. 또한 docker-compose를 사용하여 데이터베이스, Redis 등 여러 컨테이너를 함께 실행하고 관리할 수 있습니다.
Kubernetes 같은 오케스트레이션 도구와 함께 사용하면 자동 스케일링, 로드 밸런싱, 무중단 배포 등 고급 기능도 구현할 수 있습니다.
실전 팁
💡 .dockerignore 파일을 만들어 불필요한 파일(pycache, .git, .env 등)이 이미지에 포함되지 않도록 하세요. 이미지 크기를 줄이고 빌드 속도를 향상시킵니다.
💡 멀티 스테이지 빌드를 사용하여 최종 이미지 크기를 더욱 줄일 수 있습니다. 빌드 도구는 첫 번째 스테이지에서만 사용하고, 실행에 필요한 것만 최종 이미지에 포함시킵니다.
💡 docker-compose.yml로 개발 환경을 구성하면 데이터베이스, Redis 등을 한 번에 실행할 수 있습니다. 팀원들도 동일한 환경을 쉽게 구축할 수 있습니다.
💡 헬스체크를 추가하여 컨테이너가 정상 작동하는지 모니터링하세요. HEALTHCHECK 명령이나 /health 엔드포인트를 구현하면 됩니다.
💡 환경 변수는 docker run -e 옵션이나 .env 파일로 전달하세요. 이미지에 민감 정보를 포함시키면 안 됩니다. AWS Secrets Manager나 HashiCorp Vault 같은 도구로 비밀 정보를 관리하는 것이 더 안전합니다.