
# EMS.E401_LOGIN_REQUIRED → {"error_detail": "로그인이 필요합니다."}
# 사용 예시
from rest_framework.response import Response
from rest_framework import status
return Response(
EMS.E401_LOGIN_REQUIRED,
status=status.HTTP_401_UNAUTHORIZED,
)
# EMS.E404_NOT_FOUND("질문") → {"error_detail": "질문을(를) 찾을 수 없습니다."}
# 사용 예시
return Response(
EMS.E404_NOT_FOUND("질문"),
status=status.HTTP_404_NOT_FOUND,
)
from rest_framework import status
from rest_framework.response import Response
from apps.core.exceptions import EMS
def get(self, request):
if not request.user.is_authenticated:
return Response(
EMS.E401_LOGIN_REQUIRED,
status=status.HTTP_401_UNAUTHORIZED,
)
from rest_framework.exceptions import APIException
from apps.core.exceptions import EMS
class QuestionNotFoundError(APIException):
status_code = 404
default_detail = EMS.E404_NOT_FOUND("질문")["error_detail"]
from rest_framework.exceptions import ValidationError
from apps.core.exceptions import EMS
raise ValidationError(EMS.E400_INVALID_REQUEST("질문 상세 조회"))
# 이 경우 custom_exception_handler가 이렇게 변환
{
"error_detail": "유효하지 않은 질문 상세 조회 요청입니다.",
"errors": {...}
}
class Serializer(serializers.Serializer):
title = serializers.CharField(error_messages=EMS.E400_REQUIRED_FIELD)
def service():
return Response(EMS.E403_PERMISSION_DENIED("조회"))
with self.assertRaises(SomeException):
실행할_코드()
“이 코드 블록 안에서 SomeException이 반드시 발생해야 한다”
*def create_question(
*,
author: User,
title: str,
content: str,
category: QuestionCategory,
image_urls: List[str],
) -> Question:
* 뒤에 오는 모든 파라미터는 위치 인자(positional)로 받을 수 없고 -----------------------------------
def f(a, b, c):
...
f(1, 2, 3) # OK (순서 중요)
f(a=1, b=2, c=3) # OK
-----------------------------------
def f(*, a, b, c):
...
f(a=1, b=2, c=3) # OK
f(1, 2, 3) # ❌ 에러
-----------------------------------
| 상황 | 의미 |
|---|---|
* 있음 | 순서 무시 / 키워드 필수 |
* 없음 | 순서 가능 / 키워드도 가능 |
@staticmethod 또는 @classmethod를 활용해 독립적으로 호출 가능하게 구성transaction.atomic으로 데이터 정합성을 관리 가능DRF
serializers.URLField()PrimaryKeyRelatedFieldfieldsclass Meta: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()에서 직접 처리해야 함
- 즉, 입력 보조용 필드 / 모델 저장을 돕는 용도
class QuestionImageCreateSerializer(serializers.ModelSerializer):
class Meta:
model = QuestionImage
fields = ["img_url"]
QuestionImage 모델을 직렬화/역직렬화하기 위한 Serializer 선언class Meta → ModelSerializer 에서 어떤 모델을 기준으로 할지, 어떤 필드를 쓸지 설정img_url 하나 / id, created_at 같은 것들은 필요 없어서 제외모델에 없는 필드도 시리얼라이저에서 “추가 선언” 하여
fields 리스트에 넣기 가능 ❗️
class QuestionCreateSerializer(serializers.ModelSerializer):
category = serializers.PrimaryKeyRelatedField(
queryset=QuestionCategory.objects.all()
)
image_urls = serializers.ListField(
child=serializers.URLField(),
write_only=True,
required=False,
)
images = QuestionImageCreateSerializer(many=True, read_only=True)
categoryPrimaryKeyRelatedField(category는 ForeignKey이기 때문에 검증 필요)queryset= 을 넣어줘야 FK 검증이 가능image_urlspresigned URL 을 받아서 저장할 때 사용 예정child=serializers.URLField() → 리스트 내 각 요소는 URL 형식이어야 함write_only=True → 요청 시에만 받고 응답에서는 제외required=False → 이미지가 없어도 질문 등록 가능imagesQuestionImage 목록을 응답에 표시하기 위한 필드read_only=True → 직접 입력받지 않음related_name="images" 로 연결된 FK 데이터를 자동으로 가져옴# serializers.py
class QuestionImage(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE, related_name="images")
related_name="images" 때문에 Question.images 로 해당 Question의 이미지 목록을 조회 가능images = ... → QuestionSerializer 에 images 라는 응답 필드를 추가하는 것QuestionImageCreateSerializer → 각 이미지 요소를 이 Serializer로 변환해서 JSON 형태로 보여줌many=True → 여러 개의 이미지가 연결될 수 있으므로 리스트 형태 직렬화.read_only=True → 클라이언트가 요청(body)에서 images를 보내면 안 됨.image_urls 로만 받고, 저장은 create()에서 직접 수행
Serializer.save()호출 시 실행되는 로직DB 저장 책임
def create(self, validated_data):
request = self.context["request"]
image_urls = validated_data.pop("image_urls", [])
question = Question.objects.create(
author=request.user,
**validated_data
)
for url in image_urls:
QuestionImage.objects.create(
question=question,
img_url=url
)
return question
requestimage_urlsquestionfor url in image_urls if serializer.is_valid():
question = serializer.save()
return Response(
{
"message": "질문이 성공적으로 등록되었습니다.",
"question_id": question.id,
},
status=status.HTTP_201_CREATED
)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
if serializer.is_valid():question = serializer.save().save() 호출 시 → Question 생성 / image_urls 리스트에서 QuestionImage들 생성test
class QuestionCreateAPITests(APITestCase):
def setUp(self):
# 테스트용 카테고리 생성
self.category = QuestionCategory.objects.create(name="프론트엔드")
# API URL
self.url = "/api/v1/qna/questions"
QuestionCreateAPITestsAPITestCase 상속def setUp(self):self.category = QuestionCategory.objects.create(...)self.url = "/api/v1/qna/questions" def test_question_create_success(self):
# 학생 유저 생성
student = User.objects.create_user(
email="student@test.com",
password="test1234",
name="학생A",
role=RoleChoices.ST,
)
# 인증 설정
self.client.force_authenticate(user=student)
payload = {
"title": "정상 등록 테스트",
"content": "내용입니다.",
"category": self.category.id,
"image_urls": ["https://test.com/img.png"],
}
response = self.client.post(self.url, payload, format="json")
# 검증
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertIn("question_id", response.data)
self.clientforce_authenticate(user=student)payloadresponse = self.client.post(self.url, payload, format="json")format="json" → DRF가 내부에서 JSON으로 인코딩해서 보내도록 전달self.assertEqual(a, b) → a == b 여야 테스트 통과. 아니면 실패(F).self.assertIn("question_id", response.data)responseview
class QuestionCreateAPIView(APIView):
permission_classes = [IsAuthenticated]
def post(self, request: Request) -> Response:
serializer = QuestionCreateSerializer(
data=request.data,
context={"request": request},
)
if serializer.is_valid():
question = serializer.save()
return Response(
{
"message": "질문이 성공적으로 등록되었습니다.",
"question_id": question.id,
},
status=status.HTTP_201_CREATED,
)
permission_classes = [IsAuthenticated] → 로그인한 사용자만 접근 가능serializer = QuestionCreateSerializercontext={"request": request}self.context["request"] 로 접근 가능if serializer.is_valid():question = serializer.save()개인적
# 오류 예시
urlpatterns = [
path("questions", QuestionListAPIView.as_view()), # GET
path("questions", QuestionCreateAPIView.as_view()), # POST
]
# 필드 하나 누락 = 문자열
raise ValidationError("잘못된 요청") → "detail": "잘못된 요청"
# 필드 단위 에러 (serializer 기본) = dict + list
{
"title": ["This field is required."],
"content": ["This field may not be blank."]
}
exc.detail == {
"title": ["This field is required."],
"content": ["This field may not be blank."]
}
# non_field_errors = dict + list
{
"non_field_errors": ["중복된 값입니다."]
}
# ListSerializer / bulk validation = list of dict
[
{"title": ["required"]},
{"content": ["required"]}
]
if isinstance(exc, ValidationError):
detail = exc.detail
if isinstance(detail, list) and detail: # list
response.data = {"error_detail": str(detail[0])}
elif isinstance(detail, dict): # dict
response.data = {"error_detail": next(iter(detail.values()))}
else: # 나머지
response.data = {"error_detail": str(detail)}
return response
if (
isinstance(exc, ValidationError)
and isinstance(view, QuestionCreateAPIView)
):
response.data = {
"error_detail": "유효하지 않은 질문 등록 요청입니다."
}
return responsecategory = get_category_or_raise(serializer.validated_data["category"])
question = create_question(
author=user,
title=serializer.validated_data["title"],
content=serializer.validated_data["content"],
category=category,
image_urls=serializer.validated_data.get("image_urls", []),
)
1. serializer.validated_data에서 title / content / image_urls 를 하나씩 꺼내서 다시 함수 인자로 나열
2. validated_data가 이미 “질문 생성에 필요한 데이터 묶음”인데 View에서 다시 풀어헤치고 있다는 느낌
3. 즉, validated_data로 대체 가능 이라는 말은
3-1. 이미 검증된 데이터 묶음이 있으니까 그걸 그대로 service로 넘기면 되지 않은가? 라는 말