oz_externship - detail

김기훈·2025년 12월 17일

부트캠프 프로젝트

목록 보기
21/39

  • 질의응답 상세 조회("/api/v1/qna/questions/{question_id}")

    • 모든 웹사이트 이용자는 조회된 질의응답 목록 중에서 특정 항목을 클릭하여 상세 조회 가능
    • 특정 항목을 클릭 시 상세조회 페이지로 이동

  • 상세 조회 페이지에서 확인 가능한 항목

    • 질문 제목
    • 질문 내용
    • 질문 내용에 첨부된 이미지
    • 질문 작성자 정보
      • 프로필 썸네일 이미지
      • 닉네임
    • 질문 카테고리 정보
      • 대, 중, 소분류 카테고리 이름
    • 질문 조회수
    • 질문 작성일시

  • 답변 목록

    • 답변 작성자 정보
      • 프로필 썸네일 이미지
      • 닉네임
    • 답변 내용
    • 답변 작성일시
    • 답변 채택 여부
    • 답변에 대한 댓글 목록
      • 댓글 작성자 정보
        • 프로필 썸네일 이미지
        • 유저 닉네임
      • 댓글 내용
      • 댓글 작성일시

Success Response Schema

  "question_id": bigint,
  "title": str,
  "content": str,
  "images": [str],
  "category_path": str,
  "view_count": int,
  "created_at": str,
  "author": {
    "nickname": str,
    "profile_img_url": str
  },
  "answers": [
    {
      "answer_id": bigint,
      "content": str,
      "created_at": str,
      "is_adopted": bool,
      "author": {
        "nickname": str,
        "profile_img_url": str
      },
      "comments": [
        {
          "comment_id": bigint,
          "content": str,
          "created_at": str,
          "author": {
            "nickname": str,
            "profile_img_url": str
          }
        }
      ]
    }
  ]
}

Error Response Schema

{
  "400": {
    "error_detail": "유효하지 않은 질문 상세 조회 요청입니다."
  },
  "404": {
    "error_detail": "해당 질문을 찾을 수 없습니다."
  }
}

시작전 정리

  • 상세 조회는 입력 검증 거의 없음 → QuerySerializer 필요 없음
    • 조회 최적화 + 응답 가공이 중요

1차 구조

apps/qna/
├── views/
│   └── question/
│       └── question_detail_api.py      # HTTP 레벨
│
├── serializers/
│   └── question/
│       └── question_detail.py           # 출력 전용 Serializer
│
├── services/
│   └── question/
│       └── question_detail/
│           ├── service.py               # 비즈니스 로직
│           └── selectors.py             # DB 조회 최적화
│
├── exceptions/
│   └── question_exceptions.py           # 404, 400 등
│
├── urls/
│   └── question_urls.py

Serializer (출력 전용 DTO)

  • ModelSerializer 안 쓰고 Serializer 이유

    • ModelSerializer의 문제
      • 모델 구조에 강하게 결합 / 응답 필드가 DB 구조를 따라감 / 나중에 FE 요구사항 바뀌면 지옥
  • Serializer에 들어가도 되는 로직
    • OK → 단순 변환 / format 변경 / 리스트 가공
    • 들어가면 안 되는 로직 → DB 조회 / save() / 조건 분기 비즈니스 규칙
  • my serializer

    • Serializer 사용 (ModelSerializer ❌) / DB 접근 ❌
    • 계산 로직 최소화 / category_path는 util 재사용

View (Controller) 책임

  • View의 본질적인 역할

    • HTTP ↔ 내부 로직을 연결하는 관문 → View는 ‘입구’일 뿐, 일꾼이 아님
      • URL에서 값 받기 | 인증 / 권한 판단 | 요청/응답 포맷 제어 만 담당
      • DB 쿼리 / 비즈니스 규칙 / 조회수 증가 같은 도메인 로직 / 복잡한 가공 담당 ❌
  • my View

    • URL 파라미터 받기 (question_id) / Service 호출 / Serializer로 응답 포맷만 맞춤
    • 인증 ❌ (전체 공개) / queryset ❌ / annotate ❌ / select_related ❌ / 로직 ❌

Service 계층 (핵심)

  • Service는 무엇인가?

    • 비즈니스 규칙을 실행하는 중심 → “업무 규칙 담당자”
      • 질문이 없으면 404
      • 상세 조회 시 조회수 증가 / 질문 + 답변 + 댓글을 하나의 “도메인 행위”로 묶음
  • my Service

    • 질문 존재 여부 판단 / 조회수 증가 / 도메인 규칙 처리 / selector 호출
    • 상세 조회시 조회수 증가 = 도메인 규칙
    • Serializer / View가 알 필요 없음

Selector (DB 조회 전용)

  • Service 안에서 바로 ORM 안 쓰고 분리한 이유

    • 역할을 분리하기 위해서 → Service: 무엇을 할지 / Selector: 어떻게 가져올지
      • question = (Question.objects.select_related(...).prefetch_related(...)
      • 도메인 규칙 ❌ / 비즈니스 판단 ❌ / 순수하게 DB 최적화
    • 성능 문제
      • 질문 1 / 답변 N / 댓글 M → N+1 문제 발생 가능성 높음
      • 분리시에 select_related / prefetch_related / 서브쿼리 / Raw SQL 통제 가능
      • 나중에 캐싱 / read-replica / raw SQL 들어와도 영향 최소
      • 테스트에서 selector 단독 검증 가능

희망 순서

    1. Spec API (Mock) 먼저 작성
    1. 실제 API 연결
    1. 테스트 작성
    • service 테스트
    • api 테스트
    1. 조회수 동시성 처리 (select_for_update or F)

상세조회 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)
profile
안녕하세요.

0개의 댓글