
Django REST Framework(DRF)를 사용하다 보면 자연스럽게 many=True 옵션을 사용하게 됩니다.
# 단일 객체 직렬화
serializer = UserSerializer(user)
# 리스트 객체 직렬화
serializer = UserSerializer(users, many=True)
겉보기에는 단순한 파라미터 하나의 차이지만, 내부적으로는 완전히 다른 클래스가 동작합니다. 오늘은 DRF의 many 옵션이 어떻게 구현되어 있는지, 그리고 이 설계가 왜 우아한지 살펴보겠습니다.
다른 개발자가 작성한 코드를 분석하던 중 이런 코드를 만났습니다.
class LocationListApi(APIView):
class OutputSerializer(serializers.OutputSerializer):
id = serializers.IntegerField()
name = serializers.CharField()
# ... 기타 필드들
def get(self, request):
locations = location_list(filters=filter_serializer.validated_data)
data = self.OutputSerializer(locations, many=True).data # 이 many는 뭘까?
OutputSerializer를 따라가보니 이 클래스에는 __init__ 메서드가 없었습니다. 그렇다면 many 파라미터는 어디서 처리되는 걸까요?
상속 체인을 따라 올라가니 BaseSerializer 클래스를 발견했습니다.
class BaseSerializer(Field):
def __init__(self, instance=None, data=empty, **kwargs):
self.instance = instance
if data is not empty:
self.initial_data = data
self.partial = kwargs.pop('partial', False)
self._context = kwargs.pop('context', {})
kwargs.pop('many', None) # many 옵션이 여기서 제거된다!
super().__init__(**kwargs)
def __new__(cls, *args, **kwargs):
# many=True일 때 ListSerializer를 반환하는 핵심 로직
if kwargs.pop('many', False):
return cls.many_init(*args, **kwargs)
return super().__new__(cls, *args, **kwargs)
여기서 __new__와 __init__을 함께 쓰면서 흥미로운 부분이 나옵니다.
__init__에서 many 제거: kwargs.pop('many', None)로 many 옵션을 제거합니다__new__에서 조건부 객체 생성: many=True이면 many_init을 호출해 다른 객체를 반환합니다__new__와 __init__의 차이__new__: 객체가 실제로 생성되기 전에 호출되어 어떤 객체를 만들지 결정__init__: 객체가 생성된 후 호출되어 인스턴스를 초기화# UserSerializer(users, many=True) 호출 시:
# 1. __new__ 메서드 실행
def __new__(cls, *args, **kwargs):
if kwargs.pop('many', False): # many=True 감지
return cls.many_init(*args, **kwargs) # ListSerializer 생성!
return super().__new__(cls, *args, **kwargs) # 일반 Serializer 생성
# 2. many=True인 경우 __init__은 건너뛰고 ListSerializer 반환
# 3. many=False인 경우 일반적인 __init__ 실행
이것이 Abstract Factory Pattern의 구현입니다. 사용자는 동일한 API를 사용하지만, 내부적으로는 조건에 따라 완전히 다른 객체 패밀리가 생성됩니다.
many_init 메서드@classmethod
def many_init(cls, *args, **kwargs):
# 1단계: ListSerializer 전용 인자들 분리
list_kwargs = {}
for key in LIST_SERIALIZER_KWARGS_REMOVE: # ('allow_empty', 'min_length', 'max_length')
value = kwargs.pop(key, None)
if value is not None:
list_kwargs[key] = value
# 2단계: 개별 아이템 처리용 Serializer 생성
list_kwargs['child'] = cls(*args, **kwargs)
# 3단계: 공통 인자들 ListSerializer에 전달
list_kwargs.update({
key: value for key, value in kwargs.items()
if key in LIST_SERIALIZER_KWARGS
})
# 4단계: Strategy Pattern - Meta 설정에 따른 ListSerializer 선택
meta = getattr(cls, 'Meta', None)
list_serializer_class = getattr(meta, 'list_serializer_class', ListSerializer)
return list_serializer_class(*args, **list_kwargs)
이 메서드는 세 가지 중요한 역할을 합니다.
min_length, max_length 등)context, partial 등)list_kwargs['child'] = cls(*args, **kwargs)
개별 아이템을 처리할 Serializer를 child로 설정합니다.
meta = getattr(cls, 'Meta', None)
list_serializer_class = getattr(meta, 'list_serializer_class', ListSerializer)
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = '__all__'
# 기본 ListSerializer 사용
class BulkCreateListSerializer(serializers.ListSerializer):
def create(self, validated_data):
return User.objects.bulk_create([
User(**item) for item in validated_data
])
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = '__all__'
list_serializer_class = BulkCreateListSerializer # 커스텀 전략
이제 UserSerializer(data=users, many=True)를 호출하면 자동으로 BulkCreateListSerializer가 사용됩니다!
getattr의 안전한 속성 접근# 일반적인 속성 접근
user.name # 속성이 없으면 AttributeError 발생
# getattr을 사용한 안전한 접근
getattr(user, 'name', 'Unknown') # 속성이 없으면 기본값 반환
DRF에서는 이를 활용해 유연한 설정을 제공합니다.
meta = getattr(cls, 'Meta', None) # Meta 클래스가 없어도 안전
list_serializer_class = getattr(meta, 'list_serializer_class', ListSerializer) # 설정이 없으면 기본값
이 구현은 여러 디자인 패턴이 조합된 우아한 설계입니다
# 조건에 따라 관련된 객체 패밀리 전체를 변경
UserSerializer(data=single_user) # → 단일 직렬화 체계
UserSerializer(data=users, many=True) # → 리스트 직렬화 체계 (ListSerializer + child)
이는 단순히 하나의 객체를 생성하는 Factory Method가 아니라, 직렬화 처리 체계 전체를 바꾸는 Abstract Factory Pattern입니다
# many=False: 단일 처리 체계
UserSerializer 인스턴스
# many=True: 리스트 처리 체계
ListSerializer(
child=UserSerializer, # 개별 아이템 처리기
# + 리스트 전용 validation, serialization 로직
)
# Meta 설정에 따라 다른 ListSerializer 사용
class UserSerializer(serializers.ModelSerializer):
class Meta:
list_serializer_class = CacheableListSerializer # 캐싱 전략
# list_serializer_class = BulkListSerializer # 벌크 처리 전략
# list_serializer_class = AsyncListSerializer # 비동기 전략
many_init 메서드는 고정된 흐름을 제공하면서, 각 단계에서 커스터마이징 지점을 제공합니다.
# UserSerializer(data=[user1, user2], many=True) 호출 시
# 1. Abstract Factory Pattern 작동
BaseSerializer.__new__()
├─ many=True 감지
├─ kwargs에서 'many' 제거
└─ UserSerializer.many_init() 호출
# 2. Strategy Pattern 작동
UserSerializer.many_init()
├─ getattr(cls, 'Meta', None) → Meta 클래스 탐색
├─ getattr(meta, 'list_serializer_class', ListSerializer) → 전략 선택
├─ 인자 분배 및 정리
├─ child = UserSerializer(처리된 kwargs) → 개별 처리기 생성
└─ SelectedListSerializer(child=child, **list_kwargs) → 최종 객체 반환
# 3. 결과
# 사용자는 UserSerializer라고 생각하지만
# 실제로는 ListSerializer(child=UserSerializer) 인스턴스
이 구현은 DRF의 핵심 설계 철학을 잘 보여줍니다.
many=True 하나로 모든 것을 제어DRF의 many 옵션 구현을 살펴보면서, 단순해 보이는 API 뒤에 숨겨진 정교한 설계를 확인할 수 있었습니다. Factory Pattern과 Strategy Pattern의 조합, Meta 클래스를 활용한 설정 방식, 그리고 __new__와 __init__의 차이를 활용한 객체 생성 제어까지.
이런 구현 덕분에 개발자는 복잡한 내부 로직을 몰라도 직관적인 API를 사용할 수 있고, 필요시에는 강력한 커스터마이징도 가능합니다.
좋은 프레임워크란 이런 것이겠죠. 간단한 일은 간단하게, 복잡한 일은 가능하게 만드는 것 말입니다.