콘텐츠로 이동

왜 ModelSchema를 쓰지 않나요?#

2024 iThome 철인 대회

이 글은 Django Ninja 입문 가이드의 15번째 글이에요.

Django API 응답은 대개 Model 객체(즉 DB의 데이터) 내용을 일정 부분 선별하고 가공하는 과정을 거쳐요.

예를 들어 "단일 글 정보 가져오기" API는 사실 Post 객체에서 필요한 필드들을 고른 다음, 이를 직렬화(Serialize)하는 것이죠.

이 과정에서 우리는 모델 객체를 어떻게 API의 응답 구조로 변환할지, 그리고 어떻게 코드의 유지보수성과 유연성을 유지할지 고민해야 해요.

이를 위해 Django REST framework(이하 DRF)는 매우 실용적인 "특별 맞춤" 직렬화기(Serializer)인 ModelSerializer를 제공하며, 이는 DRF 개발자라면 반드시 익혀야 할 핵심 기능이라 할 수 있어요.

Django Ninja에도 이와 비슷한 역할을 하는 ModelSchema가 있긴 하지만, 저에게는 계륵과 같은 존재라 거의 써본 적이 없어요.

이러한 차이는 두 프레임워크가 가진 핵심 설계 철학의 다름에서 비롯된 것이 틀림없어요.

이전 3편에서 두 프레임워크 간의 기능적인 주요 차이점을 논의한 적이 있었죠. 이번 글에서는 대표적인 주제인 "Django 모델 객체의 직렬화"를 통해 "왜 제가 DRF보다 Django Ninja를 더 좋아하는지" 설명해 드릴게요.

GitHub 예제 프로젝트#

👉 Django-Ninja-Tutorial


ModelSerializer의 장점#

DRF의 ModelSerializer는 매우 강력한 도구예요. Django 모델을 API가 요구하는 데이터 구조(즉, 직렬화기)로 자동 변환해 주어, "직렬화기에 필드를 정의하는" 과정을 크게 단축시켜 줘요.

참고로 DRF의 직렬화기는 Django Ninja에서 사용하는 Schema와 대동소이한 개념으로, 둘 다 데이터 검증과 직렬화에 사용돼요.

만약 "단일 글 정보 가져오기" API의 응답을 ModelSerializer를 이용해 다시 작성한다면 다음과 같은 모습일 거예요:

from rest_framework import serializers

# Author 직렬화기
class AuthorSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ['id', 'username', 'email']

# Post 직렬화기
class PostSerializer(serializers.ModelSerializer):
    author = AuthorSerializer()  # 중첩된 Author 직렬화기

    class Meta:
        model = Post
        fields = ['id', 'title', 'content', 'author', 'created_at', 'updated_at']

보시다시피 ModelSerializer를 사용하면 아주 적은 양의 코드만으로도 직렬화기를 완성할 수 있어, 수동 설정에서 오는 반복과 번거로움을 피할 수 있어요.


ModelSerializer의 숨은 위험성#

하지만 이렇게 편리한 이면에는 어느 정도의 위험성도 도사리고 있어요.

필드를 직접 정의할 필요가 없기 때문에, ModelSerializer가 Django Model 필드를 직렬화기 필드로 변환하는 수많은 암묵적 변환(Implicit Conversion)을 알아서 대신 처리해 버리거든요.

왜 "암묵적"이라고 할까요? 자동으로 변환된 직렬화기 필드들의 타입, 속성, 읽기 전용(read_only) 여부 등의 세부 사항들을 여러분이 명확하게 파악하지 못할 수도 있기 때문이에요.

바꿔 말하면, ModelSerializer는 필드를 자동으로 생성할 뿐만 아니라, 그 필드의 타입, 속성, 그리고 속성의 매개변수들까지도 스스로 추론(infer)한다는 거죠.

예시를 통한 설명#

말로만 설명하면 좀 추상적이라, DRF를 경험해 보지 않은 독자분들은 잘 와닿지 않으실 수 있어요. 간단한 예를 들어볼게요:

from django.db import models

class Person(models.Model):
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=30)
이것은 Django 공식 문서에서 가져온 아주 기초적인 Django Model이에요.

이 모델은 first_namelast_name이라는 두 개의 필드를 가지고 있고, 코드에는 나타나지 않았지만 실제로는 Django가 자동으로 만들어준 id 필드도 함께 존재해요.

ModelSerializer가 부리는 "마법"#

ModelSerializer를 사용하면 이 직렬화기를 다음과 같이 정의할 수 있어요:

from rest_framework import serializers
from .models import Person

class PersonSerializer(serializers.ModelSerializer):
    class Meta:
        model = Person
        fields = ['id', 'first_name', 'last_name']

코드는 무척 단순하지만, 그 뒤에 숨겨진 "마법"은 생각보다 엄청나요.

왜냐하면 실제 직렬화기와 그 필드들은 다음과 같이 구성되기 때문이에요:

from rest_framework import serializers

class PersonSerializer(serializers.Serializer):
    id = serializers.IntegerField(read_only=True)
    first_name = serializers.CharField(max_length=30)
    last_name = serializers.CharField(max_length=30)

    def create(self, validated_data):
        return Person.objects.create(**validated_data)

    def update(self, instance, validated_data):
        instance.first_name = validated_data.get('first_name', instance.first_name)
        instance.last_name = validated_data.get('last_name', instance.last_name)
        instance.save()
        return instance
어떤가요? 조금 놀랍지 않나요?

암묵적 변환이 수행하는 수많은 일들#

그중에서도 id 필드에는 read_only=True 속성이 자동으로 붙고, first_namelast_name에는 max_length=30이라는 제약이 스르륵 추가됐어요.

이건 어디까지나 Django Model의 설계와 필드 매개변수가 비교적 단순한 상황에서의 이야기예요. Model의 필드들이 더 복잡해지면 ModelSerializer가 부리는 "마법" 역시 훨씬 더 복잡해지죠.

이 마법의 이면에는 다양한 변환 로직들이 숨겨져 있어서, 개발자는 어떤 경우에는 이 "숨겨진 규칙들"을 반드시 파악해야만 해요. 왜냐하면 프레임워크의 자동 추론이 때로는 여러분의 의도와 맞지 않을 수 있고, 그럴 때면 결국 수동으로 코드를 덮어써야(override) 하니까요.

요약하자면, 자동 추론과 변환 덕분에 하나하나 설정해야 하는 수고는 덜었지만, 특정한 세부 설정을 바꾸고 싶거나 변환 과정의 세세한 부분을 알고 싶을 때면 이런 암묵적인 동작들이 오히려 여러분을 혼란스럽게 만들 수 있다는 거예요.

마법의 대가#

실제 개발 현장에서는, 이러한 암묵적 변환 "마법" 때문에 개발자가 변환 과정에 대한 이해와 통제력을 잃어버리는 일이 잦아요. 직렬화된 결과물이 여러분이 상상했던 것과 미묘하게 다를 때가 분명 생길 테니까요!

그럴 때마다 우리는 이 필드 변환이 내부적으로 어떻게 일어나는지 확인하기 위해 DRF 공식 문서를 뒤적거려야 하지만, 모든 세부 사항이 친절하고 명확하게 적혀 있는 것도 아니죠.

결과적으로, 특히 복잡한 API를 다루는 개발자 입장에서는 학습 부담과 유지보수 비용이 눈에 띄게 증가할 수밖에 없어요.

이게 바로 제가 겪었던 경험이에요!

DRF를 2년이나 다뤘음에도, 직렬화와 관련된 문제에 부딪힐 때면 여전히 문서를 다시 찾아봐야 하는 일이 아주 흔했어요.


ModelSchema#

Django Ninja의 ModelSchema는 ModelSerializer와 비교하면 아주 "소박하고 밋밋한" 느낌이에요.

어째서냐고요? 공식 문서에 있는 예시를 한번 볼까요:

from django.contrib.auth.models import User
from ninja import ModelSchema

class UserSchema(ModelSchema):
    class Meta:
        model = User
        fields = ['id', 'username', 'first_name', 'last_name']

# 다음과 같은 schema를 생성할 거예요:
#
# class UserSchema(Schema):
#     id: int
#     username: str
#     first_name: str
#     last_name: str
#

이걸 소박하다고 표현한 이유는, 이것이 오로지 필드의 "타입"만을 자동으로 변환하고 정의해 주기 때문이에요. max_length 같은 다른 세부 사항들은 온전히 Field를 이용해서 직접 설정해야 해요 — ModelSchema는 그 부분까지 해주지 않아요.

반면 앞서 이야기했듯 DRF의 ModelSerializer는 "더 많은 일"을 해주죠.

ModelSchema의 자동 변환이 이토록 단순명료함에도 불구하고, 어째서 저는 여전히 이 방식을 추천하지 않는 걸까요? 그 이유에는 두 가지가 있어요.

그리고 그중 첫 번째 이유가 바로 이 글의 제목인 "내가 왜 Django Ninja를 더 좋아하는지"에 대한 직접적인 대답이기도 해요.


이유 1: 낮은 결합도 + 암묵적인 것보다 명시적인 것이 낫다#

Django Ninja는 API 구조에 대한 개발자의 완전한 장악력을 중요하게 생각하는 반면, DRF는 편의성과 고도로 통합된 도구들을 제공하는 데 초점을 맞추고 있어요.

이러한 차이는 두 프레임워크가 Django 모델의 직렬화를 어떻게 바라보는지에도 고스란히 반영되어 있으며, 이 프레임워크들을 사용할 때 개발자의 스타일이나 사고방식에도 큰 영향을 줘요.

Django REST framework와 Django의 높은 결합도 (Coupling)#

살펴보면, DRF는 사실상 "Django를 위해 고도로 맞춤 제작된" API 개발 도구라고 해도 과언이 아니에요.

이러한 긴밀한 결합은 편리함을 가져다주긴 하지만, 동시에 DRF가 Django의 내부 구조와 기능에 크게 의존한다는 것을 의미하기도 해요. Generic views이든, 오늘 살펴본 ModelSerializer이든 모두 마찬가지죠.

결합도가 높을 때의 장점은 여러분이 해야 할 일이 줄어든다는 것이고, 그 대가는 자신이 도대체 무슨 일을 하고 있는지 아주 잘 알고 있어야 한다는 것이에요.

명시적인 것이 암묵적인 것보다 낫다#

DRF와 비교할 때, Django Ninja와 Django 사이의 결합도는 훨씬 낮아요.

제가 보기에 Django Ninja는 "암묵적인 것보다 명시적인 것(Explicit is better than implicit)"이라는 철학을 더 선호해요. Django Ninja의 Schema 정의는 Pydantic을 바탕으로 하며, 이는 개발자가 입력이든 출력이든 간에 모든 필드를 명시적으로 정의하도록 요구해요.

조금 번거로울 수는 있지만, 그로 인해 얻는 이점은 아주 명확해요.

명시적인 방식의 두 가지 큰 장점#

첫째로, Schema를 수동으로 정의하면 개발자는 데이터 구조에 대한 절대적인 통제권을 갖게 돼요. 숨겨진 규칙이나 몰래 이루어지는 작업 없이, 모든 것이 투명하게 드러나죠.

둘째로, 이런 접근 방식은 모델 계층(Model layer)과 API 계층(API layer) 사이의 결합도를 효과적으로 낮춰줘요. 실제 프로젝트에서 요구사항이 바뀌면 모델 설계도 변경되어야 할 때가 많은데, 이 변경이 API에 직접적인 영향을 주어서는 안 되거든요.

결론적으로 Django Ninja는 Schema 중심의 통제를 강조함으로써, API 설계의 안정성과 유연성을 높이고 데이터 흐름에 대한 완벽한 통제권을 개발자에게 부여해요.


이유 2: 더 훌륭하고 가독성 높은 API 문서#

18편에서는 Schema 필드 설정이 API 문서에 미치는 영향에 대해 아주 깊이 있게 다룰 거예요.

간단히 말씀드리자면, 만약 ModelSchema를 사용한다면 렌더링 되어 나오는 API 문서 역시 상당히 빈약하고 심심할 거예요.

이건 제가 지향하는 명확하고 이해하기 쉬운 API 문서의 기준과 결코 맞지 않아요.


맺음말#

이전 글에서 언급했던 source= 매개변수처럼 직관적이고 우아한 방식 등, Django REST framework가 매우 유용하고 세심한 설계들을 가지고 있다는 점은 부정할 수 없는 사실이에요.

하지만 Django Ninja는 개발자가 가능한 한 모든 필드를 직접 정의하도록 권장하여, 모델과 API 계층 간의 결합을 최소화하려고 해요. 이는 암묵적인 동작에서 발생할 수 있는 잠재적인 문제들을 예방해 주며, 파이썬 철학인 "암묵적인 것보다는 명시적인 것이 낫다"와도 깊이 맞닿아 있어요.

바로 이것이 제가 Django Ninja를 더 선호하는 진짜 이유예요.

Django Ninja가 추구하는 이러한 명확성 덕분에, 저는 API를 개발하고 유지보수할 때 대부분의 상황에서 마음이 훨씬 더 가벼워짐을 느껴요.