Django REST Framework many=True 옵션 완벽 분석 BaseSerializer 내부 구현과 Factory Pattern

김현수·2025년 5월 31일

many=True

Django REST Framework(DRF)를 사용하다 보면 자연스럽게 many=True 옵션을 사용하게 됩니다.

# 단일 객체 직렬화
serializer = UserSerializer(user)

# 리스트 객체 직렬화  
serializer = UserSerializer(users, many=True)

겉보기에는 단순한 파라미터 하나의 차이지만, 내부적으로는 완전히 다른 클래스가 동작합니다. 오늘은 DRF의 many 옵션이 어떻게 구현되어 있는지, 그리고 이 설계가 왜 우아한지 살펴보겠습니다.

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 발견

상속 체인을 따라 올라가니 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__을 함께 쓰면서 흥미로운 부분이 나옵니다.

  1. __init__에서 many 제거: kwargs.pop('many', None)many 옵션을 제거합니다
  2. __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)

이 메서드는 세 가지 중요한 역할을 합니다.

1. 인자 분배 (Argument Distribution)

  • 리스트 전체에 적용되는 옵션 (min_length, max_length 등)
  • 개별 아이템에 적용되는 옵션 (context, partial 등)
  • 공통으로 필요한 옵션들을 적절히 분배

2. 자식-부모 관계 설정

list_kwargs['child'] = cls(*args, **kwargs)

개별 아이템을 처리할 Serializer를 child로 설정합니다.

3. Strategy Pattern 구현

meta = getattr(cls, 'Meta', None)
list_serializer_class = getattr(meta, 'list_serializer_class', ListSerializer)

Meta 클래스 패턴의 활용

기본 사용

class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = '__all__'
        # 기본 ListSerializer 사용

커스텀 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)  # 설정이 없으면 기본값

설계 패턴의 조합

이 구현은 여러 디자인 패턴이 조합된 우아한 설계입니다

1. Abstract Factory Pattern (추상 팩토리 패턴)

# 조건에 따라 관련된 객체 패밀리 전체를 변경
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 로직
)

2. Strategy Pattern (전략 패턴)

# Meta 설정에 따라 다른 ListSerializer 사용
class UserSerializer(serializers.ModelSerializer):
    class Meta:
        list_serializer_class = CacheableListSerializer  # 캐싱 전략
        # list_serializer_class = BulkListSerializer     # 벌크 처리 전략
        # list_serializer_class = AsyncListSerializer    # 비동기 전략

3. Template Method Pattern (템플릿 메서드 패턴)

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) 인스턴스

Django REST Framework의 설계 철학

이 구현은 DRF의 핵심 설계 철학을 잘 보여줍니다.

1. Convention over Configuration (관례 우선 설정)

  • 기본적으로는 설정 없이 동작
  • 필요시에만 Meta 클래스로 커스터마이징

2. Pluggable Architecture (플러그인 가능한 아키텍처)

  • 각 레이어가 독립적으로 교체 가능
  • 새로운 전략을 쉽게 추가 가능

3. Transparent Complexity (투명한 복잡성)

  • 복잡한 내부 구현을 단순한 API로 숨김
  • 사용자는 many=True 하나로 모든 것을 제어

4. Family-based Object Creation (패밀리 기반 객체 생성)

  • 단일 객체가 아닌 관련된 객체 체계 전체를 교체
  • Abstract Factory Pattern을 통한 일관된 직렬화 생태계 제공

마무리하며

DRF의 many 옵션 구현을 살펴보면서, 단순해 보이는 API 뒤에 숨겨진 정교한 설계를 확인할 수 있었습니다. Factory Pattern과 Strategy Pattern의 조합, Meta 클래스를 활용한 설정 방식, 그리고 __new____init__의 차이를 활용한 객체 생성 제어까지.

이런 구현 덕분에 개발자는 복잡한 내부 로직을 몰라도 직관적인 API를 사용할 수 있고, 필요시에는 강력한 커스터마이징도 가능합니다.

좋은 프레임워크란 이런 것이겠죠. 간단한 일은 간단하게, 복잡한 일은 가능하게 만드는 것 말입니다.


참고 자료

profile
숭실대학교 소프트웨어학부생

0개의 댓글