2025/12/18 MainProject - 11

김기훈·2025년 12월 18일

TIL

목록 보기
87/194

오늘 학습 내용 ✅

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 / OneToOneselect_related
1:N / M:Nprefetch_related
1:N 안에서 FKPrefetch + 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 문제

# ex. 질문 10개를 조회해서 각 질문의 작성자 닉네임을 보여주기 

questions = Question.objects.all()  # 1번 쿼리
for q in questions:
    print(q.author.nickname)        # 질문마다 author 조회 쿼리 발생 가능

# result
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()

최적화 전

# 학생 10명을 조회해서 각 학생의 학교 이름 + 과제 제목들 + 과제의 댓글들을 출력

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명이 여러 개를 가질 때
# Student → Assignment (1:N) / Assignment → Comment (1:N)
# 적용
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

  • “prefetch하는 대상도 최적화”
# 문제 : comment_set에서 다시 N+1 발생
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 쿼리 조립기"

# “ORM이 들어간 제목 중에서 조회수가 0이 아닌 것들을 최신순으로 가져올 예정”
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]               # 여기서 DB 쿼리 발생

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)      # QuerySet

type(question)  # Question
type(qs)        # QuerySet

QuerySet은 “체이닝”이 가능

# 새로운 쿼리마다 쿼리 실행X / 조건만 누적 → 최종 실행 시점에 한번에 실행
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 응답

새롭게 알게된 내용 ✅

  • Spec API GET 에서는: request=None / 제거

  • source

    • 객체의 어느 속성에서 값을 가져올지를 지정하는 옵션
기본 동작: 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)을 읽어와서, 파이썬에서 다루기 좋게 만든 “실체”
      • Django에서는 이걸 모델 인스턴스
    • 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)을 파이썬 클래스로 감싼 인스턴스
    • DB의 한 줄 = Django 객체 하나

# 클래스
class User:
    def __init__(self, name):
        self.name = name

# 객체(인스턴스)
user = User("철수")

# User → 설계도(클래스) / user → 실제 만들어진 물건(객체) / user.name → 객체가 가진 값
  • Django 모델은 “DB 설계도 클래스”
    | DB | Django |
    | ------ | ---------- |
    | 테이블 | Model 클래스 |
    | 컬럼 | 필드 |
    | 행(Row) | 객체(Object) |

  • Django에서 “객체”가 생기는 순간

- 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 설명"
  • Django 객체의 중요성
question = Question.objects.get(id=1)

- 1. 이 객체는 단순한 dict 아님 / JSON 아님 / DB 그 자체 아님
- 2. DB와 연결된 파이썬 객체

# 따라서 이 작업 가능 
question.title = "새 제목"
question.save()   # DB UPDATE 발생

- 3. 객체 조작 = DB 조작
  • ForeignKey도 “객체로 연결됨”

class Answer(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE)

# 사용 → 객체 안에 객체가 연결됨
answer = Answer.objects.get(id=1)

answer.question        # Question 객체 / question_id 숫자 아님
answer.question.title  # 질문 제목
  • Serializer에서 말하는 “객체”

class QuestionSerializer(serializers.Serializer):
    title = serializers.CharField()

# obj = Serializer의 obj = Django 모델 객체
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 해석
# 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 필드가 빈 배열)
  • answers = AnswerSerializer(many=True)에는 왜 source 안써도 되는가?

    • Question에는 이미 question.answers 가 존재하기 때문
class Question(models.Model):
    ...
    answers = models.ForeignKey(...related_name="answers")

  • 시리얼라이즈 기본 동작

    • Serializer 필드명 → obj.<필드명> 접근 → 있으면 사용 → 없으면 에러
  • 시리얼라이저에 필드를 FK없는 필드를 추가?

어려운 내용(추가 학습 필요) ✅

오늘 발생한 문제(발생 했다면) ✅

profile
안녕하세요.

0개의 댓글