[체크리스트] 파이썬
Spring의 MVC 패턴으로만 쭉 백엔드 개발 연습을 했는데, Python의 장고와 FastApi를 경험해보는것도 좋을 것 같아서 정리하고자 함. 계속 내용 수정해야함. 일단은 대략적인 흐름만 작성해두고 나중에 경험이 쌓이면 틀린내용 수정해 가겠음.
일단 내용은 Spring의 체크리스트의 흐름으로 작성(Gpt 도움 받으면서)
-
Django의 핵심 특징
사용 예시: 콘텐츠 관리 시스템(CMS), 전자상거래 플랫폼, 데이터 기반 앱
-
“batteries-included” 철학을 따르는 풀스택 프레임워크 (즉, 웹 개발 필요 기능 대부분 제공)
-
MVT(Model-Template-View) 아키텍처 패턴 사용
-
Django는 맨 처음 URL을 보고 알맞은 메인 로직을 처리하는 View을(를) 호출
-
View에서는 필요하다면 Model을 통해 데이터베이스와 소통하고
-
처리한 데이터를 화면을 담당하는 Template과 함께 렌더해서 최종 화면을 만든 후
스프링의 타임리프가 생각나는 뷰 템플릿 부분이다..
-
View를 통해 클라이언트 에게 응답으로 돌려줌
-
-
강력한 ORM 시스템과 자동 관리자 인터페이스 제공
-
보안 기능이 기본 내장되어 있음 (XSS, CSRF, SQL 인젝션 방지)
-
-
FastAPI의 핵심 특징
사용 예시: 실시간 앱, 머신러닝 API 서비스, 서버리스 함수
-
Python 3.6+ 기반의 현대적이고 고성능 API 프레임워크
-
비동기 프로그래밍 지원으로 높은 동시성 처리
-
Python 타입 힌트를 활용한 자동 API 검증 및 문서화
-
Swagger UI와 ReDoc을 통한 자동 API 문서 생성
-
-
MVT 보충설명: 참고 링크
MVC구조 MVT구조 Model 데이터 저장, 보관 => Model View 사용자에게 보여지는 부분 담당 => Template Controller 웹 사이트의 로직 담당 => View MVT 아키텍처는 MVC 아키텍처 구조를 기반으로 만들어져서, 둘 간의 공통점이 많다.
차이점은 MVC구조에서 Controller가 했던 역할을 일부 분리해서, Django프레임워크가 직접 처리한다.Django 개발자는 Model, View, Template에 집중하고, 나머지 모든 부분은 Django 프레임 워크에게 맡겨서 빠르게 개발할 수 있다.
Django 프레임워크
기본 설치 법
# Django 설치 (콘다 base에 설치했음)
pip install django
# 프로젝트 생성
django-admin startproject projectname
# 앱 생성
cd projectname
python manage.py startapp app1
아래 흐름으로 정리해보겠음.
프로젝트 권장구조 -> MVT 개발 흐름(Model, Template, View…) -> 테스트코드(?) -> application.yml같은 환경설정 -> 리팩토링(캐시, 메시지국제화, 외부설정, 검증, 예외처리 정도?) -> 배포(프로필설정 있는강?) -> 참고지식(로그인 인증방식 Spring때랑 같은지보자, 파일업로드 다운로드도 같은지 보자 딱 이정도만?)
권장 구조
기본 프로젝트 구조
projectname/
├── apps/ -> 모든 장고 앱을 한 곳에 모아 관리
│ ├── app1/ -> 각 앱은 독립적인 기능 단위로 구성
│ ├── app2/
├── projectname/
│ ├── settings/ -> 환경별로 설정 파일 분리
│ │ ├── base.py -> 공통 설정
│ │ ├── development.py -> 개발 환경 설정
│ │ └── production.py -> 운영(배포) 환경 설정
│ ├── urls.py -> 프로젝트 레벨
│ └── wsgi.py -> WS(아파치 등)와 장고 앱 통신 담당
├── static/ -> 전역 static 폴더는 공통 자원
├── media/
├── templates/ -> 기본 템플릿
└── manage.py
앱 구조(app1, app2…)
app1/
├── migrations/
├── static/ -> 앱별 static 폴더 사용
├── templates/ -> 앱별 템플릿
├── admin.py
├── apps.py
├── models.py
├── views.py
├── urls.py -> 앱 레벨
└── tests/
MVS? (Model, View, Serializer) - API (JSON)
api위해 rest_framework(viewset,router 쓰려고)설치와 settings.py 등록!
pip install djangorestframework
# settings.py에 등록:
INSTALLED_APPS = [
...
"rest_framework", # 설치한 rest_framework 추가
"tarot", # 새로 만든 앱 추가
]
Model 부분 (엔티티 느낌)
-
class TarotCard(models.Model):
란 Django의 Model 클래스를 상속받는다는 의미 -
models.Model이 제공하는 기능:
# 자동으로 사용 가능한 메서드들 card = TarotCard.objects.create(name="The Fool") # 생성 card.save() # 저장 card.delete() # 삭제 TarotCard.objects.all() # 전체 조회
# tarot/models.py
from django.db import models
class TarotCard(models.Model):
name = models.CharField(max_length=100)
number = models.IntegerField(null=True)
arcana_type = models.CharField(max_length=20, choices=[
('MAJOR', 'Major Arcana'),
('MINOR', 'Minor Arcana')
])
keywords = models.CharField(max_length=255)
meaning_upright = models.TextField()
meaning_reversed = models.TextField()
description = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.name
Serializer부분. JSON으로 반환 위함 (API) - 직렬화, 역직렬화
# tarot/serializers.py
from rest_framework import serializers
from .models import TarotCard
class TarotCardSerializer(serializers.ModelSerializer):
class Meta:
model = TarotCard
fields = '__all__' # 모든 필드 사용한다고.
View 부분
- ModelViewSet은 자동으로 CRUD 생성!! 그래서 rest_framework 이거 설치한 것
# tarot/views.py
from rest_framework import viewsets
from .models import TarotCard
from .serializers import TarotCardSerializer
class TarotCardViewSet(viewsets.ModelViewSet):
queryset = TarotCard.objects.all() # 전체 조회
serializer_class = TarotCardSerializer
def get_queryset(self):
queryset = TarotCard.objects.all()
arcana_type = self.request.query_params.get('arcana_type', None)
if arcana_type:
queryset = queryset.filter(arcana_type=arcana_type)
return queryset
URL 설정 (컨트롤러 느낌-라우터..)
- Router는 ModelViewSet이 생성한 CRUD를 자동으로 URL 매핑!! 그래서 rest_framework 설치!
# tarot/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import TarotCardViewSet
router = DefaultRouter()
router.register(r'cards', TarotCardViewSet)
urlpatterns = [
path('api/', include(router.urls)),
]
# projectname/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('tarot.urls')), # tarot 앱의 URLs 포함
]
MVT (Model, View, Template) - 웹
위에서 만든 api와 파일명 구분하기 위해 디렉토리 살짝 수정
자세한 코드는 CODE 폴더에 projectname 폴더보면 됨ㅇㅇ
#앱 구조(app1, app2...) 부분임.
projectname/
├── tarot/
│ ├── templates/
│ │ └── tarot/
│ │ ├── base.html
│ │ ├── card_list.html
│ │ ├── card_detail.html
│ │ └── card_form.html
│ ├── api/
│ │ ├── __init__.py
│ │ ├── views.py # API 뷰
│ │ ├── urls.py # API URL 패턴
│ │ └── serializers.py
│ ├── web/
│ │ ├── __init__.py
│ │ ├── views.py # 웹 뷰
│ │ └── urls.py # 웹 URL 패턴
│ ├── models.py
│ └── forms.py
Model 위에꺼 그대로 사용ㅇㅇ
-
class TarotCard(models.Model):
란 Django의 Model 클래스를 상속받는다는 의미 -
models.Model이 제공하는 기능:
# 자동으로 사용 가능한 메서드들 card = TarotCard.objects.create(name="The Fool") # 생성 card.save() # 저장 card.delete() # 삭제 TarotCard.objects.all() # 전체 조회
# tarot/models.py
from django.db import models
class TarotCard(models.Model):
name = models.CharField(max_length=100)
number = models.IntegerField(null=True)
arcana_type = models.CharField(max_length=20, choices=[
('MAJOR', 'Major Arcana'),
('MINOR', 'Minor Arcana')
])
keywords = models.CharField(max_length=255)
meaning_upright = models.TextField()
meaning_reversed = models.TextField()
description = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.name
Template이 필요한 경우임. (웹 페이지)
-
다른 템플릿들이 base.html을 상속받아 content 블록을 채우는 방식
-
템플릿 태그 설명:
{% extends %} - 템플릿 상속 {% block %} - 자식 템플릿이 채울 수 있는 영역 정의 {% csrf_token %} - CSRF 보안 토큰 {{ form.as_p }} - 폼 필드를 p 태그로 감싸서 렌더링
# tarot/web/views.py
from django.shortcuts import render, get_object_or_404, redirect
from ..models import TarotCard
from .forms import TarotCardForm
def card_list(request):
cards = TarotCard.objects.all()
return render(request, 'tarot/card_list.html', {'cards': cards})
def card_detail(request, pk):
card = get_object_or_404(TarotCard, pk=pk)
return render(request, 'tarot/card_detail.html', {'card': card})
def card_create(request):
if request.method == "POST":
form = TarotCardForm(request.POST)
if form.is_valid():
card = form.save()
return redirect('tarot:card_detail', pk=card.pk)
else:
form = TarotCardForm()
return render(request, 'tarot/card_form.html', {'form': form})
<!-- templates/tarot/base.html -->
<!DOCTYPE html>
<html>
<head>
<title>타로 카드 앱</title>
<style>
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
padding: 20px;
}
.card {
border: 1px solid #ddd;
padding: 15px;
border-radius: 8px;
}
.btn {
display: inline-block;
padding: 10px 20px;
background-color: #007bff;
color: white;
text-decoration: none;
border-radius: 5px;
}
</style>
</head>
<body>
<nav>
<!-- views.py의 메소드 -->
<a href="{% url 'tarot:card_list' %}">홈</a>
</nav>
<main>
<!-- nav태그의 홈은 모든 페이지에서 유지, 이 main태그 부분만 변동 -->
{% block content %}
{% endblock %}
</main>
</body>
</html>
<!-- templates/tarot/card_list.html -->
{% extends 'tarot/base.html' %}
{% block content %}
<h1>타로 카드 목록</h1>
<div class="card-grid">
{% for card in cards %}
<div class="card">
<h2>{{ card.name }}</h2>
<p>{{ card.description|truncatewords:30 }}</p>
<!-- views.py의 메소드 -->
<a href="{% url 'tarot:card_detail' pk=card.pk %}">자세히 보기</a>
</div>
{% endfor %}
</div>
<!-- views.py의 메소드 -->
<a href="{% url 'tarot:card_create' %}" class="btn">새 카드 추가</a>
{% endblock %}
<!-- templates/tarot/card_detail.html -->
{% extends 'tarot/base.html' %}
{% block content %}
<div class="card-detail">
<h1>{{ card.name }}</h1>
<div class="card-info">
<p><strong>번호:</strong> {{ card.number }}</p>
<p><strong>종류:</strong> {{ card.get_arcana_type_display }}</p>
<p><strong>키워드:</strong> {{ card.keywords }}</p>
</div>
<div class="card-meanings">
<h2>정방향 의미</h2>
<p>{{ card.meaning_upright }}</p>
<h2>역방향 의미</h2>
<p>{{ card.meaning_reversed }}</p>
</div>
<div class="card-description">
<h2>설명</h2>
<p>{{ card.description }}</p>
</div>
</div>
{% endblock %}
<!-- templates/tarot/card_form.html -->
{% extends 'tarot/base.html' %}
{% block content %}
<h1>새 타로 카드 추가</h1>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn">저장</button>
</form>
{% endblock %}
form.py 가 필요 (위에 시리얼라이즈 처럼 DTO)
# tarot/web/form.py
from django import forms
from ..models import TarotCard
class TarotCardForm(forms.ModelForm):
class Meta:
model = TarotCard
fields = ['name', 'number', 'arcana_type', 'keywords',
'meaning_upright', 'meaning_reversed', 'description']
widgets = {
'description': forms.Textarea(attrs={'rows': 4}),
'meaning_upright': forms.Textarea(attrs={'rows': 3}),
'meaning_reversed': forms.Textarea(attrs={'rows': 3}),
}
labels = {
'name': '카드 이름',
'number': '번호',
'arcana_type': '아르카나 종류',
'keywords': '키워드',
'meaning_upright': '정방향 의미',
'meaning_reversed': '역방향 의미',
'description': '설명',
}
URL 설정 (컨트롤러 느낌-라우터)
# tarot/web/urls.py
from django.urls import path
from . import views
app_name = 'tarot'
urlpatterns = [
path('', views.card_list, name='card_list'),
path('card/<int:pk>/', views.card_detail, name='card_detail'),
path('card/new/', views.card_create, name='card_create'),
]
# projectname/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('tarot.api.urls')), # API 경로 (tarot/api/urls.py)
path('', include('tarot.web.urls')), # 웹 경로 (이걸로 변경)
]
# tarot/api/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import TarotCardViewSet
router = DefaultRouter()
router.register(r'cards', TarotCardViewSet)
urlpatterns = [
path('', include(router.urls)), # http://localhost:8000/api/ 로 접근 위해
# 만약 이부분 수정 안하면 http://localhost:8000/api/api/ 로 접근해야 함.
]
테스트
SQLite 메모리 DB 설정 가능
=> 근데 비추! 서버 시작마다 마이그레이션이 초기화! 그래서 서버시작시 자동 마이그레이션도 추가해야함.
따라서 SQLite DB 사용! 개발에서!
# settings.py
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
"NAME": BASE_DIR / "db.sqlite3",
# 'NAME': ':memory:',
}
}
# db 마이그레이션
python manage.py makemigrations tarot
python manage.py migrate
# db 테이블 재생성도 해주는..
python manage.py migrate --run-syncdb
# 서버(앱) 실행
python manage.py runserver
테스트코드.. 스프링처럼.. 작성하는거 있나… 체크…..ㄱ…ㄱ…ㄹ…
from django.test import TestCase 이런게 또 보이더라구 ㅇㅅㅇ
Fastapi 프레임워크
기본 설치법
# Fastapi 설치 (콘다 base에 설치했음)
pip install fastapi
pip install uvicorn
# 기본 앱 구조 생성은 수동
아래 흐름으로 정리해보겠음.
프로젝트 권장구조 -> 얜 어떤 개발 흐름이냐 ㅠㅠ(프로젝트 구조대로… GPT부탁ㄱ)+타로ai 바로 만들면서ㄱㄱㄹ -> 테스트코드(?) -> application.yml같은 환경설정 -> 리팩토링(캐시, 메시지국제화, 외부설정, 검증, 예외처리 정도?) -> 배포(프로필설정 있는강?) -> 참고지식(로그인 인증방식 Spring때랑 같은지보자, 파일업로드 다운로드도 같은지 보자 딱 이정도만?)
권장 구조
기본 프로젝트 구조
fastapi-project/
├── main.py -> 앱 진입 점
├── requirements.txt -> 프로젝트에서 사용하는 패키지 목록
└── app/ -> 앱 코드
├── __init__.py
├── models.py
├── schemas.py
└── routers/
└── __init__.py
앱 구조
app/
├── main.py -> 애플리케이션의 진입점으로 FastAPI 인스턴스 생성
├── routers/ -> 리소스별로 구분된 라우트 정의 파일들 포함
│ ├── __init__.py
│ ├── users.py
│ └── items.py
├── models/ -> 데이터베이스 모델 정의
│ ├── __init__.py
│ └── user.py
├── schemas/ -> Pydantic 모델을 사용한 데이터 검증 및 직렬화 스키마 (DTO)
│ ├── __init__.py
│ └── user.py
└── services/ -> 비즈니스 로직 처리 계층
├── __init__.py
└── user_service.py
장고와 주요 차이점
- Django와 달리 FastAPI는 프로젝트/앱 생성 명령어가 없음
- 프로젝트 구조를 수동으로 생성해야 함
- uvicorn이라는 별도의 서버가 필요함
- Docs 기능을 지원하여 등록된 API를 문서화된 UI로 확인이 가능하다.
- 접속 방법: URL/docs 를 추가하면 접근 가능
기본 개발 흐름
main.py -> 시작 점
from fastapi import FastAPI
from app.routers import tarot
app = FastAPI(title="Tarot AI Reading API")
# 라우터 등록
app.include_router(tarot.router, prefix="/api/v1")
app/models/database.py -> db 설정
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
SQLALCHEMY_DATABASE_URL = "sqlite:///./tarot.db"
engine = create_engine(
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
app/models/tarot.py -> 엔티티 느낌
from sqlalchemy import Column, Integer, String, DateTime, Text
from sqlalchemy.sql import func
from .database import Base
class ReadingHistory(Base):
__tablename__ = "reading_history"
id = Column(Integer, primary_key=True, index=True)
ip_address = Column(String(50))
situation = Column(Text)
question = Column(Text, nullable=True)
reading = Column(Text)
created_at = Column(DateTime(timezone=True), server_default=func.now())
app/schemas/tarot.py -> DTO 사용과 유사
from pydantic import BaseModel
from typing import Optional
class TarotRequest(BaseModel):
situation: str
question: Optional[str] = None
class TarotResponse(BaseModel):
reading: str
app/services/tarot_service.py -> 레포지토리+서비스 느낌
from sqlalchemy.orm import Session
from ..models.tarot import ReadingHistory
class TarotService:
def __init__(self, db: Session):
self.db = db
async def record_reading(self, ip_address: str, situation: str, question: str, reading: str):
reading_record = ReadingHistory(
ip_address=ip_address,
situation=situation,
question=question,
reading=reading
)
self.db.add(reading_record)
self.db.commit()
app/services/perplexity_service.py
- 비지니스 로직으로써 perplexity api 연동
- https://docs.perplexity.ai/guides/getting-started 보고 적절한 모델, 프롬프트 세팅
import os
import httpx
from dotenv import load_dotenv
load_dotenv()
class PerplexityService:
def __init__(self):
self.api_key = os.getenv("PERPLEXITY_API_KEY")
self.base_url = "https://api.perplexity.ai"
async def generate_reading(self, situation: str, question: str = None) -> str:
prompt = self._create_prompt(situation, question)
async with httpx.AsyncClient() as client:
response = await client.post(
f"{self.base_url}/chat/completions",
headers={"Authorization": f"Bearer {self.api_key}"},
json={
"model": "sonar-medium-chat",
"messages": [
{"role": "system", "content": "You are an experienced tarot reader."},
{"role": "user", "content": prompt}
]
}
)
if response.status_code == 200:
return response.json()["choices"][0]["message"]["content"]
else:
raise Exception("Failed to generate reading")
def _create_prompt(self, situation: str, question: str = None) -> str:
base_prompt = f"Based on this situation: {situation}"
if question:
base_prompt += f"\nSpecific question: {question}"
return base_prompt + "\nPlease provide a detailed tarot reading with insights and guidance."
app/routers/tarot.py -> 라우터 설정 (애노테이션 사용하는게 Spring과 유사하네)
=> 컨트롤러 느낌
from fastapi import APIRouter, Depends, Request
from slowapi import Limiter
from slowapi.util import get_remote_address
from sqlalchemy.orm import Session
from ..models.database import get_db
from ..schemas.tarot import TarotRequest, TarotResponse
from ..services.perplexity_service import PerplexityService
from ..services.tarot_service import TarotService
router = APIRouter()
limiter = Limiter(key_func=get_remote_address)
perplexity_service = PerplexityService()
@router.post("/reading", response_model=TarotResponse)
@limiter.limit("5/hour") # IP당 시간당 5회로 제한
async def get_tarot_reading(
request: Request,
tarot_request: TarotRequest,
db: Session = Depends(get_db)
):
reading = await perplexity_service.generate_reading(
tarot_request.situation,
tarot_request.question
)
# 리딩 기록 저장
tarot_service = TarotService(db)
await tarot_service.record_reading(
request.client.host,
tarot_request.situation,
tarot_request.question,
reading
)
return TarotResponse(reading=reading)
환경 변수 설정 (.env):
PERPLEXITY_API_KEY=your_api_key_here
테스트
장고처럼 FastAPI도 DB 마이그레이션 필요 -> SQLAlchemy + Alembic
pip install alembic sqlalchemy
# alembic 초기화
alembic init alembic
# alembic.ini 설정 -> 직접 해당파일에 들어가서 수정하면 됨ㅇㅇ.
sqlalchemy.url = sqlite:///./tarot.db
# 마이그레이션 파일 생성 단, alembic/env.py 파일에 target_metadata 설정 필수!
alembic revision --autogenerate -m "Initial migration"
# 마이그레이션 적용
alembic upgrade head
# 마이그레이션 롤백
alembic downgrade -1
#alembic/env.py파일에 target_metadata 설정법은? 그래야 Alembic이 메타데이터 인식하고 자동으로 마이그레이션 파일 생성
from app.models.database import Base
target_metadata = Base.metadata
#main.py 에 테이블 생성 코드도 있어야 함.
from app.models.database import Base, engine
Base.metadata.create_all(bind=engine) # 애플리케이션 시작 시 테이블 생성
# 서버(앱) 실행
uvicorn main:app --reload --host 0.0.0.0 --port 8000
curl localhost:8000 # index 페이지 접근
{"Python":"Framework"}
- uvicorn : python 서버 실행을 위해 필요한 기본 명령어
- main : 실행할 초기 python 파일 이름
- app : FastApi() 모듈을 항당한 객체명
- reload : 소스 코드가 변경되었을때 자동 재시작 옵션
- host : 모든 접근이 가능하게 하기 위해 0.0.0.0
- port : 웹서버 포트 할당
스프링 테스트코드처럼.. 있나 쳌쳌
댓글남기기