본문으로 건너뛰기
Advertisement

Django REST Framework

**DRF(Django REST Framework)**는 Django 위에서 강력한 REST API를 구축하는 라이브러리입니다. Serializer, ViewSet, Router의 조합으로 빠르게 API를 만들 수 있습니다.


설치와 설정

pip install djangorestframework
# settings.py
INSTALLED_APPS = [
...
"rest_framework",
]

REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework_simplejwt.authentication.JWTAuthentication",
],
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
],
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 20,
"DEFAULT_RENDERER_CLASSES": [
"rest_framework.renderers.JSONRenderer",
],
}

Serializer — 직렬화/역직렬화

# products/serializers.py
from rest_framework import serializers
from .models import Product, Category, Review


class CategorySerializer(serializers.ModelSerializer):
product_count = serializers.SerializerMethodField()

class Meta:
model = Category
fields = ["id", "name", "slug", "product_count"]
read_only_fields = ["id"]

def get_product_count(self, obj) -> int:
return obj.products.count()


class ReviewSerializer(serializers.ModelSerializer):
author_name = serializers.CharField(source="author.get_full_name", read_only=True)

class Meta:
model = Review
fields = ["id", "author_name", "rating", "comment", "created_at"]
read_only_fields = ["id", "created_at"]


class ProductListSerializer(serializers.ModelSerializer):
"""목록용 — 간결한 정보"""
category_name = serializers.CharField(source="category.name", read_only=True)

class Meta:
model = Product
fields = ["id", "name", "price", "stock", "category_name", "is_active"]


class ProductDetailSerializer(serializers.ModelSerializer):
"""상세용 — 전체 정보 + 리뷰"""
category = CategorySerializer(read_only=True)
category_id = serializers.PrimaryKeyRelatedField(
queryset=Category.objects.all(), source="category", write_only=True
)
reviews = ReviewSerializer(many=True, read_only=True)
avg_rating = serializers.SerializerMethodField()

class Meta:
model = Product
fields = [
"id", "name", "description", "price", "stock",
"category", "category_id", "reviews", "avg_rating",
"is_active", "created_at",
]
read_only_fields = ["id", "created_at"]

def get_avg_rating(self, obj) -> float | None:
reviews = obj.reviews.all()
if not reviews:
return None
return sum(r.rating for r in reviews) / len(reviews)


# 커스텀 검증
class ProductCreateSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = ["name", "description", "price", "stock", "category"]

def validate_price(self, value):
if value <= 0:
raise serializers.ValidationError("가격은 0보다 커야 합니다")
return value

def validate(self, data):
"""전체 데이터 검증"""
if data.get("stock", 0) > 0 and not data.get("is_active", True):
raise serializers.ValidationError("재고가 있는 상품은 활성 상태여야 합니다")
return data

ViewSet — CRUD 자동화

# products/views.py
from rest_framework import viewsets, permissions, status, filters
from rest_framework.decorators import action
from rest_framework.response import Response
from django_filters.rest_framework import DjangoFilterBackend
from .models import Product
from .serializers import (
ProductListSerializer, ProductDetailSerializer, ProductCreateSerializer
)


class ProductViewSet(viewsets.ModelViewSet):
"""
list: GET /products/
create: POST /products/
retrieve: GET /products/{id}/
update: PUT /products/{id}/
partial_update: PATCH /products/{id}/
destroy: DELETE /products/{id}/
"""
queryset = Product.objects.select_related("category", "owner").prefetch_related("reviews")
permission_classes = [permissions.IsAuthenticatedOrReadOnly]

# 필터·정렬·검색
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ["category", "is_active"]
search_fields = ["name", "description"]
ordering_fields = ["price", "created_at", "stock"]
ordering = ["-created_at"]

def get_serializer_class(self):
"""액션별 다른 serializer 사용"""
if self.action == "list":
return ProductListSerializer
if self.action == "create":
return ProductCreateSerializer
return ProductDetailSerializer

def perform_create(self, serializer):
"""생성 시 owner 자동 설정"""
serializer.save(owner=self.request.user)

def get_queryset(self):
"""비관리자는 자신의 상품만"""
qs = super().get_queryset()
if not self.request.user.is_staff:
return qs.filter(owner=self.request.user)
return qs

# 커스텀 액션 (비표준 엔드포인트)
@action(detail=True, methods=["post"], url_path="toggle-active")
def toggle_active(self, request, pk=None):
"""POST /products/{id}/toggle-active/"""
product = self.get_object()
product.is_active = not product.is_active
product.save()
return Response({"is_active": product.is_active})

@action(detail=False, methods=["get"], url_path="in-stock")
def in_stock(self, request):
"""GET /products/in-stock/"""
products = self.get_queryset().filter(stock__gt=0)
serializer = ProductListSerializer(products, many=True)
return Response(serializer.data)

Router — URL 자동 생성

# products/urls.py
from rest_framework.routers import DefaultRouter
from . import views

router = DefaultRouter()
router.register(r"products", views.ProductViewSet, basename="product")
router.register(r"categories", views.CategoryViewSet, basename="category")

urlpatterns = router.urls

# 자동 생성 URL:
# GET /products/ → list
# POST /products/ → create
# GET /products/{id}/ → retrieve
# PUT /products/{id}/ → update
# PATCH /products/{id}/ → partial_update
# DELETE /products/{id}/ → destroy
# POST /products/{id}/toggle-active/ → toggle_active (커스텀)
# GET /products/in-stock/ → in_stock (커스텀)

권한 클래스

from rest_framework.permissions import BasePermission, IsAuthenticated, IsAdminUser, SAFE_METHODS


class IsOwnerOrReadOnly(BasePermission):
"""읽기는 누구나, 쓰기는 소유자만"""

def has_object_permission(self, request, view, obj):
if request.method in SAFE_METHODS:
return True
return obj.owner == request.user


class IsVerifiedUser(BasePermission):
"""이메일 인증된 사용자만"""

def has_permission(self, request, view):
return bool(
request.user and
request.user.is_authenticated and
request.user.is_email_verified
)


# ViewSet에서 액션별 권한 설정
class ProductViewSet(viewsets.ModelViewSet):
def get_permissions(self):
if self.action in ["list", "retrieve"]:
return [permissions.AllowAny()]
if self.action == "destroy":
return [permissions.IsAdminUser()]
return [permissions.IsAuthenticated(), IsOwnerOrReadOnly()]

정리

구성 요소역할
ModelSerializer모델 기반 자동 직렬화
SerializerMethodField계산 필드 추가
ModelViewSetCRUD 자동 구현
@action커스텀 엔드포인트 추가
DefaultRouterURL 자동 등록
BasePermission커스텀 권한 클래스

DRF는 ModelSerializer + ModelViewSet + Router 조합으로 최소한의 코드로 완전한 CRUD API를 구축합니다.

Advertisement