Django 더 잘 쓸 수 있는 몇가지 팁

Johnywhisky·2022년 10월 22일
0

Django

목록 보기
1/1

11개월을 다닌 회사를 퇴사하고 그동안 멈췄던 블로그를 다시 시작합니다 :-)

PostgreSQL(on RDS) 한글 정렬

아마 장고를 사용 중인 많은 분이 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를 날리는 방법이고, 장고에서는 해당 기능을 위와 같이 제공해주고 있습니다.

주의사항

  1. field_name에는 정렬할 칼럼 명을 써넣어야 합니다.
  2. template에서 "%(function)s"의 따옴표(quote)는 항상 큰따옴표(double quote)여야 합니다. 특히, black formatter를 사용 중인 분들은 자동으로 아래와 같이 포매팅 될 수 있는데 이러면 에러가 발생하니 주의 하셔야 합니다.
    template="(%(expressions)s) COLLATE '%(function)s'"

Error Customizing

Django(DRF)에서 에러를 커스터마이징 하는 방법에 대해 얘기해보겠습니다.

Customizing Error Message

첫 번째 방법으로 소개해드릴 것은 자주 사용하는 기존의 HTTP 응답 메세지 오버라이딩 해서 사용하는 것입니다. 이 방법은 status code의 의미은 살리되 세부적인 내용을 입맛에 맞게 바꿔서 쓰고싶을 때 사용할 수 있습니다. 예를 들어 똑같은 404 응답 코드도 다음과 같이 나눌 수 있습니다.

  1. 요청 url에 대응하는 api가 없을 때
  2. path 파라미터에 대응하는 리소스가 존재하지 않을 때

각각의 경우에 대해 똑같은 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를 던져줄 수 있다는 장점이 있다. 또한 상세한 메세지 설정으로 프론트에서 디버깅할 때 편리함을 더해주는 장점도 있다.

Custom Error Define

두 번째 방법은 내부적으로 사용하는 에러 코드와 메시지 모두 새롭게 만드는 방법입니다. 여담으로 지난 회사에서 내부적으로 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 시리얼라이저에서 어떻게 기준을 정해주는지를 필드 내 옵션을 통해 정의 가능합니다.

allow_blank

class Serializer(Serializer):
	tmp = CharField(allow_blank=True)

{"tmp": ""}와 같은 형태를 허용합니다. 보통 modelblank=True인 경우 해당 옵션을 활성화 해서 사용합니다.

allow_null

class Serializer(Serializer):
	tmp = CharField(allow_null=True)

{"tmp": null}와 같은 형태를 허용합니다. 보통 modelnull=True인 경우 해당 옵션을 활성화 해서 사용합니다.

required

class Serializer(Serializer):
	tmp = Field(required=False)

{"tmp":undefined}와 같은 형태를 허용합니다. 또는 json bodytmp 키값 자체를 뺄 수 있습니다.

시리얼라이저 특정 필드 유효성 검사

Make Spesific field validation func in Serializer

Validation fields

DRF Serializer에서는 input data를 validation을 실행합니다.
하지만 이것은 기본적인 검증 로직으로 시리얼라이저에 선언된 필드 정의에 따라 type check 만 하는 수준(level)입니다.

validate function

하지만 아래와 같이 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)은 어떤식으로 작명하든 무방합니다.

profile
안녕하세요 :) 1년 차 Pythonist 백엔드 개발자 윤서준입니다.

0개의 댓글