2025/12/20 현재 구현 현황

김기훈·2025년 12월 20일

TIL

목록 보기
89/191

오늘 학습 내용 ✅

현재까지 구현 현황

질문 등록 API

  • Endpoint: POST /api/questions/

    • 권한: 인증 필요 / 학생(ST)만 가능 / QuestionCreatePermission
  • Request Body

    • QuestionCreateSerializer
{
  "title": "질문 제목",
  "content": "질문 내용",
  "category_id": 3,
  "image_urls": [
    "https://s3.amazonaws.com/xxx/1.png",
    "https://s3.amazonaws.com/xxx/2.png"
  ]
}
  • Response

# 201
{
  "message": "질문이 성공적으로 등록되었습니다.",
  "question_id": 12
}
  • 내부 흐름

APIView
 └─ Serializer 검증
 └─ get_category_or_raise()
 └─ create_question()
     ├─ Question 생성
     └─ QuestionImage bulk 생성

질문 목록 조회 API

  • Endpoint → GET /api/questions/

    • 권한: 공개(비로그인 가능) / QuestionListQuerySerializer
  • Query Parameters

{
  "answered": true,
  "category": 2,
  "search": "django",
  "page": 1,
  "page_size": 10
}
  • Response

{
  "page": 1,
  "size": 10,
  "total_count": 42,
  "questions": [
    {
      "question_id": 1,
      "category_path": "Backend > Django",
      "profile_img_url": null,
      "nickname": "철수",
      "title": "Django ORM 질문",
      "content_preview": "ORM에서 annotate를 쓰면...",
      "answer_count": 2,
      "view_count": 15,
      "created_at": "2025-12-18T12:30:00",
      "thumbnail_img_url": "https://..."
    }
  ]
}
  • 내부 흐름

APIView
 └─ QuerySerializer 검증
 └─ get_question_list()
     ├─ base queryset
     ├─ filter_by_answered
     ├─ filter_by_category (하위 카테고리 포함)
     ├─ filter_by_search
     ├─ annotate (preview, thumbnail)
     └─ pagination

질문 상세 조회 API

  • Endpoint: GET /api/questions/{question_id}/

    • 권한: GET /api/questions/{question_id}/
  • Path Parameter

    • question_id: int (>=1)
  • Response

{
  "question_id": 1,
  "title": "Django ORM 질문",
  "content": "내용...",
  "images": ["https://..."],
  "category_path": "Backend > Django",
  "view_count": 16,
  "created_at": "2025-12-18T12:30:00",
  "author": {
    "nickname": "철수",
    "profile_img_url": null
  },
  "answers": [
    {
      "answer_id": 3,
      "content": "이렇게 하세요",
      "created_at": "2025-12-18T13:00:00",
      "is_adopted": false,
      "author": {...},
      "comments": [...]
    }
  ]
}
  • 내부 흐름

APIView
 └─ question_id 검증
 └─ get_question_detail()
     ├─ selector (select_related + prefetch)
     ├─ 없으면 404
     └─ view_count 증가
 └─ Serializer 응답

Query Parameter

from rest_framework import serializers


class QuestionListQuerySerializer(serializers.Serializer):
    page = serializers.IntegerField(min_value=1, default=1)
    size = serializers.IntegerField(min_value=1, max_value=50, default=10)

    search_keyword = serializers.CharField(required=False, allow_blank=True)
    category_id = serializers.IntegerField(required=False, min_value=1)

    answer_status = serializers.ChoiceField(
        choices=["answered", "unanswered"],
        required=False,
    )

    sort = serializers.ChoiceField(
        choices=["latest", "oldest", "views"],
        required=False,
        default="latest",
    )
    
# 필터 변경
def filter_by_answered(qs, answer_status: str | None):
    if answer_status == "answered":
        ...
  • 위 처럼 필터코드를 변경하면 문제점
    • filter가 HTTP 계약을 알게 됨
    • 나중에 enum 변경 시 filter도 수정 / 재사용성 ↓ / 책임 분리 붕괴
      • Service Layer 침범

sort 방어 로직

def filter_by_sort(qs, sort):
    ...
    return qs

# 이렇게 변환하지 않는 이유
if sort not in {"latest", "oldest", "views"}:
    return qs

- 1. 이미 QuerySerializer에서 ChoiceField로 막고 있어서 굳이 추가 안함

새롭게 알게된 내용 ✅

QuestionFactory

  • Django 테스트에서 쓰는 “테스트용 객체 생성기

args=[self.question.id]

  • reverse("question-detail", args=[self.question.id])
    • URL path에 들어갈 “동적 숫자 값”을 순서대로 넣어주는 것
  • args (순서 기반)

    • <int:question_id> 가 첫 번째 path 변수라서 리스트의 첫 번째 값이 들어감
  • kwargs (이름 기반)

    • reverse("question-detail", kwargs={"question_id": self.question.id})
    • path 변수 이름과 매칭 / 순서 바뀌어도 안전 / 가독성 더 좋음
    • 장점

# 현재 
path("questions/<int:question_id>/", ...)
# 미래
path("questions/<int:question_id>/detail/", ...)

1. 위의 예시처럼 변경될 경우 args=[...] → 문제 생길 가능성 있음

reverse() 사용 시

  • path parameter가 있으면 → kwargs 사용
  • query parameter는 → ?key=value

(oz-externship-05-py3.13) ➜  oz_externship git:(feature/qna-question-detail) git rebase develop

Current branch feature/qna-question-detail is up to date
  • 최신브랜치라는 뜻 굳이 푸시할 필요 없음

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

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

1

[ 🔴 문제: 테스트코드 실패 ]

질문 상세 조회 API 호출 시 TypeError: QuestionDetailAPIView.get()
got an unexpected keyword argument 'pk' 에러 발생으로 인해 테스트 실패 

[ 🟡 원인: URL Path Parameter와 View 메서드 시그니처 불일치 ] 

URL에서는 <int:pk>를 사용하고 있었고 View의 get() 메서드는 question_id 파라미터를 받도록 정의되어 있었다.

# urls.py
path("questions/<int:pk>/", ...)

# views.py
def get(self, request, question_id: int):
    ...

이로 인해 Django가 pk 인자를 전달했지만 View에서 이를 받을 수 없어 TypeError가 발생했다.

[ 🔵 해결: URL Path Parameter 이름을 View와 통일 - questions/<int:question_id> ]

2

[ 🔴 문제: Permission에서 Exception을 return한 문제 ]

if isinstance(user, AnonymousUser) or not user.is_authenticated:
    return QuestionUpdateNotAuthenticated()


[ 🟡 원인: has_permission()의 반환값은 반드시 bool ]

Exception을 return하면 DRF 내부에서 True로 평가될 수 있음
permission이 통과되어 인증되지 않은 사용자도 로직 진입 가능 결과적으로 401이 보장되지 않음

[ 🔵 해결: Exception은 raise 해야 함 ]

if isinstance(user, AnonymousUser) or not user.is_authenticated:
    raise QuestionUpdateNotAuthenticated()

3

[ 🔴 문제: has_object_permission이 호출되지 않는 문제 ]

QuestionUpdatePermission에 has_object_permission()이 정의되어 있지만 실제로 한 번도 호출되지 않음

[ 🟡 원인: View에서 get_object() 사용 ❌ / self.check_object_permissions() 호출 ❌]

DRF는 객체 권한 검사를 자동으로 하지 않음


[ 🔵 해결: def has_object_permission(self,request: Request,view: APIView,obj: Question) 제거 ]

작성자 검증은 Service에서만 처리  Permission에서는 인증 여부 수강생(ST) 여부만 체크

4

[ 🔴 문제: Service에서 PermissionError 사용 문제 ]

if question.author_id != author_id:
    raise PermissionError()

[ 🟡 원인: PermissionError는 DRF 예외 ❌ / APIException ❌ / EMS ❌ ]

custom_exception_handler와 연결되지 않음
500 에러 / 혹은 의도치 않은 응답 발생 가능

[ 🔵 해결: DRF 예외로 명확히 변환 ]

from rest_framework.exceptions import PermissionDenied

raise PermissionDenied(
    EMS.E403_OWNER_ONLY_EDIT("질문")["error_detail"]
)
profile
안녕하세요.

0개의 댓글