11개월을 다닌 회사를 퇴사하고 그동안 멈췄던 블로그를 다시 시작합니다 :-)
아마 장고를 사용 중인 많은 분이 RDS에 PostgreSQL를 올려서 같이 사용 중인 거로 알고 있습니다. 그런데 장고 ORM 중 order_by
를 사용해 무언가를 정렬하다 한글 정렬을 하는 순간 원하는 대로 결과가 잘 나오지 않는 경우를 맞닥뜨리게 됩니다.
이것은 RDS에서 디폴트로 만들어주는 데이터베이스의 Collate가 en_US.UTF-8이기 때문에 생기는 현상입니다.
이것을 해결하는 방법은 생각보다 간단합니다.
from django.db.models import Func
ko_kr = Func(
"field_name",
function="ko_KR.utf8",
template='(%(expressions)s) COLLATE "%(function)s"'
)
Foo.objects.all().order_by(ko_kr.asc()) # 오름차순
Foo.objects.all().order_by(ko_kr.desc()) # 내림차순
이런 문제를 해결하는 방법은 직접 low query를 날리는 방법이고, 장고에서는 해당 기능을 위와 같이 제공해주고 있습니다.
field_name
에는 정렬할 칼럼 명을 써넣어야 합니다.template
에서 "%(function)s"
의 따옴표(quote)는 항상 큰따옴표(double quote)여야 합니다. 특히, black formatter를 사용 중인 분들은 자동으로 아래와 같이 포매팅 될 수 있는데 이러면 에러가 발생하니 주의 하셔야 합니다.template="(%(expressions)s) COLLATE '%(function)s'"
Django(DRF)에서 에러를 커스터마이징 하는 방법에 대해 얘기해보겠습니다.
첫 번째 방법으로 소개해드릴 것은 자주 사용하는 기존의 HTTP 응답 메세지 오버라이딩 해서 사용하는 것입니다. 이 방법은 status code의 의미은 살리되 세부적인 내용을 입맛에 맞게 바꿔서 쓰고싶을 때 사용할 수 있습니다. 예를 들어 똑같은 404
응답 코드도 다음과 같이 나눌 수 있습니다.
각각의 경우에 대해 똑같은 404 코드와 메시지로 응답을 보낼 수 있지만 서로 다른 메시지를 보여줄 수도 있습니다.
예시로 2번의 경우에 대해 메시지를 수정해보겠습니다. 전체적인 프로젝트 구조와 코드는 다음과 같습니다.
Proj Structure
code
# utils/custom_exceptions.py
from rest_framework import status
from rest_framework.exceptions import APIException
class NoObjectMatchException(APIException):
status_code = status.HTTP_404_NOT_FOUND
default_detail = "No object matches {path}"
def __init__(self, *args, **kwargs):
intstance = super().__init__(*args, **kwargs)
if params:=kwargs.get("path"):
intstance.default_detail.format(path=params)
# api/foo/urls.py
from django.urls import path
from . import views
urlpatterns = [
...,
path("<str:pk>/", views.SomeAPIView.as_view(), name="some"),
...
]
# api/foo/views.py
from rest_framework.generics import RetrieveAPIView
from custom_exceptions import NoObjectMatchException
from apps.foo.models import Foo
from . import serializers
class SomeAPIView(RetrieveAPIView):
queryset = Foo.objects.all()
serializer_class = serializers.SomeSerializer
def get_object(self):
filter_kwargs = {self.lookup_field: self.kwargs["pk"]}
if not Product.objects.filter(**filter_kwargs).exists():
raise NoObjectMatchException(self.kwargs["pk"])
return Product.objects.get(**filter_kwargs)
def get(self, request, *args, **kwargs):
return super().get(request, *args, **kwargs)
따로 APIException
를 상속받아 에러 클래스를 정의하면 raise
를 이용해 Serializer, view 또는 permission 단계에서 언제든지 http response를 던져줄 수 있다는 장점이 있다. 또한 상세한 메세지 설정으로 프론트에서 디버깅할 때 편리함을 더해주는 장점도 있다.
두 번째 방법은 내부적으로 사용하는 에러 코드와 메시지 모두 새롭게 만드는 방법입니다. 여담으로 지난 회사에서 내부적으로 HTTP 응답 코드를 직접 만든다는 사실 자체를 반대하는 분도 계셨었고 내부적으로만 사용할 것이기 때문에 문제가 될 것이 없다는 분도 계셨었습니다. 아무튼 저는 이 방법을 사용했고 잘 사용했었습니다.
이메일로 회원가입을 하는 서비스에서는 당연히 같은 이메일로 중복가입이 되어서는 안 됩니다. 장고의 경우 모델 클래스에서 unique=True
옵션을 통해 중복 값이 들어오지 못하고 500 에러를 내뱉을 수 있습니다. 하지만 500 에러를 받은 클라이언트에서는 이게 email 중복 상태인지 어떤 상황인 정확히 알 수 없으므로 에러 코드와 메세지 모두를 활용해 정확히 어떤 에러인지 나타내주는 것입니다.
# custom_exceptions.py
from rest_framework.exceptions import APIException
HTTP_600_duplicated_email_error:int = 600
class DuplicateEmailErrorClass(APIException):
status_code = HTTP_600_duplicated_email_error
default_detail:str = "Email already signed up"
# serializers.py
from rest_framework.serializer import CharField, Serializer
from exceptions import DuplicateEmailErrorClass
from apps.user.models import User
class UserSignupSerializer(Serializer):
email = CharField(allow_blank=False, allow_empty=False, required=True)
def validate(self, attrs: Dict[str, Any]) -> Dict[str, Any]:
email = attrs["email"]
if User.objects.filter(email=email).exists():
raise DuplicateEmailErrorClass
...
추가적으로 serializer나 view 클래스가 아닌 permission class를 커스터마이징 해서 컨트롤 할 수도 있습니다.
# exceptions.py
from rest_framework.exceptions import APIException
HTTP_601_NOT_MEET_REQUIREMENT:int = 601
class NotMeetRequirementError(APIException):
status_code = HTTP_601_NOT_MEET_REQUIREMENT
default_detail:str = "This User NOT Meet all requirement policy"
# permissions.py
from exceptions import NotAgreePolicyError
from rest_framework.permissions import IsAuthenticated
class IsMeetRequirement(IsAuthenticated):
def has_permission(self, request, view):
if not getattr(request.user, "is_meet_requirement"):
raise NotAgreePolicyError
return super().has_permission(request, view)
# views.py
class UserSignInAPIView(GenericAPIView):
permission_classes = [IsMeetRequirement]
...
def post(self, request, *args, **kwargs):
...
프론트엔드에서(js 또는 tx 기반 프론트엔드 기준) 백엔드로 json 데이터를 전송할 때 DRF 시리얼라이저에서 어떻게 기준을 정해주는지를 필드 내 옵션을 통해 정의 가능합니다.
class Serializer(Serializer):
tmp = CharField(allow_blank=True)
{"tmp": ""}
와 같은 형태를 허용합니다. 보통 model
에 blank=True
인 경우 해당 옵션을 활성화 해서 사용합니다.
class Serializer(Serializer):
tmp = CharField(allow_null=True)
{"tmp": null}
와 같은 형태를 허용합니다. 보통 model
에 null=True
인 경우 해당 옵션을 활성화 해서 사용합니다.
class Serializer(Serializer):
tmp = Field(required=False)
{"tmp":undefined}
와 같은 형태를 허용합니다. 또는 json body
내 tmp
키값 자체를 뺄 수 있습니다.
Make Spesific field validation func in Serializer
DRF Serializer에서는 input data를 validation을 실행합니다.
하지만 이것은 기본적인 검증 로직으로 시리얼라이저에 선언된 필드 정의에 따라 type check
만 하는 수준(level)입니다.
하지만 아래와 같이 validate
함수를 오버라이딩해서 원하는 검증 로직을 추가할 수 있습니다.
from rest_framework.serializer import Serializer, ValidationError
class CustomSerializer(Serializer):
field1 = ChaField()
field2 = BooleanField()
field3 = ListField(child=IntegerField())
def validate(self, attrs):
attrs = super().validate(attrs)
# field1 부터 fields3까지 타입 체크에 머무는 수준이다.
# 원하는 유효성 검사 코드를 이 곳에 추가
# ex)
f1:str = attrs.get("field1")
if not f1.startswith("https://"):
raise ValidationError() # raise 400 error
...
return attrs
한 단계 더 나아가 field1
만 체크해도 무방한 경우에는 어떻게 만들 수 있을까요? 또는 가독성을 위해 특정 필드만 유효성 검사하는 함수를 만들 수 있을까요? 다음과 같이 만들 수 있습니다.
class CustomSerializer(Serializer):
field1 = ChaField()
field2 = BooleanField()
field3 = ListField(child=IntegerField())
def validate_field1(self, f1):
f1: str
if not isinstance(f1, str):
raise ValidationError("...")
if not f1.startswith("https://"):
raise ValidationError("...")
...
return f1
위와 같이 Serializer class
내부에 선언해두면 해당 필드만 검증 로직이 작동합니다. 물론 기본적으로 전체 유효성 검사 함수 def validate(self, attrs):
또한 작동합니다.
return field
필수리턴 값이 없을 시 serializer에서는 해당 필드를 None
으로 리턴 받습니다.
def validate_field_name(self, variable_name):
field_name에는 시리얼라이저에 선언한 필드 이름(field1, field2 또는 field13)을 사용해야하고, 해당 이름에 따라 들어오는 값도 달라집니다. self 뒤에 따라오는 인자 명(argument naming)은 어떤식으로 작명하든 무방합니다.