view = context.get("view")
# DRF가 예외를 처리할 때 예외가 발생한 View 인스턴스를 넣어줌
is_question_create_api = isinstance(view, QuestionCreateAPIView)
# view가 QuestionCreateAPIView의 인스턴스면 → True 아니면 → False
if isinstance(response.data, dict) and "detail" in response.data:
response.data = {"error_detail": response.data["detail"]}
{ "detail": "..." } 형태면 True { "title": ["required"] } 형태면 False| Method | 의미 |
|---|---|
| GET | 조회 |
| POST | 생성 |
| PUT/PATCH | 수정 |
| DELETE | 삭제 |
list[ErrorDetail] / dict[str, list[ErrorDetail]]# 오류 예시
urlpatterns = [
path("questions", QuestionListAPIView.as_view()), # GET
path("questions", QuestionCreateAPIView.as_view()), # POST
]
Serializer를 쓰면 모델에 정의되지 않은 필드를 자유롭게 정의해서 사용할 수 있다.
ModelSerializer는 모델 필드 중심이지만, 제한적으로만 추가 필드를 쓸 수 있다.
| 구분 | Serializer | ModelSerializer |
|---|---|---|
| 모델 의존성 | ❌ 없음 | ✅ 있음 |
| 모델에 없는 필드 | ✅ 자유롭게 가능 | ⚠️ 가능하지만 제한적 |
| 자동 필드 생성 | ❌ 없음 | ✅ 있음 |
| save() | ❌ 직접 구현 | ✅ 자동 |
| 목적 | 요청/응답 스펙 정의 | 모델 CRUD |
from rest_framework import serializers
class ExampleSerializer(serializers.Serializer):
title = serializers.CharField()
page = serializers.IntegerField()
answered = serializers.BooleanField(required=False)
- 위의 필드들은 모델이 없어도 사용 가능 / DB 저장과 무관 / 요청 파라미터 검증에 최적
- 조회 조건(Query Param), 복합 입력, 계산 전용 필드에 자주 사용
class QuestionCreateSerializer(serializers.ModelSerializer):
image_urls = serializers.ListField(
child=serializers.URLField(),
write_only=True,
required=False,
)
class Meta:
model = Question
fields = ["title", "content", "image_urls"]
- 가능은 함 하지만, image_urls는 DB 필드가 아님, save()에서 직접 처리해야 함
- 즉, 입력 보조용 필드 / 모델 저장을 돕는 용도
path("spec/questions", QuestionCreateSpecAPIView.as_view()),
path("spec/questions", QuestionListSpecAPIView.as_view()),# 실제 API → View가 하나 → Method는 View 내부에서 분기
class QuestionAPIView(APIView):
def get(self): ...
def post(self): ...
/spec/questions # GET (목록 스펙)
/spec/questions/create # POST (등록 스펙)
# 기존
def custom_exception_handler(
exc: Exception,
context: dict[str, Any],
) -> Optional[Response]:
response = exception_handler(exc, context)
if response is None:
return None
view = context.get("view")
is_question_create_api = isinstance(view, QuestionCreateAPIView)
# "포맷 통일" detail -> error_detail(메시지 내용은 바꾸지 않음)
## 403도 따로 설정하지 않아도 포맷되서 error_detail로 나옴
if isinstance(response.data, dict) and "detail" in response.data:
response.data = {"error_detail": response.data["detail"]}
# 1) 400: serializer validation
if isinstance(exc, ValidationError) and is_question_create_api:
response.data = {"error_detail": "유효하지 않은 질문 등록 요청입니다."}
# 2) 401: 인증 안 됨
elif isinstance(exc, NotAuthenticated) and is_question_create_api:
response.data = {"error_detail": "로그인한 수강생만 질문을 등록할 수 있습니다."}
# 3) 404: 카테고리 없음
elif isinstance(exc, CategoryNotFoundError) and is_question_create_api:
response.data = {"error_detail": str(exc.detail)}
# 4) 409: 중복제목
elif isinstance(exc, DuplicateQuestionTitleError) and is_question_create_api:
response.data = {"error_detail": str(exc.detail)}
return response
# 변화
def custom_exception_handler(exc, context):
response = exception_handler(exc, context)
if response is None:
return None
request = context.get("request")
method = request.method if request else None
# 공통 포맷 통일 -> 403도 알아서 처리
if isinstance(response.data, dict) and "detail" in response.data:
response.data = {"error_detail": response.data["detail"]}
# POST /questions (등록)
if method == "POST":
if isinstance(exc, ValidationError):
response.data = {"error_detail": "유효하지 않은 질문 등록 요청입니다."}
elif isinstance(exc, NotAuthenticated):
response.data = {"error_detail": "로그인한 수강생만 질문을 등록할 수 있습니다."}
elif isinstance(exc, CategoryNotFoundError):
response.data = {"error_detail": str(exc.detail)}
elif isinstance(exc, DuplicateQuestionTitleError):
response.data = {"error_detail": str(exc.detail)}
# GET /questions (조회)
elif method == "GET":
if isinstance(exc, ValidationError):
response.data = {"error_detail": "유효하지 않은 질문 목록 조회 요청입니다."}
return response