오늘 학습 내용 ✅
PR review
annotated_qs = filtered_qs.annotate(content_preview=Substr("content", 1, 100)).annotate(
thumbnail_image_url=Subquery(
QuestionImage.objects.filter(question=OuterRef("pk")).order_by("created_at").values("img_url")[:1]
- created_at이 동일할 때 결과가 비동일적일 수도 있으니
- order_by("created_at", "id")로 바꾸는게 좋을 거 같아요
- order_by("created_at", "id")
- 1차 기준: created_at 오름차순 / 2차 기준: created_at 값이 같은 경우에만 id 오름차순
-를 붙이면 당연하게 내림차순 → id 큰 것 우선
- .order_by("-created_at") → 내림차순 (DESC) → 최근에 생성된 데이터가
결론
- 이미지는 최초 생성을 기준으로 가져오기 때문에 order_by("created_at", "id") 이게 맞다.
- 최신 이미지 → order_by("-created_at", "-id")
시리얼라이저
- 시리얼라이저는 항상 “현재 객체 1개” 기준
- 중첩 시리얼라이저는
- 부모 객체의 속성 / related_name 값을 기준으로 새로 시작
- source는
- “이 필드는 기준 객체의 어떤 속성을 쓸 건지” 지정
- View / Service에서 QuerySet 만들 때 사용
- “1:N / N:M 관계 데이터를 미리 한 번에 가져오는 ORM 최적화 도구”
- Question → answers (1:N)
- Answer → answer_comments (1:N)
- Question → images (1:N)
- “Serializer가 쓰게 될 데이터를 미리 준비하는 곳”에 사용
| 관계 | 방법 |
|---|
| FK / OneToOne | select_related |
| 1:N / M:N | prefetch_related |
| 1:N 안에서 FK | Prefetch + select_related |
| 깊은 중첩 | Prefetch 중첩 |
Question
├─ author (FK)
├─ category (FK)
├─ images (1:N)
└─ answers (1:N)
├─ author (FK)
└─ answer_comments (1:N)
└─ author (FK)
.select_related("author", "category")
- FK / OneToOne
- SQL JOIN
- “한 행당 하나”일 때
- JOIN이 아니라 별도 쿼리로 미리 로딩하고 싶을 때
.prefetch_related("answers", "images")
- 1:N / M:N
- 별도 쿼리 + Python에서 매칭
- “여러 개일 수 있을 때”
Prefetch
- prefetch_related("answers")는 “그냥 다 가져오기”밖에 못 함
- 답변 최신순 정렬 / 특정 조건만 가져오기
- 답변에 또 select_related / prefetch_related 적용 가능
from django.db.models import Prefetch
Prefetch(
"관계명",
queryset=커스텀_queryset,
to_attr="저장할_속성명"
)
| 상황 | 선택 |
|---|
| 그냥 관계 미리 불러오기 | prefetch_related("xxx") |
| 필터 / 정렬 필요 | Prefetch |
내부에 select_related 필요 | Prefetch |
| 댓글 → 작성자 같이 필요 | Prefetch |
| 성능 튜닝 | Prefetch |
from django.db.models import Prefetch
question = (
Question.objects
.select_related(
"author",
"category",
)
.prefetch_related(
"images",
Prefetch(
"answers",
queryset=Answer.objects
.select_related("author")
.prefetch_related(
Prefetch(
"answer_comments",
queryset=AnswerComment.objects.select_related("author"),
)
),
),
)
.get(id=question_id)
)
- 1. 질문 1개당 작성자 1명, 카테고리 1개(Question.author, Question.category → FK)
Question.objects.select_related("author", "category")
- 2. 이미지 개수는 0~N개 / JOIN 하면 row 수가 폭발함
Question.objects.prefetch_related("images")
- 3. answers는 1:N(prefetch) 하지만 answer.author는 FK(select_related)
Prefetch("answers",queryset=Answer.objects.select_related("author")...)
- 4. 댓글은 답변마다 여러 개 / 댓글 작성자도 FK
.prefetch_related(Prefetch
("answer_comments",queryset=AnswerComment.objects.select_related("author"),))
Prefetch
Question.objects.prefetch_related(
Prefetch(
"answers",
queryset=Answer.objects
.select_related("author")
.prefetch_related("answer_comments__author")
.order_by("-created_at")
)
)
질문은 한 번 / 답변은 한 번 / 답변 작성자는 JOIN / 답변의 댓글 + 댓글 작성자는 또 한 번
N+1 문제
questions = Question.objects.all()
for q in questions:
print(q.author.nickname)
1. 질문 목록 가져오는 쿼리 1번 (N개 질문)
2. 각 질문마다 author를 따로 가져오면 추가로 N번
3. 총 1 + N → 이게 N+1 문제
DRF serializer에서 중첩된 관계(작성자/이미지/답변/댓글)를 펼칠수록 N+1이 더 심해짐.
학습 재료
Student ───▶ School
│
└───▶ Assignment (여러 개)
│
└───▶ Comment (여러 개)
class School(models.Model):
name = models.CharField(max_length=50)
class Student(models.Model):
name = models.CharField(max_length=50)
school = models.ForeignKey(School, on_delete=models.CASCADE)
class Assignment(models.Model):
student = models.ForeignKey(Student, on_delete=models.CASCADE)
title = models.CharField(max_length=100)
class Comment(models.Model):
assignment = models.ForeignKey(Assignment, on_delete=models.CASCADE)
content = models.TextField()
최적화 전
students = Student.objects.all()
for s in students:
print(s.school.name)
for a in s.assignment_set.all():
print(a.title)
for c in a.comment_set.all():
print(c.content)
| 단계 | 쿼리 |
|---|
| 학생 목록 | 1 |
| 학생마다 학교 | 10 |
| 학생마다 과제 | 10 |
| 과제마다 댓글 | 30 |
| 총합 | ❌ 51번 |
“1명당 1개” 관계를 JOIN / 한 객체당 딱 하나만 연결된 관계 / FK
students = Student.objects.select_related("school")
for s in students:
print(s.school.name)
- JOIN으로 Student + School을 한 번에 가져옴 / school 접근할 때 DB 안 감
| 단계 | 쿼리 |
|---|
| 학생 + 학교 | 1 |
| 과제 | 10 |
| 댓글 | 30 |
| 총합 | ❌ 41 |
- “여러 개” 관계를 따로 모아서 묶기 / 1명이 여러 개를 가질 때
students = Student.objects.select_related("school").prefetch_related("assignment_set")
for s in students:
for a in s.assignment_set.all():
print(a.title)
- Django가 내부적으로
- 학생 id 목록 → WHERE student_id IN (...) / 파이썬에서 자동으로 학생별로 묶어줌
| 단계 | 쿼리 |
|---|
| 학생 + 학교 | 1 |
| 모든 과제 (IN 쿼리) | 1 |
| 댓글 | 30 |
| 총합 | ❌ 32 |
Prefetch
for a in s.assignment_set.all():
for c in a.comment_set.all():
print(c.content)
from django.db.models import Prefetch
students = Student.objects.select_related("school").prefetch_related(
Prefetch(
"assignment_set",
queryset=Assignment.objects.prefetch_related("comment_set")
)
)
| 단계 | 쿼리 |
|---|
| 학생 + 학교 | 1 |
| 모든 과제 | 1 |
| 모든 댓글 | 1 |
| 총합 | ✅ 3번 |
정리
| 도구 | 한 문장 설명 |
|---|
| select_related | “딱 1개 연결된 건 JOIN으로 한 번에” |
| prefetch_related | “여러 개는 따로 모아서 파이썬에서 묶기” |
| Prefetch | “prefetch할 때 가져오는 방식까지 직접 정하기” |
QuerySet
QuerySet = Django 객체들의 집합
- “아직 실행되지 않은 DB 조회 계획서이자, 실행되면 Django 객체들의 집합이 되는 것”
QuerySet이 생기는 순간
- qs = Question.objects.all()
- DB 조회 안 함 / 데이터 안 가져옴 / “Question 테이블을 전부 가져올 예정” 이라는 계획만 존재
QuerySet은 “DB 쿼리 조립기"
qs = (
Question.objects
.filter(title__icontains="ORM")
.exclude(view_count=0)
.order_by("-created_at")
)
DB에 진짜 쿼리를 날리는 시기
list(qs) / qs[0] / for q in qs: / len(qs) / bool(qs)
qs = Question.objects.all()
first = qs[0]
QuerySet의 결과물
qs = Question.objects.all()
[
<Question 객체>,
<Question 객체>,
<Question 객체>,
]
QuerySet vs 객체 차이
| 구분 | 정체 |
|---|
Question.objects.get() | 객체 1개 |
Question.objects.filter() | QuerySet |
qs[0] | 객체 |
qs | 객체들의 묶음 |
question = Question.objects.get(id=1)
qs = Question.objects.filter(id=1)
type(question)
type(qs)
QuerySet은 “체이닝”이 가능
qs = Question.objects.all()
qs = qs.filter(category=1)
qs = qs.exclude(is_deleted=True)
- 1. “Question 객체를 가져오는데 answer_count라는 가짜 컬럼을 붙여서 가져와라”
qs = Question.objects.annotate(
answer_count=Count("answers")
)
- 1-1. 나중에 가능
q = qs[0]
q.answer_count
- 2. QuerySet 단계에서 객체의 모양이 결정됨
QuerySet은 리스트X
- qs.append() / qs.pop() 불가능
- QuerySet은 DB 조회 결과 / 리스트는 메모리 데이터
- QuerySet은 읽기 전용 결과 뷰(view)에 가깝다
QuerySet이 Serializer로 넘어갈 때
- serializer = QuestionSerializer(qs, many=True)
- Serializer는 QuerySet을 순회
- 내부의 Question 객체 하나씩 처리 / dict로 변환 / 최종적으로 JSON 응답
새롭게 알게된 내용 ✅
기본 동작: 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
객체
- DB에서 한 행(row)을 읽어와서, 파이썬에서 다루기 좋게 만든 “실체”
- ex.
| id | content | answer_id |
| -- | -------- | --------- |
| 7 | 좋은 답변이네요 | 3 |
# Django ORM으로 조회
comment = AnswerComment.objects.get(id=7) # 이 comment가 바로 “객체”
# 파이썬 객체
comment.id # 7
comment.content # "좋은 답변이네요"
comment.answer_id # 3
DB에 접근(hit)하는 횟수를 줄이고 더 빠르게 데이터를 조회할 수 있게 해줌
객체
- DB의 한 행(row)을 파이썬 클래스로 감싼 인스턴스
class User:
def __init__(self, name):
self.name = name
user = User("철수")
- 1. DB에 데이터가 있을 때
id | title | content
1 | ORM? | ORM 설명
- 2. Django가 이걸 가져오면
question = Question.objects.get(id=1)
- 3. 이때 만들어지는 question이 Django 객체
question.id # 1
question.title # "ORM?"
question.content # "ORM 설명"
question = Question.objects.get(id=1)
- 1. 이 객체는 단순한 dict 아님 / JSON 아님 / DB 그 자체 아님
- 2. DB와 연결된 파이썬 객체
question.title = "새 제목"
question.save()
- 3. 객체 조작 = DB 조작
class Answer(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE)
answer = Answer.objects.get(id=1)
answer.question
answer.question.title
class QuestionSerializer(serializers.Serializer):
title = serializers.CharField()
obj = Question.objects.get(id=1)
obj.title
obj.author.nickname
| 구분 | 정체 |
|---|
Question | 클래스 |
question | 객체 (한 행) |
QuerySet | 객체들의 묶음 |
dict | 단순 데이터 |
JSON | 문자열 데이터 |
상세조회 400 발생 조건
-
검증 대상이 ‘request body’가 아니기 때문에 상세조회에서는
- serializer.is_valid() 형태를 거의 안 쓴다
- 입력받을게 question_id (path param) 이거 하나
- Body 없음 / QuerySerializer도 없음 때문에 굳이 안 만듬
-
POST / PUT / PATCH → Body 검증 → Serializer 사용 ✅
-
GET /{question_id} → Path parameter 검증 → Serializer ❌ (대부분)
-
굳이 Serializer로 검증하기를 원하다면
- 1.
class QuestionDetailPathSerializer(serializers.Serializer):
question_id = serializers.IntegerField(min_value=1)
serializer = QuestionDetailPathSerializer(
data={"question_id": question_id}
)
serializer.is_valid(raise_exception=True)
- 2.
class QuestionDetailQuerySerializer(serializers.Serializer):
question_id = serializers.IntegerField(min_value=1)
serializer = QuestionDetailQuerySerializer(
data={"question_id": question_id}
)
serializer.is_valid(raise_exception=True)
source
source는 Serializer 필드명과 실제 모델 속성명이 다를 때 반드시 써야 한다.
class AnswerSerializer(serializers.Serializer):
...
comments = AnswerCommentSerializer(source="answer_comments", many=True)
- source="answer_comments"를 안 쓰면, 기본적으로 answer.comments를 찾으려고 함
- 모델에는 comments라는 속성이 없으면 에러가 나거나 빈 값이 나옴
source 기본 규칙
- DRF Serializer 필드는 기본 → 필드명 = serializers.XXXField()
- 필드명과 동일한 속성을 객체에서 찾는다 → 내부적으로 obj.comments 해석
class Answer(models.Model):
...
class AnswerComment(models.Model):
answer = models.ForeignKey("qna.Answer",related_name="answer_comments",...)
answer.answer_comments
class Question(models.Model):
...
answers = models.ForeignKey(...related_name="answers")
시리얼라이즈 기본 동작
- Serializer 필드명 → obj.<필드명> 접근 → 있으면 사용 → 없으면 에러
시리얼라이저에 필드를 FK없는 필드를 추가?
어려운 내용(추가 학습 필요) ✅
오늘 발생한 문제(발생 했다면) ✅