안녕하세요 피처링 백엔드 엔지니어 제리입니다.

오늘은 피처링 2.0 서비스의 속도 개선을 위해 진행한 여정중 Django의 직렬화 개선 여정을 함께 나누고자 합니다. 이야기의 중심에는 예상치 못한 곳에서 찾은 직렬화 과정의 역설적인 영향이 있습니다. 그래서 이번 글에서는 어떻게 이 문제를 해결하고 더욱 빠른 서비스를 제공할 수 있었는지에 대해 소개해 드리겠습니다.

피처링 2.0 서비스를 개발하면서 매주 목요일마다 진행되는 코드리뷰에서 우리 팀은 주로 속도 개선에 대한 이야기를 나누게 되었습니다.

데이터베이스 쿼리를 개선하는 방법, 캐시 처리를 효율적으로 하는 방법 등에 대한 논의가 주를 이루었습니다. 그러나 우리는 예상치 못한 곳에서 문제를 발견하게 되었습니다. 직렬화 과정이 서비스의 속도를 저해하고 있던 것이죠.

(날카로운 지적을 해주신 해리님께 감사드립니다..!)

이 문제를 발견한 것은 우리 팀에게 정말 큰 충격이었습니다. 우리는 직렬화를 사용해 데이터를 저장하고 전송하는 일상적인 작업으로 여겨왔기 때문입니다. 그렇기에 직렬화 과정이 성능 저하의 주범이 될 줄은 상상조차 못했던 것이죠. 근본적인 속도 개선을 위해 어떤 방식으로 진행했는지 살펴보겠습니다.

직렬화(Serialization)란?

직렬화(Serialization)란, 객체나 데이터 구조를 저장하거나 전송하기 위해 일련의 바이트로 변환하는 과정을 말합니다. 직렬화를 통해 객체나 데이터 구조를 파일로 저장하거나, 네트워크를 통해 전송할 수 있습니다.

직렬화는 일반적으로 객체를 바이트 스트림으로 변환하고, 이후에 다시 객체로 역직렬화(Deserialization)됩니다. 직렬화된 데이터는 JSON, XML, Protocol Buffers, MessagePack 등 다양한 형식으로 저장될 수 있으며, 이러한 형식을 사용하여 데이터를 직렬화하고 역직렬화하는 라이브러리가 많이 존재합니다.

Django에서는 RESTful API를 만들기 위해 데이터를 직렬화하는데, 이 때 Django REST Framework의 Serializer를 사용하여 데이터를 직렬화합니다. 이를 통해 Django 모델의 인스턴스나 Python 객체를 JSON이나 XML 형식으로 변환하여 API에서 제공할 수 있습니다.

Django에서 Model의 데이터를 직렬화하는 방법.

1. ModelSerializer (모델 시리얼라이저)

ModelSerializer는 Django REST 프레임워크에서 제공하는 클래스로, 모델에 정의된 필드와 관련된 시리얼라이저 클래스가 자동으로 생성됩니다. 이를 사용하면 일반적으로 필드를 정의하거나 직접 시리얼라이저 클래스를 작성해야 할 필요 없이, 모델에 정의된 필드와 관련된 시리얼라이저 클래스가 자동으로 생성됩니다. ModelSerializer를 사용하면 다음과 같은 방법으로 데이터를 직렬화할 수 있습니다.

from rest_framework import serializers
from myapp.models import MyModel

class MyModelSerializer(serializers.ModelSerializer):
    class Meta:
        model = MyModel
        fields = ['field1', 'field2', 'field3']

my_model = MyModel.objects.first()
serializer = MyModelSerializer(my_model)
serialized_data = serializer.data

2. Serializer (정규 시리얼라이저)

Serializer는 ModelSerializer와 유사하지만, 직접 시리얼라이저 클래스를 작성해야 합니다. 이를 사용하면 ModelSerializer에서 지원하지 않는 기능을 추가할 수 있습니다. Serializer를 사용하면 다음과 같은 방법으로 데이터를 직렬화할 수 있습니다.

from rest_framework import serializers
from myapp.models import MyModel

class MyModelSerializer(serializers.Serializer):
    field1 = serializers.CharField()
    field2 = serializers.IntegerField()
    field3 = serializers.DateField()

my_model = MyModel.objects.first()
serializer = MyModelSerializer(my_model)
serialized_data = serializer.data

3. Simple Function Serializer

Serializer와 ModelSerializer와 달리, 함수 기반의 시리얼라이저는 클래스가 아닌 함수를 사용하여 데이터를 직렬화합니다. 이를 사용하면 Serializer나 ModelSerializer에서 제공하지 않는 맞춤형 로직을 구현할 수 있습니다. 함수 기반의 시리얼라이저를 사용하면 다음과 같은 방법으로 데이터를 직렬화할 수 있습니다.

from rest_framework import serializers
from myapp.models import MyModel

def my_model_serializer(my_model):
    return {
        'field1': my_model.field1,
        'field2': my_model.field2,
        'field3': my_model.field3,
    }

my_model = MyModel.objects.first()
serialized_data = my_model_serializer(my_model)

피처링의 인플루언서 데이터

현재 피처링에서는 사용자가 여러 가지 필터 조건을 선택할 때, 엘라스틱을 활용하여 빠르게 인플루언서를 검색해오고 있습니다. 이를 위해 사용자가 선택한 필터 조건에 맞게 엘라스틱 쿼리를 생성하고, 해당 조건에 맞는 인플루언서 데이터를 엘라스틱의 인덱스에서 가져옵니다.

이후에는 DB에 해당 인플루언서 데이터를 마이그레이션하는 과정을 거치고, 직렬화 과정을 거쳐 클라이언트 사이드에 전달합니다. 이를 통해 사용자는 간편하게 필요한 조건에 맞는 인플루언서를 검색하고, 원하는 데이터를 빠르게 받아올 수 있습니다.

처음에는 Django Rest Framework에서 제공하는 ModelSerializer를 사용하여 다음과 같이 구현하였습니다.

# serializers.py
class InfluencerSerializer(serializers.ModelSerializer):
    class Meta:
        model = Influencer
        fields = "__all__"

# views.py
class InfluencerListView(FeaturingListAPIView):
    def get(self, *args, **kwargs):
        # ...(중략)
        queryset = Influencer.list(influencer_pk_list=parsed_elastic_response)
        serialized_data = InfluencerSerializer(queryset, many=True).data
        return BaseResponse(serialized_data)

문제 정의

저희 서비스는 모든 API가 3초 이내에 응답하는 것을 목표로 하고 있습니다. 하지만 인플루언서 데이터를 불러오는 API에서는 3초 이상의 시간이 소요되어 문제가 발생했습니다. 디버깅을 통해, 데이터 직렬화 과정에서 대부분의 시간이 소요된다는 것을 발견하였습니다.

Tom Christie For performance critical views you might consider dropping the serializers entirely and simply use .values() in your database queries.

Django Rest Framework의 창시자인 Tom Christie는 한 기사에서, 성능이 중요한 뷰의 경우에는 직렬화를 Drop하고, .values()를 통해 직접 데이터베이스에 접근하는 방식을 조언하였습니다. 여기서 힌트를 얻어, 앞서 소개드린 3가지 직렬화 방법과, 각각에서 읽기전용 필드 적용시를 고려하여 가장 좋은 방법이 무엇인지 직접 성능을 테스트하여 결정하고자 했습니다.

다음은 성능테스트에 사용된 시리얼라이저 예시 코드입니다.

1. ModelSerializer

class InfluencerModelSerializer(serializer.ModelSerializer):
    class Meta:
        model = InfluencerPlatform
        fields = "__all__"

2. ModelSerializer(read_only)

class InfluencerModelReadOnlySerializer(serializer.ModelSerializer):
    class Meta:
        model = InfluencerPlatform
        fields = [
					"platform_code",
					"platform_pk",
					"follower_count",
					"main_category",
					"sub_category",
					"cpv",
					"cpr",
					"reach_score",
					"audience_estimate_age",
					"audience_estimate_gender"
					# ...(중략)
				]
				read_only_fields = fields

3. Serializer

class InfluencerRegularSerializer(serializer.Serializer):
		platform_code = serializer.Charfield()
		platform_pk = serializer.Charfield()
		follower_count = serializer.IntegerField()
		main_category = serializer.Charfield()
		sub_category = serializer.Charfield()
		cpv = serializer.FloatField()
		cpr = serializer.FloatField()
    reach_score = serializer.FloatField()
		audience_estimate_age = serializer.FloatField()
		audience_estimate_gender = serializer.Charfield()

4. Serializer(read_only)

class InfluencerRegularSerializer(serializer.Serializer):
		platform_code = serializer.Charfield(read_only=True)
		platform_pk = serializer.Charfield(read_only=True)
		follower_count = serializer.IntegerField(read_only=True)
		main_category = serializer.Charfield(read_only=True)
		sub_category = serializer.Charfield(read_only=True)
		cpv = serializer.FloatField(read_only=True)
		cpr = serializer.FloatField(read_only=True)
    reach_score = serializer.FloatField(read_only=True)
		audience_estimate_age = serializer.FloatField(read_only=True)
		audience_estimate_gender = serializer.Charfield(read_only=True)

5. Simple Function Serializer

class InfluencerSimpleFunctionSerializer:
		def data(influencer_queryset: QuerySet):
				influencer_list = []
				for instance in influencer_queryset:
						data = {
							"platform_code": instance.platform_code,
							"platform_pk": instance.platform_pk,
							"follower_count": instance.follower_count,
							"main_category": instance.main_category,
							"sub_category": instance.sub_category,
							"cpv": instance.cpv,
							"cpr": instance.cpr,
							"reach_score": instance.reach_score,
							"audience_estimate_age": instance.audience_estimate_age,
							"audience_estimate_gender": instance.audience_estimate_gender,
						}
				influencer_list.append(data)
				return influencer_list

테스트

다음은 200개의 인플루언서 인스턴스를 직렬화시 측정된 속도입니다.

시리얼라이저 속도(단위: 초)
ModelSerializer 4.415959
ModelSerializer(read_only_fields) 2.57322
Serializer 2.227293
Serializer(read_only) 2.229535
Simple Function Serializer 1.926606

먼저, ModelSerializer에 read_only_fields를 추가하면 데이터베이스에 유효성 검증의 과정이 필요 없기 때문에 직렬화 작업에서 불필요한 작업을 하지 않아 속도가 개선됩니다. 이로 인해 약 40%의 성능 향상을 확인할 수 있습니다.

정규 Serializer를 사용할 때도 읽기전용 ModelSerializer보다 성능이 개선되는 것을 확인했습니다. 그러나 읽기전용 필드를 추가했을 때와 큰 차이가 없는 것으로 나타났으며, 미세하게 성능이 떨어진 것을 확인할 수 있습니다.

Simple Function Serializer를 사용하면 성능 개선이 더욱 확실해집니다. 이 방법은 ModelSerializer에 비해 약 57%의 성능 향상을 보여줍니다. ModelSerializer는 코드의 양을 줄일 수 있는 장점이 있지만, 성능적으로 큰 차이가 있음을 확인할 수 있습니다.


결론

이번 글에서는 피처링 서비스의 속도 개선을 위한 Serializer 개선 기법에 대해 소개했습니다. 하지만, 모든 상황에서 함수 기반 Serializer를 사용하는 것이 최적화된 기법은 아닙니다. 함수 기반 Serializer는 개발자가 직접 구현해야 하기 때문에, 코드가 잘못 작성될 경우 성능 저하의 원인이 될 수 있습니다. 또한, ModelSerializer가 자동으로 제공하는 다양한 기능들을 직접 구현해야 하기 때문에 코드 양도 증가할 수 있습니다.

Django에서 API 요청의 전반적인 응답 시간을 개선하는 데 유효한 방법들은 DB 접근을 개선하기 위해 select_related나 prefetch_related와 같은 메소드를 사용하는 것이나 캐싱 등 다양합니다. 그러나 모든 방법들에는 Trade-off가 있으므로 상황에 맞게 코드를 개선하시길 바랍니다.

지금까지 피처링 2.0 서비스의 속도 개선을 위한 여정중 Django의 직렬화에 대해 이야기했습니다. 직렬화라는 예상치 못한 곳에서의 문제를 발견하고 해결하는 과정에서 우리 팀은 많은 것을 배웠고, 더 나은 서비스를 제공할 수 있게 되었습니다. 앞으로도 우리는 성능 개선에 주목하며 사용자들에게 더 나은 경험을 제공하기 위해 노력할 것입니다. 감사합니다!