DRF의 ListCreateAPIView는 내부에서 어떻게 동작하는가

ch__oi·2026년 3월 9일
post-thumbnail

Django REST Framework(DRF)를 처음 쓰면 generics.ListCreateAPIView 한 줄로 GET/POST가 다 된다는 게 신기하면서도 불안하다. 뭔가 마법처럼 동작하는 것 같아서 문제가 생기면 어디서 봐야 할지 모른다.

이 글에서는 ListCreateAPIView가 요청을 받았을 때 내부에서 어떤 경로로 실행되는지 소스 코드를 따라가며 정리한다.


예시 코드

class ScrapeTaskListCreateView(generics.ListCreateAPIView):
    queryset = ScrapeTask.objects.all().order_by('-created_at')
    serializer_class = ScrapeTaskSerializer

이게 전부다. 근데 GET 요청을 보내면 목록이 나오고, POST를 보내면 생성이 된다. 어떻게?


상속 구조

View (Django 기본)
  └── APIView
        └── GenericAPIView
              └── ListModelMixin + CreateModelMixin
                    └── ListCreateAPIView

DRF의 generic view는 여러 클래스를 조합해서 만들어진다.

# rest_framework/generics.py
class ListCreateAPIView(mixins.ListModelMixin,
                        mixins.CreateModelMixin,
                        GenericAPIView):
    def get(self, request, *args, **kwargs):
        return self.list(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        return self.create(request, *args, **kwargs)

get()self.list()로, post()self.create()로 위임한다. list()create()는 각각 ListModelMixin, CreateModelMixin에 정의되어 있다.


요청 흐름 추적

1. urls.py에서 .as_view() 호출

path("tasks/", ScrapeTaskListCreateView.as_view())

as_view()View(Django 기본)에 정의된 클래스 메서드다. 호출하면 클래스를 함수처럼 쓸 수 있는 view 함수를 반환한다. Django 라우터는 함수만 받기 때문에 이 변환이 필요하다.

2. 요청이 들어오면 dispatch() 실행

HTTP 요청이 들어오면 APIView.dispatch()가 호출된다.

# rest_framework/views.py
def dispatch(self, request, *args, **kwargs):
    request = self.initialize_request(request, *args, **kwargs)
    self.initial(request, *args, **kwargs)  # 인증, 권한, 쓰로틀 체크

    if request.method.lower() in self.http_method_names:
        handler = getattr(self, request.method.lower(), self.http_method_not_allowed)

    response = handler(request, *args, **kwargs)
    return self.finalize_response(request, response, *args, **kwargs)

request.method.lower()로 HTTP 메서드를 소문자로 변환한 뒤, 같은 이름의 메서드를 자기 자신에서 찾는다. GET이면 self.get, POST면 self.post.

3. get()list() 실행

# rest_framework/mixins.py
class ListModelMixin:
    def list(self, request, *args, **kwargs):
        queryset = self.filter_queryset(self.get_queryset())
        serializer = self.get_serializer(queryset, many=True)
        return Response(serializer.data)

get_queryset()은 클래스에 선언한 queryset을 반환한다. get_serializer()serializer_class로 시리얼라이저 인스턴스를 만든다. serializer.data는 Python 딕셔너리로 변환된 데이터고, Response가 이걸 JSON으로 렌더링한다.

4. post()create() 실행

# rest_framework/mixins.py
class CreateModelMixin:
    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)
        return Response(serializer.data, status=status.HTTP_201_CREATED)

    def perform_create(self, serializer):
        serializer.save()

request.data로 요청 바디를 받아서 유효성 검사 후 저장한다. perform_create()를 별도로 분리한 이유는 오버라이드하기 쉽게 하기 위해서다. 예를 들어 저장 후 Celery 태스크를 발행하고 싶다면:

def perform_create(self, serializer):
    task = serializer.save()
    scrape_url.delay(task.id)

get_serializer_class()는 언제 쓰나

serializer_class를 하나만 선언하면 GET/POST 모두 같은 시리얼라이저를 사용한다. GET 응답에는 전체 필드를 주고, POST 요청에는 일부 필드만 받고 싶다면 get_serializer_class()를 오버라이드한다.

class ScrapeTaskListCreateView(generics.ListCreateAPIView):
    queryset = ScrapeTask.objects.all()

    def get_serializer_class(self):
        if self.request.method == "POST":
            return ScrapeTaskCreateSerializer
        return ScrapeTaskSerializer

get_serializer()가 내부에서 get_serializer_class()를 호출하기 때문에, 메서드에 따라 다른 시리얼라이저가 선택된다.


함수형 뷰와 비교

같은 기능을 함수형 뷰로 작성하면 흐름이 훨씬 명확하다.

@api_view(["GET", "POST"])
def task_list(request):
    if request.method == "GET":
        tasks = ScrapeTask.objects.all().order_by("-created_at")
        serializer = ScrapeTaskSerializer(tasks, many=True)
        return Response(serializer.data)

    elif request.method == "POST":
        serializer = ScrapeTaskCreateSerializer(data=request.data)
        if serializer.is_valid():
            task = serializer.save()
            scrape_url.delay(task.id)
            return Response(ScrapeTaskSerializer(task).data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

Generic View는 이 코드를 추상화한 것이다. 코드량은 줄지만 내부 동작을 모르면 커스터마이즈할 때 막힌다.


정리

항목설명
as_view()클래스를 Django 라우터가 받을 수 있는 함수로 변환
dispatch()HTTP 메서드에 맞는 핸들러(get, post 등)를 찾아 실행
list()queryset 조회 → 시리얼라이즈 → Response 반환
create()요청 데이터 유효성 검사 → 저장 → Response 반환
get_serializer_class()GET/POST 별로 다른 시리얼라이저를 쓰고 싶을 때 오버라이드
perform_create()저장 후 추가 작업(태스크 발행 등)을 끼워 넣을 때 오버라이드

Generic View를 쓸 때 뭔가 안 되면 dispatchget/postlist/create 순으로 소스 코드를 따라가면 대부분 원인을 찾을 수 있다.

profile
Builder, Sweeper

0개의 댓글