Django - Layer / Permissions

김기훈·2026년 1월 3일

Django

목록 보기
14/17

각 레이어별 책임 🟥

HTTP 요청
 ↓
View
 ↓
Service  ← 정책 / 예외 / 트랜잭션
 ↓
Selector ← 쿼리 최적화
 ↓
ORM
 ↓
DB

ORM (Model Layer) 🟧

  • 역할: DB 구조 정의 + 관계 정의만

    • 해야하는 일
      • Field 정의 / FK, related_name / Meta 옵션
    • 하면 안 되는 일
      • 비즈니스 규칙 / 복잡한 쿼리 조합 / 권한 판단

View 🟧

  • 역할

    • “HTTP 조립” → request 파싱 / 요청데이터 검증 / service 호출 / response 반환
question = get_question_detail(question_id=question_id)
serializer = QuestionDetailSerializer(question)
  • 요청 → serializer → service → response 흐름
  • 요청 받기 → serializer 호출 / service 호출 / Response 만들기
    • HTTP status 결정
      • return Response(data, status=200)
# views.py에 있으면 안 되는 것
❌ ORM 쿼리
❌ annotate
❌ 비즈니스 규칙
❌ exists() 판단

Serializer/Selector 🟧

  • 흐름

                        "response에 필요한 데이터 다 가져와!"
                                     ↓
                요청 → View → Service → Selector → ORM 객체 → Serializer → Response(JSON)
                                                   ↑			  ↑
                                              (이미 관계 로딩 완료)	"이걸 이렇게 보여줄게"
  • Serializer는 “어떻게 보여줄지”를 책임

  • Selector는 “그걸 보여주기 위해 어떤 데이터를 어떻게 가져올지”를 책임짐


Selector (Service 분리)

  • 역할: “어떻게 가져올 것인가”만 책임

    • 어떤 테이블을? / 어떤 JOIN? / 어떤 prefetch / annotate / order_by
      • 쿼리 최적화 (select_related, prefetch_related)
    • DB에서 response에 필요한 데이터 형태를 만족하도록 가져오는 곳
    • “이 응답을 만들려면 어떤 관계까지 미리 로딩해야 하지?”를 고민하는 곳
    • 데이터 조회 최적화
  • 특징

    • 입력 → QuerySet / Instance 반환

  • 하지 않는 것

    • 예외 ❌ / 권한 ❌ / Response 구조 ❌
    • JSON 구조를 만들지 않음 / key 이름 바꾸지 않음 / 가공된 문자열 만들지 않음
      • 결과물은 Django ORM 객체(Question, Answer, …)
        • question = get_question_detail_queryset(question_id)
        • 이 시점의 question은 “Serializer가 꺼내 쓰기 좋은 상태로 로딩된 객체”

Serializer (Selector와의 관계)

  • 역할

    • Selector가 가져온 객체를 / API 응답(JSON) 형태로 변환
  • 하는 일

    • 필드 이름 바꾸기 (id → question_id)
    • 중첩 구조 만들기 (author, answers, comments)
    • 필요한 값만 뽑기 (이미지 URL 리스트)
    • 최종 response schema 보장
  • QuestionDetailSerializer(question).data
    • 여기서 비로소 response가 생성

Serializer

  • 시리얼라이즈 기본 동작

    • Serializer 필드명 → obj.<필드명> 접근 → 있으면 사용 → 없으면 에러
  • 역할: “데이터 계약 + 입력 검증”

    • DTO + 표현 담당
      • source 매핑 / 응답 구조 / format
      • ❌ ORM 접근 금지 / ❌ filter / annotate 금지
    • 입력 데이터 검증
      • class QuestionListQuerySerializer(serializers.Serializer):
      • 타입 / 범위 / 필수 여부 / 포맷
    • 출력 데이터 구조 정의
      • class QuestionListSerializer(serializers.ModelSerializer):
      • 어떤 필드를 내려줄지 / 프론트와의 응답 계약
    • 단순 필드 가공 (OK)
      • 문자열 자르기 / 날짜 포맷 변경 / DB 조회X
            # 시리얼라이저에 있으면 안되는 것 		|	# 입력 - service - 출력 
            ❌ Question.objects.filter(...)	|	입력 Serializer
            ❌ qs.exists()					|	└─ validation only
            ❌ raise QuestionListEmptyError	|	Service
            ❌ permission 체크				|	 └─ 데이터 조회 + 계산 필드
                                            |	출력 Serializer
            serializer는 절대 DB 상태 판단 ❌	|	 └─ 표현 + 가공 + 캐싱
  • Serializer가 내부적으로 하는 일

    • Serializer.__init__() 안에서는 다음 작업들이 일어남
      • self.instance 설정 (QuerySet / 객체 저장)
      • self.initial_data 설정 (입력 Serializer일 때)
      • self.fields 구성 (Meta.fields 기반)
      • SerializerMethodField 바인딩
      • context 설정
      • many=True 처리
  • 출력 serializer

    • 출력 Serializer에서는 뭐까지 허용되는가

      • SerializerMethodField
      • 데이터 가공 (path, preview)
      • 계산 필드
      • 캐싱 (in-memory)
      • 포맷 변경 (날짜, 문자열)
      • UI 친화적 구조 생성
입력 Serializer
 └─ validation only

Service
 └─ 데이터 조회 + 계산 필드

출력 Serializer
 └─ 표현 + 가공 + 캐싱

DRF 작성 가이드

  • serializers.py

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},
        }
  • ProductSerializerProduct 등록, 리스트 조회 시 사용할 시리얼라이저
    • 리스트 조회 시 상품 id, 이름, 상품이미지, 가격만 표시하도록 하기 위해
    • exclude와 extra_kwargs를 적절히 사용하여 이외의 필드가 노출되지 않도록 구성
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')
  • ProductDetailSerializerProduct 단일 상세 조회, 정보 수정 시 사용할 시리얼라이저
    • 시리얼라이저를 작성할 때 필요시
      • read_only_fields, extra_kwargs, validators 등을 통해 유효성 검증을 추가 가능

source 🟧

  • 객체의 어느 속성에서 값을 가져올지를 지정하는 옵션
    • source 기본 규칙

      • DRF Serializer 필드는 기본 → 필드명 = 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")

    • “API 응답에 answer_id라는 필드를 만들 건데, 그 값은 객체의 id 속성에서 가져와라”
    • AnswerSerializer(answer_instance)
      • Answer(id=3, content="...", ...)
        • DB 컬럼 이름: id / Django 모델 속성: answer_instance.id

예제 1

# Serializer
class AnswerSerializer(serializers.Serializer):
		...
    comments = AnswerCommentSerializer(source="answer_comments", many=True)
  • source="answer_comments"를 안 쓰면,
    • 기본적으로 answer.comments를 찾으려고 함
    • 모델에는 comments라는 속성이 없으면 에러가 나거나 빈 값이 나옴

예제 2

# 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.comments
      1. comments 속성이 없음 / 2. Serializer가 조용히 실패(comments 필드가 빈 배열)

예제 3

class Question(models.Model):
    ...
    answers = models.ForeignKey(...related_name="answers")

class QuestionDetailSerializer(serializers.Serializer):
	...
    answers = AnswerSerializer(many=True)
  • answers = AnswerSerializer(many=True)에는 왜 source 안써도 되는가?

    • Question에는 이미 question.answers 가 존재하기 때문

Service (Business Layer) 🟧

  • 비즈니스 로직을 Django의 View, Model, Serializer 등으로부터 분리하여,
  • 독립적인 모듈 또는 클래스로 정의하는 계층 구조(Layered Architecture)
    • 비즈니스 로직 예시
      • 인증(Auth)과 관련된 API를 호출할 때, 사용자를 회원가입 시키거나,
      • 로그인 요청을 처리하고, 비밀번호를 검증하거나, 토큰을 발급하는 등의 모든 작업
  • 역할: “무엇을 할 것인가”를 결정(“유스케이스의 핵심”)

    • 해야하는 일
      • 권한 검사 / 존재 여부 체크 / 예외 발생 / 상태 변경 / 트랜잭션
    • 특징
      • Selector 호출 / 예외 발생 / 상태 변경 가능 / 테스트 대상 1순위
      • 정책은 여기 / 실패 판단도 여기 / selector는 도구일 뿐
  • 장점

    • 인증과 관련된 다양한 처리 로직을 명확히 분리하고 관리할 수 있음
    • 유지보수성과 재사용성이 높아짐
  • 필요한 이유?

    • 비즈니스 로직 분리 → View는 요청 처리와 응답 반환에 집중하고, 서비스는 로직을 담당
    • 유지보수성 증가 → 하나의 서비스 로직만 수정하면 전체 로직에 반영됨
    • 재사용성 향상 → 여러 View나 Task 등에서 동일한 서비스 함수를 재사용 가능
    • 단위 테스트 용이 → View와 분리된 서비스 함수는 독립적으로 테스트 가능
# services.py에 있으면 안 되는 것
❌ request / response
❌ serializer.is_valid()
❌ HTTP status

흐름

  • 유스케이스 흐름 (Orchestration)

    • 여러 단계를 조합 / 순서가 중요 / 정책이 들어감
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()
  • ORM 쿼리 구성

    • 성능 최적화 / 조회 전략
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 → “들어온 뒤에 해도 되나?”

    구분PermissionService
    실행 시점요청 진입로직 실행 중
    관심사요청 자격도메인 규칙
    HTTP 의존O
    모델 접근거의 ❌
    DB 조회가능
    테스트API 테스트Service 테스트
    역할권장 위치
    비즈니스/도메인 검증 (title 중복, category 존재 여부, 유저 권한 등)Serializer가 담당하는 것이 DRF 공식 Best Practice
    요청 메서드 제어(GET/POST/PUT), 인증 여부 등View(APIView, GenericView)가 담당

Permission

  • Request → Authentication → Permission → Service

    • 이 요청 자체를 이 사용자가 할 자격이 있는가?
    • 로그인 했는가? | 관리자 / 일반 유저인가? | 이 HTTP 메서드를 쓸 수 있는가?
      • 이 요청을 누가 할 수 있는지 → permission은 if/else 판단만
- 인증 여부 / 역할 / 소유자 여부 (내 글만 수정)
class QuestionCreatePermission(BasePermission):
    def has_permission(self, request, view):
        return request.user.role == RoleChoices.ST
  • 접근 불가 시 즉시 차단
    • raise QuestionCreateNotAuthenticated()
# permissions.py에 있으면 안 되는 것
❌ 데이터 생성
❌ 조회 조건
❌ business logic

is_authenticated / is_anonymous

  • DRF에서 IsAuthenticated permission_classes가 적용돼 있으면

    • request.user.is_anonymous 체크는 대부분 필요 없음
    • 왜냐면 DRF가 먼저 막고 401을 반환하기 때문
    • 하지만 커스텀 메시지를 반환하기 위해서 직접 is_anonymous를 체크 함
  • request.user.is_anonymous → True / False 반환

    속성AnonymousUserUser
    is_authenticatedFalseTrue
    is_anonymousTrueFalse

profile
안녕하세요.

0개의 댓글