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에 정의되어 있다.
.as_view() 호출path("tasks/", ScrapeTaskListCreateView.as_view())
as_view()는 View(Django 기본)에 정의된 클래스 메서드다. 호출하면 클래스를 함수처럼 쓸 수 있는 view 함수를 반환한다. Django 라우터는 함수만 받기 때문에 이 변환이 필요하다.
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.
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으로 렌더링한다.
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를 쓸 때 뭔가 안 되면 dispatch → get/post → list/create 순으로 소스 코드를 따라가면 대부분 원인을 찾을 수 있다.