오늘 학습 내용 ✅
진짜 이해해보기
- 아래의 코드중 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.images.all()
[
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 (지연 실행)
qs = Question.objects.filter(category=1)
print(qs)
list(qs)
qs.first()
qs[0]
Model (모델)
DB 테이블을 Python 클래스로 표현한 것
Instance (인스턴스)
question = Question.objects.first()
question.title / question.author → 이미 DB에서 가져온 값
Manager (objects)
쿼리 생성기
- Question.objects
.filter() / .get() / .annotate() / .select_related()
filter / get / exclude 차이
Question.objects.filter(category=1)
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
흐름
요청
↓
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=...
)
class QuestionImageSerializer(serializers.Serializer):
img_url = serializers.CharField()
구현상속
- 공통적으로 사용되는 함수나 모듈을 미리 만들어 놓고 상속하는 것
인터페이스 상속
어려운 내용(추가 학습 필요) ✅
오늘 발생한 문제(발생 했다면) ✅