선생님 몰래 춤추기 프로젝트에서 백엔드 역할을 맡아 점수 관리 시스템을 구축하게 되었습니다! 이 프로젝트에서는 사용자들이 게임 점수를 제출하고, 랭킹을 조회할 수 있는 기능을 만들어야 했습니다.
구현해야할 기능
저는 FastAPI와 MongoDB, Docker를 이용하여 API를 제작하기로 결정하였습니다.
FastAPI 선택 이유
FastAPI는 Python으로 빠르게 API를 구축할 수 있는 강력한 도구입니다. 간결하고 직관적인 코드 작성을 통해 빠르게 개발할 수 있으며, 자동 문서화 기능으로 API 문서를 손쉽게 관리할 수 있습니다. 또한 비동기 처리를 기본으로 지원하여 높은 성능을 제공하며, 많은 사용자 요청을 효율적으로 처리할 수 있습니다.
MongoDB 선택 이유
MongoDB는 NoSQL 데이터베이스로, JSON과 유사한 BSON 형식으로 데이터를 저장하며 유연한 데이터 모델링을 지원합니다. 저희 프로젝트에서는 사용자 점수 데이터를 동적으로 관리해야 했기 때문에 MongoDB의 이러한 특성이 매우 유용했습니다. 또한 빠른 읽기 및 쓰기 성능과 수평 확장성을 제공하여 대용량 데이터를 효율적으로 처리할 수 있었습니다.
Docker의 활용
프로젝트를 Docker 컨테이너로 패키징하여 배포하였습니다. Docker는 개발 환경과 운영 환경의 일관성을 유지하고, 배포 과정을 단순화하여 개발자와 운영팀 간의 협업을 원활하게 하였습니다. 또한 다양한 환경에서 동일한 설정으로 애플리케이션을 실행할 수 있어 개발 생산성을 크게 향상시켰습니다.
먼저, Docker를 사용하여 MongoDB를 설정하고 Python을 통해 연결을 테스트합니다.
import pymongo
# MongoDB 연결 정보
mongo_host = 'localhost' # 또는 컨테이너의 IP 주소
mongo_port = 27017
mongo_user = 'admin'
mongo_password = '2024md'
# MongoDB에 연결
try:
client = pymongo.MongoClient(f'mongodb://{mongo_user}:{mongo_password}@{mongo_host}:{mongo_port}')
db = client.admin # 연결 테스트를 위해 admin 데이터베이스 사용
print("Connected to MongoDB successfully!")
except pymongo.errors.ConnectionFailure as e:
print(f"Failed to connect to MongoDB: {e}")
finally:
if 'client' in locals():
client.close()
pymongo.MongoClient
를 사용하여 MongoDB에 연결을 시도합니다.admin
데이터베이스에 연결하여 연결 테스트를 수행합니다.이제 Python과 FastAPI를 설치하고, 점수 제출 및 랭킹 조회 API를 개발합니다.
pip install fastapi uvicorn pymongo
from fastapi import FastAPI, HTTPException, Path
from fastapi.responses import JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from pymongo import MongoClient
from enum import Enum
import os
from urllib.parse import quote, unquote
FastAPI
, HTTPException
, Path
: FastAPI 프레임워크와 예외 처리, 경로 매개변수에 사용됩니다.JSONResponse
: JSON 응답을 생성합니다.CORSMiddleware
: CORS (Cross-Origin Resource Sharing) 설정을 위해 사용됩니다.MongoClient
: MongoDB와 연결하기 위해 사용됩니다.Enum
: Python 열거형을 정의하기 위해 사용됩니다.os
: 환경 변수에 접근하기 위해 사용됩니다.quote
, unquote
: URL 인코딩 및 디코딩을 위해 사용됩니다.app = FastAPI()
origins = [
"<http://localhost:3000>", # React development server
"<http://127.0.0.1:3000>" # Alternative local address
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app = FastAPI()
: FastAPI 애플리케이션 인스턴스를 생성합니다.origins
: 허용할 출처 목록을 정의합니다.app.add_middleware
: CORS 미들웨어를 추가하여 정의한 출처에서 오는 요청을 허용합니다.mongo_uri = os.getenv("MONGO_URI")
if not mongo_uri:
raise ValueError("MONGO_URI 환경 변수가 설정되지 않았습니다.")
client = MongoClient(mongo_uri)
db = client["admin"]
user_collection = db["user"]
mongo_uri
: 환경 변수에서 MongoDB URI를 가져옵니다.client
, db
, user_collection
: MongoDB 클라이언트, 데이터베이스, 그리고 컬렉션을 설정합니다.class Teacher(str, Enum):
KYH = "KYH"
YBS = "YBS"
KJS = "KJS"
KYC = "KYC"
KHS = "KHS"
SMS = "SMS"
Teacher
: 선생님 이름을 열거형으로 정의합니다. 이는 강력한 타입 체크를 위해 사용됩니다.@app.get("/")
async def index():
return "home"
/
엔드포인트: 간단한 홈 페이지로 "home" 문자열을 반환합니다.@app.post("/score/{user_id}/{score}/{teacher}", tags=["score"])
async def submit_score(user_id: str = Path(..., title="사용자 ID", description="사용자의 고유한 ID"),
score: int = Path(..., title="점수", description="점수"),
teacher: Teacher = Path(..., title="선생님", description="점수를 부여한 선생님")):
"""
사용자의 점수를 제출합니다.
"""
user_id_encoded = quote(user_id)
teacher_encoded = quote(teacher.value)
existing_user = user_collection.find_one({"user_id": user_id_encoded})
if not existing_user:
user_collection.insert_one({"user_id": user_id_encoded, "score": score, "teacher": teacher_encoded})
else:
if score > existing_user["score"]:
user_collection.update_one({"user_id": user_id_encoded}, {"$set": {"score": score, "teacher": teacher_encoded}})
return JSONResponse(content={"message": "Score submitted successfully"})
submit_score
: 사용자 점수를 제출하는 엔드포인트입니다.user_id
, score
, teacher
: 경로 매개변수로 받아옵니다.quote
함수로 user_id
와 teacher
값을 URL 인코딩합니다.@app.get("/ranking", tags=["ranking"])
async def get_ranking():
ranking = list(user_collection.find(projection={"_id": 0}).sort("score", -1))
for user in ranking:
user["user_id"] = unquote(user["user_id"])
if "teacher" in user:
user["teacher"] = unquote(user["teacher"])
return JSONResponse(content={"ranking": ranking})
get_ranking
: 사용자 랭킹을 조회하는 엔드포인트입니다.이렇게 하여 FastAPI와 MongoDB를 사용하여 사용자 점수를 관리하고 랭킹을 조회하는 간단한 API를 구현하였습니다!
전체코드는 아래와 같습니다 👇🏻
from fastapi import FastAPI, HTTPException, Path
from fastapi.responses import JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from pymongo import MongoClient
from enum import Enum
import os
from urllib.parse import quote, unquote
app = FastAPI()
# CORS 설정
origins = [
"<http://localhost:3000>", # React development server
"<http://127.0.0.1:3000>" # Alternative local address
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# MongoDB 연결
mongo_uri = os.getenv("MONGO_URI")
if not mongo_uri:
raise ValueError("MONGO_URI 환경 변수가 설정되지 않았습니다.")
client = MongoClient(mongo_uri)
db = client["admin"]
user_collection = db["user"]
# 선생님 열거형 정의
class Teacher(str, Enum):
KYH = "KYH"
YBS = "YBS"
KJS = "KJS"
KYC = "KYC"
KHS = "KHS"
SMS = "SMS"
@app.get("/")
async def index():
return "home"
@app.post("/score/{user_id}/{score}/{teacher}", tags=["score"])
async def submit_score(user_id: str = Path(..., title="사용자 ID", description="사용자의 고유한 ID"),
score: int = Path(..., title="점수", description="점수"),
teacher: Teacher = Path(..., title="선생님", description="점수를 부여한 선생님")):
"""
사용자의 점수를 제출합니다.
- **user_id**: 사용자의 고유한 ID입니다.
- **score**: 제출할 점수입니다.
- **teacher**: 점수를 부여한 선생님입니다.
"""
# 사용자 아이디와 선생님 이름을 URL 인코딩
user_id_encoded = quote(user_id)
teacher_encoded = quote(teacher.value)
# 사용자 아이디로 검색 후 결과가 없으면 새로운 사용자 추가
existing_user = user_collection.find_one({"user_id": user_id_encoded})
if not existing_user:
user_collection.insert_one({"user_id": user_id_encoded, "score": score, "teacher": teacher_encoded})
else:
# 이미 존재하는 사용자의 경우 기존 점수와 비교하여 업데이트
if score > existing_user["score"]:
user_collection.update_one({"user_id": user_id_encoded}, {"$set": {"score": score, "teacher": teacher_encoded}})
return JSONResponse(content={"message": "Score submitted successfully"})
@app.get("/ranking", tags=["ranking"])
async def get_ranking():
"""
사용자의 랭킹을 가져옵니다.
"""
ranking = list(user_collection.find(projection={"_id": 0}).sort("score", -1))
# 사용자 ID와 선생님 이름을 URL 디코딩하여 원래의 값으로 변환
for user in ranking:
user["user_id"] = unquote(user["user_id"])
if "teacher" in user:
user["teacher"] = unquote(user["teacher"])
return JSONResponse(content={"ranking": ranking})
이제 Docker 및 Docker Compose를 사용하여 MongoDB와 FastAPI 서버를 함께 실행할 수 있도록 설정합니다.
# Base image
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9
# Set working directory
WORKDIR /app
# Copy requirements.txt to the container
COPY ./requirements.txt .
# Install dependencies
RUN pip install -r requirements.txt
# Copy the FastAPI app code into the container
COPY . .
# Expose FastAPI port
EXPOSE 8000
# Command to run the application
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
fastapi==0.68.0
uvicorn==0.15.0
pymongo==3.12.0
version: "3"
services:
fastapi-mongodb:
build:
context: .
dockerfile: Dockerfile
ports:
- "3001:8000"
environment:
- MONGO_URI=mongodb://mongo:27017/
networks:
- 2024md
depends_on:
- mongo
mongo:
image: mongo
restart: always
ports:
- "3002:27017"
networks:
- 2024md
networks:
2024md:
설정 분석
Dockerfile
은 FastAPI 애플리케이션을 실행하기 위한 이미지를 빌드합니다.requirements.txt
는 필요한 Python 패키지를 명시합니다.docker-compose.yml
은 FastAPI와 MongoDB 서비스를 정의하고, 두 서비스를 동일한 네트워크에서 실행되도록 설정합니다.Docker와 Docker Compose를 사용하여 개발 환경을 설정하고, 두 서비스가 원활히 통신할 수 있도록 구성하였습니다!
이를 통해 프론트엔드 개발자가 로컬에서 서버를 쉽게 실행할 수 있도록 했습니다.