날먹을 위한 몸부림/쉽지않음

보러가기 쉽지 않음 - django rest framework 시리얼라이저의 data 속성

프로그래밍하는 지팡이 2024. 12. 25. 22:57

django rest framework의 Serializer

django rest framework (이하 drf)에는 Serializer(시리얼라이저)라고 불리는 도구 집합이 있음

시리얼라이저는 rest api json 입력과 출력을 검증하는 역할을 함

다른 프레임워크에서 주로 쓰이는 pydantic의 역할과 같음

 

시리얼라이저의 사용 방법

클래스 기반 뷰에서 다음과 같이 사용하는걸 볼 수 있음

class HamsterSerializer(serializers.ModelSerializer):
    class Meta:
        model = models.Hamster
        fields = "__all__"


class HamsterViewSet(
    viewsets.GenericViewSet,
    viewsets.mixins.CreateModelMixin,
    viewsets.mixins.ListModelMixin,
    viewsets.mixins.RetrieveModelMixin,
):
    queryset = models.Hamster.objects.all()
    serializer_class = HamsterSerializer
    permission_classes = [permissions.AllowAny]

 

또는 직접 시리얼라이저를 사용한다면 다음과 같이 사용가능함

class HamsterCreateAPIView(generics.GenericAPIView):
    permission_classes = [permissions.AllowAny]
    serializer_class = HamsterSerializer

    def post(self, request):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        serializer.save()
        return Response(serializer.data, status=status.HTTP_201_CREATED)

 

위 구조는 실제로 CreateModelMixin와 거의 같음

CreateModelMixin의 시리얼라이저 사용

 

시리얼라이저의 data 속성

위 사용법을 보면 시리얼라이저에 데이터를 넘겨주고 검증한 뒤 data속성을 응답으로 사용하는걸 볼 수 있음

시리얼라이저의 data는 데이터가 저장된 멤버변수가 아니라 실제로는 property로 작성됨

시리얼라이저의 data 프로퍼티
베이스 클래스의 data 프로퍼티

 

코드를 살펴보면 실제 데이터는 self._data에 직렬화된 채로 저장됨

있다면 리턴하고 없다면 직렬화 작업 뒤 저장하는 작업을함

 

슈뢰딩거의 data 속성

앞의 배경은 끝났고 여기서부터 실제 본론임

이제 다음과 같은 시리얼라이저와 뷰를 작성해봄

class HamsterSerializer(serializers.ModelSerializer):
    name = serializers.CharField(read_only=True)
    age = serializers.IntegerField(read_only=True)

    class Meta:
        model = models.Hamster
        fields = ["name", "age"]


    def create(self, validated_data):
        random_hamster_name = random.choice(["햄!", "쥐!", "쮝!"])
        age = random.randint(1, 3)
        return models.Hamster.objects.create(name=random_hamster_name, age=age)


class HamsterViewSet(
    viewsets.GenericViewSet,
    viewsets.mixins.CreateModelMixin,
    viewsets.mixins.ListModelMixin,
    viewsets.mixins.RetrieveModelMixin,
):
    queryset = models.Hamster.objects.all()
    serializer_class = HamsterSerializer
    permission_classes = [permissions.AllowAny]

 

name, age를 읽기 속성(read_only)로 선언하고 생성을 시도할때는 랜덤으로 생성을 시도함

post로 호출할때마다 랜덤한 햄스터를 생성하는 api임

 

호출할 때는 api로 post요청만 보내면 되고 응답은 다음과 같음

{
  "name": "쥐!",
  "age": 1
}

 

예상한 대로 작동하고 아무 문제 없어보임

vscode의  watch = 상자에 들어간 고양이

vscode에 파이썬 개발환경을 설정하면 디버깅을 할 수 있음

기본 설정에는 현재 컨텍스트의 변수들, 등록해서 보고싶은 감시 리스트, 브레이크 포인트(이하 bp)를 볼 수 있음

 

디버깅중에 볼 수 있는 vscode의 디버깅 패널

이 중에서 이번에 확인해야할 부분은 watch임

보고싶은 변수를 등록해두고 확인할 수 있음

위와 같이 보고싶은 변수를 등록한 뒤 bp가 디버거에 의해서 잡히게 되면 해당 범위에서 변수의 값을 확인할 수 있음

만약 해당 범위에서 변수를 확인할 수 없다면 관련 메시지 같은게 표시됨

 

이제 시리얼라이저의 create함수에 bp를 걸고 디버거로 실행, api 요청을 해봄

bp에서 실행이 정상적으로 멈춘 모습

해당 컨텍스트에서 변수들을 확인할 수 있고

watch에서 등록한 변수의 값을 확인할 수 있음

 

그리고 응답을 확인하면 다음과 같이 잘못된걸 볼 수 있음

{}

 

테스트서버 응답도 확인하면 다음과 같음을 확인할 수 있음

"POST /api/hamster/hamsters/random/ HTTP/1.1" 201 2

 

그리고 bp를 제거 또는 비활성화 한 뒤에 실행해보면 정상적으로 실행되는걸 볼 수 있음

 

당신이 watch탭으로 특정 시점에 data를 관찰하려고 하면 data는 잘못된 또는 의도하지 않은 값이 들어간걸 확인할 수 있음

 

제 고양이가 왜 죽는건가요

당신의 data 고양이는 슈뢰딩거의 고양이마냥 관찰할때 상태가 변할 수 있음

그건 watch가 마법같은 방법이 아니라 진짜 파이썬을 실행하듯이 평가를 하기 때문으로 보임

 

확인을 위해서 외부 라이브러리도 디버깅할 수 있도록 디버깅 옵션을 수정해봄

디버거를 붙일때 "justMyCode": false 옵션을 추가해서 외부라이브러리도 포함해서 디버깅할 수 있음

 

"justMyCode": false가 추가된 디버깅 옵션

 

그리고 저렇게 설정한 뒤 다시 디버거를 실행하고 프레임워크의 코드에 bp를 걸면 정상적으로 실행이 멈춤

그런데 data 프로퍼티에 bp를 걸고 실행하면 bp가 잡히긴 하는데 우리가 원하는 타이밍에 잡히지는 않음

 

처음에 예상했던 타이밍은 create함수에서 걸어둔 bp가 멈추기 전에 data 프로퍼티에서 bp가 잡히길 원했음

그런데 data에 걸어뒀던 bp는 잡히지 않고 watch 탭에서 이미 빈 값으로 나와있음

 

그냥 미친척하고 라이브러리에 print출력을 추가하고 확인하면 실제로 호출되는걸 확인할 수 있음

미쳐버린 모습

 

중앙 하단의 call data property를 볼 수 있음

 

data 프로퍼티에는 bp가 잡히지 않고 바로 실행된 모습을 확인할 수 있음

이제 여기까지 오면 원인 분석은 다 끝남

 

bp를 걸면 다음과 같은 순서로 실행됨

watch에 data를 등록해두기만 해도 변수가 평가되면서 실행됨

검증 데이터와 인스턴스가 비어있는 상태에서 평가됨

빈 데이터가 등록됨(self._data 에 빈 데이터)

이후 시리얼라이저의 create함수가 리턴한 값이 instance에 저장되더라도 이미 _data 값은 생성된 뒤라서 무의미함

빈 data 완성

 

bp를 걸지 않는다면 create함수 이후에 instance에 저장되고 다음 data호출에서 정상적으로 계산되서 저장됨

시리얼라이저의 save 함수 내부에서 create결과가 저장되는 모습

 

CreateModelMixin의 create함수

정상적인 흐름이라면 post 요청

create함수 호출

검증

perform_create

시리얼라이저.save -> save 내부의 create함수 호출 -> 시리얼라이저의 instance 멤버 변수에 저장

그리고 20번째 줄의 serializer.data에서 처음으로 계산됨

 

위 순서대로라면 정상적으로 작동함

 

또 다른 이유

인스턴스가 없더라도 잘 작동할 수 있음

생성된 인스턴스가 없어도 검증데이터만 있으면 작동할 수 있음

인스턴스가 없다면 검증 데이터에서 데이터를 생성가능함

그런데 이 예시에서는 post 데이터 바디가 요청시에는 비어있고 생성된 뒤 응답에만 들어가있는 예시라서 그런거임

몇가지 회피 방법이 존재하는데 그건 님이 코드 보면서 ㄱㄱ혓

 

요약

watch에 변수를 등록해두면 실행하면서 평가됨

시리얼라이저의 data는 단순 멤버변수가 아니고 특정한 작업을 같이하는 프로퍼티임

watch에 등록되면서 일부 조건(요청 바디가 비어있고 응답에만 쓰임)이 만족되면 의도치않은 값을 가질 수 있음

 

참고

사실 이런 코드는 주변에서 보기 쉽지않음

그냥 밥먹으면서 보는 글로 보셈

 

근데 코드가 보기 힘들다고했지 도구들은 있을 수 있으니 조심

ex) 특정 상태를 기록하는 로거

 

django rest framework 열어보면 위 코드들 다 있으니 직접 확인하는 것도 좋음(djangorestframework==3.15.2)