2025/12/19 MainProject - 12

김기훈·2025년 12월 19일

TIL

목록 보기
88/194

오늘 학습 내용 ✅

진짜 이해해보기

  • 아래의 코드중 prefetch_related 이 부분부터 완벽이해를 해보자
def get_question_detail_queryset(question_id: int) -> Question | None:
    return (
        Question.objects
        .select_related("author", "category")
        .prefetch_related(
            "images",
            Prefetch(
                "answers",
                queryset=Answer.objects
                .select_related("author")
                .prefetch_related(
                    "answer_comments__author"
                )
            )
        )
        .filter(id=question_id)
        .first()
    )
  • 이 코드의 현재 목표


                Question (1)					# 질문 1개를 가져오면서
                 ├─ images (N)					# 질문의 이미지들
                 └─ answers (N)					# 질문의 답변들
                      ├─ author (1)				# 각 답변의 작성자
                      └─ answer_comments (N)	# 각 답변의 댓글들
                            └─ author (1)		# 각 댓글의 작성자
                                                # 를 추가 쿼리 없이 한 번에 메모리에 올리기  

.prefetch_related(
    "images",
    Prefetch(
        "answers",
        queryset=Answer.objects
            .select_related("author")
            .prefetch_related("answer_comments__author")
    )
)
  • 해석

    • Question을 가져올 때
      • Question.images 는 prefetch 하고
      • Question.answers 는 특별한 조건이 적용된 쿼리셋으로 prefetch 하겠다
        • 그 answers 안에서는
          • answer.author 는 select_related
          • answer.answer_comments 와 그 author 는 prefetch 하겠다
  • .prefetch_related("images")

    • Question.images 는 역참조 + 다대일/다대다
      • 단순히 “전부 다 가져오기”만 하면 됨 / 정렬, 필터, select_related 필요 없음
    • 별도의 쿼리로 QuestionImage row들을 가져와서, 파이썬 객체로 Question.images에 붙인다
    question_image 테이블
    -------------------------
    id
    question_id   ← FK (related_name = "images")
    img_url
    created_at
    updated_at
    → question_id + img_url 는 한 row에 같이 있음
    • .prefetch_related("images") 의 의미

      • Question 객체를 가져온 뒤 그 Question의 PK들을 모아서 QuestionImage 테이블에서
        • question_id IN (...) 인 row들을 전부 가져와라
      • 즉, QuestionImage 모델에서 해당 question에 속한 모든 row 를 필터 없이 가져온다
question.images.all()

# 위의 결과가 될 수 있는 모든 이미지row를 한번에 미리 로딩해 두는 것
[
    QuestionImage(question_id=1, img_url="a.jpg"),
    QuestionImage(question_id=1, img_url="b.jpg"),
    QuestionImage(question_id=1, img_url="c.jpg"),
]

# 장점
- prefetch 없을 때
question = Question.objects.get(id=1)
question.images.all()  # ❗ 여기서 쿼리 발생

- prefetch 있을 때
question = Question.objects.prefetch_related("images").get(id=1)
question.images.all()  # ⭕️ 쿼리 안 나감

- prefetch는 이 역참조 이름을 기준으로 동작 related_name이 없었다면 
question.questionimage_set
  • images는 Prefetch 객체 안쓴 이유

    이유설명
    필터 없음이미지 전부 필요
    정렬 필요 없음생성순 그대로 OK
    추가 FK 접근 없음serializer에서 img_url만 씀
  • 요약

    • related_name="images" 로 인해 Question.pk == QuestionImage.question_id 인
      • QuestionImage 객체들(row들) 을 .prefetch_related("images") 가 미리
      • 로딩해서 question.images 에 붙여준다

Prefetch

  • Question.answers 를 불러올 때 기본 Answer 쿼리 말고 내가 정의한 이 쿼리셋으로 가져와라
Prefetch(
    "answers",
    queryset=Answer.objects
        .select_related("author")
        .prefetch_related("answer_comments__author")
)
| 항목          | 의미                        |
| ----------- | -----------------------     |
| `"answers"` | Question → answers 관계      |
| `queryset=` | answers를 가져올 **커스텀 쿼리** |
  • .prefetch_related("answer_comments__author")
    • answer → answer_comments (N)
    • answer_comment → author (FK)
  • 전체 요약

    • 이 prefetch_related 블록은 Question → Answers → AnswerComments → Author 까지
      • 모든 중첩 관계를 N+1 없이 한 번에 로딩하기 위한 최적화 쿼리 구성이다

중첩

  • 중첩 “표현”은 시리얼라이저 덕분이고, 중첩 “데이터 구조”는 ORM 관계 덕분이다

기본용어 정리

Query (쿼리)

  • DB에게 “이런 데이터 줘”라고 요청하는 명령

    • SQL 예시 → SELECT * FROM question WHERE id = 1;
    • Django에서의 쿼리 → Question.objects.filter(id=1)
      • 아직 DB에 실행되진 않음 / “이런 조건의 데이터를 가져오겠다”는 요청서 수준

QuerySet (쿼리셋)

  • 쿼리들의 집합 + 아직 실행되지 않은 결과 후보

    • ex. qs = Question.objects.filter(category=1)
      • 이 qs: 리스트 ❌ / 실제 데이터 ❌ / DB에 날아간 결과 ❌ = “나중에 실행될 수 있는 쿼리 묶음 객체”
  • Lazy Evaluation (지연 실행)

# 아직 DB쿼리 안나감
qs = Question.objects.filter(category=1)
print(qs)

# 아래 순간 DB 쿼리 실행
list(qs)
qs.first()
qs[0]

Model (모델)

  • DB 테이블을 Python 클래스로 표현한 것

Instance (인스턴스)

  • 테이블의 “한 row”

# question = 인스턴스 → 실제 DB 데이터 1줄
question = Question.objects.first()

question.title / question.author → 이미 DB에서 가져온 값

Manager (objects)

  • 쿼리 생성기

    • Question.objects
      • .filter() / .get() / .annotate() / .select_related()
        • 전부 QuerySet을 만들어내는 역할

filter / get / exclude 차이

# 결과: QuerySet | 0개여도 에러 ❌
Question.objects.filter(category=1)

# 결과: Instance | 0개 ❌ / 2개 이상 ❌ → 에러
Question.objects.get(id=1)

# 조건을 제외
Question.objects.exclude(is_deleted=True)

Serializer/Selector

  • Serializer는 “어떻게 보여줄지”를 책임
    • Selector는 “그걸 보여주기 위해 어떤 데이터를 어떻게 가져올지”를 책임짐
  • Selector (조회 전용 레이어)

    • 역할

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

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

    • 역할

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

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

흐름

요청
 ↓
View
 ↓
Service
 ↓
Selector  ← "response에 필요한 데이터 다 가져와!"
 ↓
ORM 객체 (이미 관계 로딩 완료)
 ↓
Serializer ← "이걸 이렇게 보여줄게"
 ↓
Response(JSON)

질문등록api

  • 응답은 "question_id"만 내려주기
    • 응답은 무엇을 만들었는가 / 성공했는가 가 중요 등록api의 응답은 아이디값 하나로 충분
  • 응답을 풍부하게 줄 경우
    • 책임이 섞인다 (Single Responsibility 위반)
      • 현재 api명세서에 나온 응답 스키마는 사실상 상세 조회 응답급
      • Create API가 Read API의 책임까지 떠안는 구조
    • 중복 데이터 & 유지보수 비용 증가
      • 등록 응답 / 상세 조회 응답 둘 다에 같은 필드들이 중복된다.
      • 나중에 필드 하나 바뀌면?
        • 등록 API 수정 / 상세 조회 API 수정 / 테스트 2배 / 프론트 파싱 로직도 2군데
  • ID만 주는 게 왜 유용한 이유
    • API 역할이 명확해진다
      • POST /questions = 생성 | GET /questions/{id} = 조회
    • 프론트 흐름이 자연스럽다
질문 등록
 → question_id 수신
 → 상세 페이지로 이동
 → 상세 조회 API 호출

새롭게 알게된 내용 ✅

  • 객체의 내용과 응답 차이

question_image 테이블
-------------------------
id
question_id   ← FK (related_name = "images")
img_url
created_at
updated_at
  • question.images.all()로 얻는 QuestionImage 객체에는
    • img_url, created_at, updated_at 전부 들어있음
      • 하지만, 응답(JSON)에 다 나오는 건 아니다
images = question.images.all() → QuerySet[QuestionImage]

# 내부적으로는 아래와 같은 모델 인스턴스들의 리스트.
QuestionImage(
    id=1,
    question_id=1,
    img_url="a.jpg",
    created_at=...,
    updated_at=...
)
  • 보통 img_url 만 보이는 이유

# serializer 때문 → serializer가 img_url만 꺼내라고 지시했기 때문.
class QuestionImageSerializer(serializers.Serializer):
    img_url = serializers.CharField()

구현상속

  • 공통적으로 사용되는 함수나 모듈을 미리 만들어 놓고 상속하는 것

인터페이스 상속

  • 모든 렌더링이 필요한 객체

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

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

profile
안녕하세요.

0개의 댓글