오늘의 목표
오늘 학습 내용 ✅
- PageNumberPagination, LimitOffsetPagination, CursorPagination
{
"count": 123,
"next": "...?page=3",
"previous": "...?page=1",
"results": [...]
}
적용 예시(내 코드)
queryset = get_question_list(**query_serializer.validated_data)
paginator = QuestionPageNumberPagination()
page = paginator.paginate_queryset(queryset, request)
serializer = QuestionListSerializer(page, many=True)
return paginator.get_paginated_response(serializer.data)
흐름
- get_question_list()가 필터/정렬/annotate가 반영된 QuerySet을 반환
- paginate_queryset(queryset, request)
- request.query_params의 page, size 등을 보고 QuerySet을 잘라서 page list 반환
- 잘린 page만 serializer에 넣어서 results를 만들고
- get_paginated_response()로 count/next/previous/results 형태로 감싸서 응답
내 코드에서 가능한 기능
- PageNumberPagination을 그대로 쓰지 않고
- QuestionPageNumberPagination로 커스터마이징
/api/v1/qna/questions?page=1&size=5 → 5개
- FE가 카드 UI에서 “한 화면에 12개/20개” 같은 UX를 원하면 size로 조절 가능
page_size = 10
page_size_query_param = "size"
max_page_size = 50
page가 “정상 범위 초과”일 때
200 + 빈 리스트 출력
DRF 기본
- page가 너무 크면 404 NotFound를 던짐
내코드
- except NotFound:에서 이를 잡아서
- page 자체가 잘못된 것(문자/0/음수) → 400(ValidationError)
- page 숫자는 정상인데, 범위만 초과 → 200 + results 빈 배열
- 페이지 초과 출력 예시(ex.총 페이지 2)
{
"count": 13,
"next": null,
"previous": "...?page=2",
"results": []
}
답변여부 필터
class QuestionListQuerySerializer(serializers.Serializer):
answer_status = serializers.CharField(required=False)
- 쿼리 파라미터 존재 여부 / 타입 검증 (문자열인가?)
queryset = get_question_list(**query_serializer.validated_data)
- Serializer 결과를 그대로 서비스에 전달
def get_question_list(
*,
answer_status: str | None = None,
...
):
answered = None
if answer_status == "answered":
answered = True
elif answer_status == "unanswered":
answered = False
GET /api/v1/qna/questions?answer_status=answered
GET /api/v1/qna/questions?answer_status=unanswered
질문 수정 api 시작
새롭게 알게된 내용 ✅
원격 변경 먼저 가져오기 (rebase)
- git pull --rebase origin feature/qna-question-detail-v2
- 원격 커밋(C)을 먼저 가져오고
- 내 커밋(B)을 그 위에 다시 얹음
- 커밋 히스토리 깔끔 유지
@transaction.atomic
- Django DB 트랜잭션을 하나의 “원자적(atomic) 작업 단위”로 묶어주는 데코레이터
- 즉, 이 함수 안의 DB 작업은 전부 성공하거나, 전부 실패해야 한다” 는 보장
- 함수 안에서 에러가 나면, 그 함수에서 수행된 모든 DB 변경 사항을 전부 롤백한다.
@transaction.atomic
def update_question(...):
question.title = ...
question.save()
QuestionImage.objects.filter(...).delete()
QuestionImage.objects.create(...)
- 트랜잭셔으로 묶인 기능
- 질문 제목/내용/카테고리 수정
- 기존 이미지 전부 삭제
- 새 이미지들 생성
- 모두 성공해야 변경 사항이 커밋 됨 / 중간에 하나라도 싪패하면 전부 롤백
Permission vs Serializer 역할 분리
Permission의 역할
- "이 요청을 처리해도 되는가?"
- 요청을 허용 / 거부
- YES / NO 판단
- side-effect 없음
- Response에 데이터를 넣지 않음
Serializer의 역할
- "이 객체를 이 사용자에게 어떻게 보여줄 것인가?"
- 응답에 포함될 데이터 표현
- UI 판단을 돕는 “상태 값” 제공
- FE를 위한 힌트
SerializerMethodField
DRF가 “이런 값 여기서 계산하라고 만든 장치”
PermissionDenied(DRF에서 403 전담 예외)
- from rest_framework.exceptions import PermissionDenied
- DRF가 제공하는 권한 거부용 예외 클래스 발생 시 HTTP 403 Forbidden 응답을 자동으로 반환
EMS
- EMS를 쓰려면 반드시 커스텀 Exception 클래스를 써야 한다?” = ❌
- PermissionDenied 사용해도 괜찮음
오늘 발생한 문제(발생 했다면) ✅