Project/[Project] Red Horse

[Backend] 정리 - 3

bonevillain 2023. 6. 15. 15:25

□ JSONField Test Case

이전 포스트에서 JSONField를 이용하여 2가지 형태를 이용했었습니다.

  • Case 1 : { "data" : [ "취미 1", "취미 2", "취미 3"] }
  • Case 2 : [ "취미 1", "취미 2", "취미 3"]

 

각각 테스트 케이스를 작성하여 발생했던 문제점을 보겠습니다.

 

테스트 케이스는 아래와 같이 작성되었습니다.

(핵심 코드만 보기 위해 로그인이나 이런 부차적인 것들은 제외시켰습니다.)

class UpdateProfileTestCase(APITestCase):
    def setUp(self):
        …

    def test_can_update_profile(self):
        data = {
            …
            "passions": {"data": ["축구", "야구"]},
        }

        response = self.client.patch(
            self.url,
            data,
        )

        self.assertEqual(
            response.status_code,
            status.HTTP_200_OK,
        )

기본 셋팅값을 이용하여 DRF를 사용할 때, 위의 테스트는 아래와 같이 에러가 발생하게 됩니다.

AssertionError: Test data contained a dictionary value for key 'passions', but multipart uploads do not support nested data. You may want to consider using format='json' in this test case.

 

대충 해석하면 multipart 데이터는 중첩된 데이터를 지원하지 않는다고 출력됩니다.

 

참고로 Case 2 형태로 테스트할 경우,

data = {
    …
    "passions": ["soccer", "tennis"],
}

invalid JSON으로 에러가 출력됩니다.

{'passions': [ErrorDetail(string='Value must be valid JSON.', code='invalid')]}


두 에러의 최종 원인은 잘못된 format 형태에 의해서 발생한 에러입니다. format 형태를 미지정 시, 기본값은 'multipart'로 지정됩니다.

https://www.django-rest-framework.org/api-guide/settings/#test-settings

 

Case 1의 에러 내용에 따라, 해결책으로 format='json'으로 지정해주면 두 케이스 모두 이상없이 테스트를 통과할 수 있습니다.

response = self.client.patch(
    self.url,
    data,
    format="json",
)

 

공식 문서에도 나오지만 여기서 조심해야할 부분은 위와 같이 하지 않고 content_type으로 "application/json"으로 지정 시, data는 JSON 문자열로 전환시켜야 테스트를 통과할 수 있습니다.

(format, content_type 둘 다 같이 설정하면 에러납니다.)

import json

response = self.client.patch(
    self.url,
    json.dumps(data),
    content_type="application/json",
)

 

공식 문서에 나온 format 인수에 대한 사용법

https://www.django-rest-framework.org/api-guide/testing/#using-the-format-argument

 

format을 일일이 지정하기 귀찮을 경우, 전역으로 지정하여 해결할 수도 있습니다.

 

settings.py

REST_FRAMEWORK = {
    …
    "TEST_REQUEST_DEFAULT_FORMAT": "json",
}

 

 

 

□ Case 2에서의 에러 원인 추적

is_valid 실패

# views.py
class MyProfileViewSet(viewsets.ViewSet):
    …

    def partial_update(self, request):
        profile = get_object_or_404(
            Profile, user=request.user.id
        )

        serializer = UpdateMyProfileSerializer(
            profile,
            data=request.data,
            partial=True,
        )

        if serializer.is_valid(): # is_valid는 False
            serializer.save()
            return Response(serializer.data)
        return Response(
            serializer.errors,
            status=status.HTTP_400_BAD_REQUEST,
        )

 

is_valid 내부

# rest_framework > serializers.py
class BaseSerializer(Field):
    def is_valid(self, *, raise_exception=False):
        …
        if not hasattr(self, '_validated_data'):
            try:
                self._validated_data = self.run_validation(
                    self.initial_data
                )
        …

 

run_validation 내부

# rest_framework > serializers.py
class Serializer(BaseSerializer, metaclass=SerializerMetaclass):
    def run_validation(self, data=empty):
        …
        value = self.to_internal_value(data)

 

to_internal_value 내부

# rest_framework > serializers.py
class Serializer(
    BaseSerializer,
    metaclass=SerializerMetaclass,
):
    def to_internal_value(self, data):
        …
        for field in fields:
            validate_method = getattr(
                self,
                'validate_' + field.field_name,
                None,
            )
            primitive_value = field.get_value(data) # 문제 지점
            try:
                validated_value = field.run_validation(
                    primitive_value
                )

위  fields 반복문 내의 JSONField 차례에서 primitive_value가 list 형태가 아닌 list의 마지막 string 값으로 초기화되기 때문에 validation 에러가 난 것입니다.

 

예상된 값

primitive_value = ["테니스", "축구", "야구"]

 

테스트 진행 시, 할당된 값

primitive_value = "야구"

 

get_value 내부

# rest_framework > fields.py
class JSONField(Field):
    default_error_messages = {
        'invalid': _('Value must be valid JSON.')
    }
    …

    def get_value(self, dictionary):
        if (html.is_html_input(dictionary)
            and self.field_name in dictionary
        ):
            ...
            class JSONString(str):
                def __new__(cls, value):
                    ret = str.__new__(cls, value)
                    ret.is_json_string = True
                    return ret
            return JSONString(dictionary[self.field_name])
        return dictionary.get(self.field_name, empty)

위의 'dictionary' 파라미터 변수는 views.py 에서 request.data와 동일한 부분입니다.

 

언급했던 format 형태에 따라 리턴되는 딕셔너리 객체 타입이 달라집니다.

통과한 테스트의 경우, dict 타입으로 되고

실패한 테스트의 경우, QueryDict 타입이 됩니다.

 

이렇게 타입이 달라지는 이유는 테스트 코드 중 self.client.patch(다른 메소드도 동일)의 format 인수에 따라 Parser가 달라지기 때문입니다.

 

format이 'multipart'인 경우, MultiPartParser를 사용하고

format이 'json'인 경우, JSONParser를 사용합니다.

 

소스코드를 보면 각 파서의 parser 메소드의 최종 리턴 타입을 보면 확인할 수 있습니다.

  • MultiPartParser의 parser 메소드 -> QueryDict 타입 값 리턴
  • JSONParser의 parser 메소드 -> dict 타입 값 리턴

 

전역적으로 설정된 기본 Parser 클래스

https://www.django-rest-framework.org/api-guide/settings/#default_parser_classes

 

참고한 자료

https://stackoverflow.com/questions/53206284/django-views-when-is-request-data-a-dict-vs-a-querydict

 

 

 

ERD Tool

대학교 다니면서 인턴쉽 과정에서 처음 ERD 툴을 다뤄봤었는데 그 때 당시 사용했던 것이 ERwin이었던 것으로 기억합니다. 그 이후로 작은 기업에서 MySQL Workbench를 맥북에 설치했던 것이 마지막이었던 것 같네요. 당시 기억으로는 데이터 타입을 커스텀하지 못하고 선택만 할 수 있었던 걸로 기억하는데 맞나 기억이 가물가물하네요.

 

그러다가 토이 프로젝트를 만들어보면서 정리할 겸 오랜만에 만들어봤습니다. 사용해봤던 것을 제외하고 새로운 것을 써볼까하고 구글링하면서 찾아보다가 ERDCloud도 추천을 많이 해서 사용해봤습니다.

 

가장 좋았던 두 가지는 사이트 이름답게 로컬에 별도로 저장하지 않아서 좋았고 다른 사람 모델링했던 것을 볼 수 있다는 점입니다.

하지만 동일한 테이블에 두 개 이상의 Foreign Key 지정하는 법을 아무리 찾아봐도 모르겠더라구요.

'좋아요 보낸이', '좋아요 받는이' 두 개 모두 auth_user에 대한 Foreign Key인데 둘 다 동시에 지정하는 법을 모르겠다.

 

그래서 무료 중에서 다른 툴을 찾아보는데 VSCode Extensions에서도 ERD 툴이 있더라구요.

그 중 제일 인기있는 ERD Editor로 선택하여 이것으로 최종 다 그렸습니다.

 

VSCode Extension ERD Editor

 

VSCode 안에서 코딩도 하고 ERD도 그리는 점이 제일 강점인 것 같습니다.

그리고 다른 툴과 다르게 축소하여 보면 아래와 같이 단순하게 볼 수 있는 점도 좋은 점이라고 볼 수 있겠네요.

Red Horse ERD

 

테이블 수가 많고 관계도가 복잡한 것을 그릴 때는 안좋을 것 같고 간단하게 그릴 때 좋으나 테이블을 조금만 움직여도

테이블 간 관계선이 좀 제멋대로 난리부르스 치는 것이 가장 거슬렸네요.

(관계선이 아름답게 그려지지는 않습니다. 관계선은 차라리 ERDCloud가 나은 것 같아요.)

 

Red Horse ERD

 

Backend 후기 끝.

'Project > [Project] Red Horse' 카테고리의 다른 글

[Backend] 정리 - 2  (0) 2023.06.14
[Backend] 정리 - 1  (0) 2023.06.13
[Backend] env 환경변수  (0) 2023.06.12
[Backend] 프로젝트 환경셋팅  (0) 2023.06.12
[Backend] API 설계  (0) 2023.06.09