HTTP 요청
↓
View
↓
Service ← 정책 / 예외 / 트랜잭션
↓
Selector ← 쿼리 최적화
↓
ORM
↓
DB
question = get_question_detail(question_id=question_id)
serializer = QuestionDetailSerializer(question)
# views.py에 있으면 안 되는 것
❌ ORM 쿼리
❌ annotate
❌ 비즈니스 규칙
❌ exists() 판단
"response에 필요한 데이터 다 가져와!"
↓
요청 → View → Service → Selector → ORM 객체 → Serializer → Response(JSON)
↑ ↑
(이미 관계 로딩 완료) "이걸 이렇게 보여줄게"
Serializer는 “어떻게 보여줄지”를 책임
Selector는 “그걸 보여주기 위해 어떤 데이터를 어떻게 가져올지”를 책임짐
response에 필요한 데이터 형태를 만족하도록 가져오는 곳question = get_question_detail_queryset(question_id) # 시리얼라이저에 있으면 안되는 것 | # 입력 - service - 출력
❌ Question.objects.filter(...) | 입력 Serializer
❌ qs.exists() | └─ validation only
❌ raise QuestionListEmptyError | Service
❌ permission 체크 | └─ 데이터 조회 + 계산 필드
| 출력 Serializer
serializer는 절대 DB 상태 판단 ❌ | └─ 표현 + 가공 + 캐싱
Serializer.__init__() 안에서는 다음 작업들이 일어남입력 Serializer
└─ validation only
Service
└─ 데이터 조회 + 계산 필드
출력 Serializer
└─ 표현 + 가공 + 캐싱
class ProductSerializer(serializers.ModelSerializer):
image = serializers.ImageField(write_only=True)
image_url = serializers.CharField(read_only=True, source='image.url')
class Meta:
model = Product
exclude = ('created_at', 'updated_at')
extra_kwargs = {
'stock': {'write_only': True},
'description': {'write_only': True},
}
ProductSerializer 는 Product 등록, 리스트 조회 시 사용할 시리얼라이저class ProductDetailSerializer(serializers.ModelSerializer):
image = serializers.ImageField(write_only=True)
image_url = serializers.CharField(read_only=True, source='image.url')
class Meta:
model = Product
fields = '__all__'
read_only_fields = ('created_at', 'updated_at')
ProductDetailSerializer는 Product 단일 상세 조회, 정보 수정 시 사용할 시리얼라이저read_only_fields, extra_kwargs, validators 등을 통해 유효성 검증을 추가 가능serializers.XXXField()obj.comments 해석기본 동작: field_name = serializers.IntegerField()
DRF 내부 동작: value = obj.field_name
source 사용 동작: question_id = serializers.IntegerField(source="id")
DRF 내부 동작: value = obj.id
answer_id = serializers.IntegerField(source="id")# Serializer
class AnswerSerializer(serializers.Serializer):
...
comments = AnswerCommentSerializer(source="answer_comments", many=True)
source="answer_comments"를 안 쓰면, answer.comments를 찾으려고 함# Answer
class Answer(models.Model):
...
# AnswerComment
class AnswerComment(models.Model):
answer = models.ForeignKey("qna.Answer",related_name="answer_comments",...)
# 역참조 이름
answer.answer_comments
source="answer_comments"가 있어야 obj.answer_comments.all() → 정상동작comments = AnswerCommentSerializer(many=True) → obj.commentsclass Question(models.Model):
...
answers = models.ForeignKey(...related_name="answers")
class QuestionDetailSerializer(serializers.Serializer):
...
answers = AnswerSerializer(many=True)
View, Model, Serializer 등으로부터 분리하여, # services.py에 있으면 안 되는 것
❌ request / response
❌ serializer.is_valid()
❌ HTTP status
def get_question_list(...):
qs = base_queryset()
qs = apply_filters(qs)
if not qs.exists():
raise QuestionListEmptyError()
return paginate(qs)
if not qs.exists():
raise QuestionListEmptyError()
Question.objects.select_related(...).annotate(...)
# models.py
- 1. 데이터베이스 테이블과 매핑되는 구조 정의 / DB 저장,조회 기능 담당
from django.contrib.auth.models import AbstractBaseUser
from django.db import models
class User(AbstractBaseUser):
email = models.EmailField(unique=True, max_length=50)
name = models.CharField(max_length=20)
nickname = models.CharField(max_length=20, unique=True)
phone = models.CharField(max_length=13)
birthday = models.DatetimeField()
created_at = models.DatetimeField(auto_now_add=True)
updated_at = models.DatetimeField(auto_now=True)
USERNAME_FIELD = "email
class Meta:
db_table = "users"
# services/auth_service.py
- 1. 핵심 비즈니스 로직 담당/트랜젝션 처리/복잡한 흐름 제어/여러 모델 간 연동 등
from django.contrib.auth import authenticate
from rest_framework_simplwjwt.tokens import RefreshToken
class AuthService:
@staticmethod
def jwt_login(email: str, password: str) -> dict:
user = authenticate(email=email, password=password)
if user is None:
raise ValueError("이메일 또는 비밀번호가 올바르지 않습니다.")
refresh_token = RefreshToken.for_user(user)
return {
"access_token": str(refresh_token.access_token),
"refresh_token": str(refresh_token),
"user": user
}
# serializers.py
- 1. 요청 데이터의 유효성 검사
- 2. 모델 인스턴스를 JSON 등으로 직렬화하여 응답 데이터 생성
from rest_framework import serializers
from apps.users.models import User
class UserLoginRequestSerializer(serailizers.Serializer)
email = serializers.EmailField(max_length=50)
password = serializers.CharField()
class UserInfoSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ("id", "nickname", "email")
read_only_fields = fields
class UserLoginResponseSerializer(serializers.Serializer):
access_token = serializers.CharField()
refresh_token = serializers.CharField()
user = UserInfoSerializer()
# views.py
- 1. 요청을 받고, 서비스 호출 및 응답을 반환
- 2. 비즈니스 로직은 최소화
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from apps.users.services.auth_service import AuthService
from apps.users.serializers import UserLoginRequestSerializer, UserLoginResponseSerializer
class UserLoginAPIView(APIView):
def post(self, request):
serailizer = UserLoginRequestSerializer(request.data)
serailizer.is_valid(raise_exception=True)
# AuthService를 호출하여 jwt_login 메서드를 통해 비즈니스 로직 구현
try:
response_data = AuthService.jwt_login(serailizer.validated_data)
except ValueError as e:
return Response({"detail": str(e)}, status=status.HTTP_400_BAD_REQUEST)
rsp_serilizer = UserLoginResponseSerializer(response_data)
return Response({"data": rsp_serilizer.data}, status=status.HTTP_200_OK)
권한 처리
| 구분 | Permission | Service |
|---|---|---|
| 실행 시점 | 요청 진입 | 로직 실행 중 |
| 관심사 | 요청 자격 | 도메인 규칙 |
| HTTP 의존 | O | ❌ |
| 모델 접근 | 거의 ❌ | ✅ |
| DB 조회 | ❌ | 가능 |
| 테스트 | API 테스트 | Service 테스트 |
| 역할 | 권장 위치 |
|---|---|
| 비즈니스/도메인 검증 (title 중복, category 존재 여부, 유저 권한 등) | Serializer가 담당하는 것이 DRF 공식 Best Practice |
| 요청 메서드 제어(GET/POST/PUT), 인증 여부 등 | View(APIView, GenericView)가 담당 |
- 인증 여부 / 역할 / 소유자 여부 (내 글만 수정)
class QuestionCreatePermission(BasePermission):
def has_permission(self, request, view):
return request.user.role == RoleChoices.ST
raise QuestionCreateNotAuthenticated()# permissions.py에 있으면 안 되는 것
❌ 데이터 생성
❌ 조회 조건
❌ business logic
DRF에서 IsAuthenticated permission_classes가 적용돼 있으면
request.user.is_anonymous 체크는 대부분 필요 없음is_anonymous를 체크 함request.user.is_anonymous → True / False 반환
| 속성 | AnonymousUser | User |
|---|---|---|
| is_authenticated | False | True |
| is_anonymous | True | False |