DRF3 튜토리얼 2 - 요청과 응답

원문 - Requests and Responses

번역을 허락해 준 Tom Christie에게 고마움을 전합니다.


튜토리얼 2: 요청과 응답

이번 튜토리얼부터 REST 프레임워크의 진정한 핵심부를 다루고자 합니다. 먼저, 두 가지 핵심 요소를 소개하죠.

요청(Request) 객체

REST 프레임워크의 Request 객체는 평범한 HttpRequest 객체를 확장하여 좀더 유연하게 요청을 파싱합니다. Request 객체의 핵심부는 request.data 속성입니다. 이 속성은 request.POST와 비슷하지만 웹 API에 좀더 적합합니다.

request.POST  # 폼 데이터만 다루며, 'POST' 메서드에서만 사용 가능
request.data  # 아무 데이터나 다룰 수 있고, 'POST'뿐만 아니라 'PUT'과 'PATCH' 메서드에서도 사용 가능

응답(Response) 객체

REST 프레임워크에는 Response 객체도 존재합니다. 이 객체는 TemplateResponse 타입이며, 렌더링되지 않은 콘텐츠를 불러와 클라이언트에게 리턴할 콘텐츠 형태로 변환합니다.

return Response(data)  # 클라이언트가 요청한 형태로 콘텐트를 렌더링함

상태 코드

여러분이 만든 뷰에서 숫자 형태의 HTTP 상태 코드를 사용하는 경우, 읽기에도 어려울 뿐더러 코드에 오류가 있더라도 발견하기가 어렵습니다. REST 프레임워크에서는 각 상태 코드에 대해 좀더 명확한 식별자를 제공합니다. 예를 들어 status 모듈의 HTTP_400_BAD_REQUEST 같은 식별자 같이요. 숫자로 된 식별자를 사용하기보다는 문자 형태의 식별자를 사용하세요.

API 뷰 감싸기

REST 프레임워크는 API 뷰를 작성하는 데 사용할 수 있는 두 가지 래퍼를 제공합니다.

  1. @api_view 데코레이터를 함수 기반 뷰에서 사용할 수 있습니다.
  2. APIView 클래스는 클래스 기반 뷰에서 사용할 수 있습니다.

이 래퍼들은 뷰에서 받은 Request에 몇몇 기능을 더하거나, 콘텐츠가 잘 변환되도록 Response에 특정 context를 추가합니다.

또한 때에 따라 405 Method Not Allowed를 반환하거나, request.data가 깨진 경우 ParseError 예외를 던지는 등의 일도 수행합니다.

이 모든 것을 한 군데 모으기

그럼 이제 이 두 요소를 사용하여 뷰를 작성해봅시다.

views.pyJSONResponse 클래스는 더이상 필요하지 않으니 지웁시다. 그리고 뷰 코드를 조금 리팩터링합니다.

from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer


@api_view(['GET', 'POST'])
def snippet_list(request):
    """
    코드 조각을 모두 보여주거나 새 코드 조각을 만듭니다.
    """
    if request.method == 'GET':
        snippets = Snippet.objects.all()
        serializer = SnippetSerializer(snippets, many=True)
        return Response(serializer.data)

    elif request.method == 'POST':
        serializer = SnippetSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

예제에 비해 목록 뷰가 조금 개선되었습니다. 조금 간단해지면서 폼 API와 유사하다는 느낌을 줍니다. 또한 이름 형태의 상태 코드를 사용하여 의미를 명확히 했습니다.

이제 views.py를 열어, 코드 조각 하나를 담당하는 뷰를 수정합시다.

@api_view(['GET', 'PUT', 'DELETE'])
def snippet_detail(request, pk):
    """
    코드 조각 조회, 업데이트, 삭제
    """
    try:
        snippet = Snippet.objects.get(pk=pk)
    except Snippet.DoesNotExist:
        return Response(status=status.HTTP_404_NOT_FOUND)

    if request.method == 'GET':
        serializer = SnippetSerializer(snippet)
        return Response(serializer.data)

    elif request.method == 'PUT':
        serializer = SnippetSerializer(snippet, data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    elif request.method == 'DELETE':
        snippet.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

이 또한 어디선가 본 듯 합니다. 일반적인 Django 뷰와 크게 다르지 않죠?

여기서 특정 콘텐츠 형태에 대한 요청이나 응답을 명시적으로 연결하지 않았음을 주목하세요. request.datajson 요청 뿐만 아니라 yaml과 같은 다른 포맷도 다룰 수 있습니다. 응답 객체에 데이터를 담아 리턴하는 것과 비슷하면서도, REST 프레임워크에서는 우리가 원하는 형태로 응답 객체를 렌더링해 줍니다.

URL의 접미어를 통해 다른 포맷 제공하기

앞에서 보았듯이 하나의 콘텐츠 형태에 묶여 있지 않다는 응답 객체의 장점을 활용하기 위해, 우리 API에서도 여러 형태의 포맷을 제공해 봅시다. 포맷의 접미어를 URL 형태로 전달받으려면, 다음과 같은 URL을 다룰 수 있어야 합니다.

http://example.com/api/items/4.json

일단 format 키워드를 두 가지 뷰에 추가해 봅시다.

def snippet_list(request, format=None):
def snippet_detail(request, pk, format=None):

그리고 이제 urls.py를 조금 수정하겠습니다. 기존의 URL에 format_suffix_patterns라는 패턴을 추가합니다.

from django.conf.urls import patterns, url
from rest_framework.urlpatterns import format_suffix_patterns
from snippets import views

urlpatterns = [
    url(r'^snippets/$', views.snippet_list),
    url(r'^snippets/(?P<pk>[0-9]+)/$', views.snippet_detail),
]

urlpatterns = format_suffix_patterns(urlpatterns)

이 외에 더 수정한 부분은 없는데도, 코드는 간명해졌고, 사용자는 자신이 원하는 형태의 포맷을 전달 받을 수 있습니다.

어떻게 되었을까?

이제 튜토리얼 1에서 했던 것처럼 명령행에서 API를 테스트해봅시다. 앞에서 했던 것과 비슷하게 작동하는 것처럼 보이지만, 이번에는 잘못된 요청에도 잘 대응합니다.

전체 코드 조각 목록은 다음과 같이 받아올 수 있습니다.

http http://127.0.0.1:8000/snippets/

HTTP/1.1 200 OK
...
[
  {
    "id": 1,
    "title": "",
    "code": "foo = \"bar\"\n",
    "linenos": false,
    "language": "python",
    "style": "friendly"
  },
  {
    "id": 2,
    "title": "",
    "code": "print \"hello, world\"\n",
    "linenos": false,
    "language": "python",
    "style": "friendly"
  }
]

그리고 이제는 Accept 헤더를 사용하여 응답 받을 데이터의 포맷도 지정할 수 있습니다.

http http://127.0.0.1:8000/snippets/ Accept:application/json  # Request JSON
http http://127.0.0.1:8000/snippets/ Accept:text/html         # Request HTML

아니면 포맷 접미어를 붙여도 되고요.

http http://127.0.0.1:8000/snippets.json  # JSON suffix
http http://127.0.0.1:8000/snippets.api   # Browsable API suffix

비슷하게, Content-Type 헤더를 사용해도 응답 받을 데이터의 포맷을 지정할 수 있습니다.

# 데이터를 넘기면서 POST 요청
http --form POST http://127.0.0.1:8000/snippets/ code="print 123"

{
  "id": 3,
  "title": "",
  "code": "print 123",
  "linenos": false,
  "language": "python",
  "style": "friendly"
}

# JSON으로 POST 요청
http --json POST http://127.0.0.1:8000/snippets/ code="print 456"

{
    "id": 4,
    "title": "",
    "code": "print 456",
    "linenos": false,
    "language": "python",
    "style": "friendly"
}

자, 이제 브라우저에서도 확인해보세요. http://127.0.0.1:8000/snippets/

탐색 가능한 API

API는 클라이언트의 요청에 따라 데이터의 포맷을 결정하여 응답합니다. 따라서 웹 브라우저의 요청에 대해서는 기본적으로 HTML 형태로 응답해주게 됩니다. 이 덕분에 API를 웹 브라우저에서 탐색할 수 있습니다.

브라우저에서 탐색 가능함은 사용성 면에서 굉장히 유용하여, API를 더 쉽게 개발하고 사용하도록 도와줍니다. 또한 다른 개발자들이 API를 파악하고 사용할 때의 진입장벽을 획기적으로 낮춰 줍니다.

브라우저에서 탐색 가능한 API에서 브라우저 탐색 기능에 대해 더 자세히 알아볼 수 있습니다.

What's next?

튜토리얼 3부에서는 클래스 기반 뷰를 사용하면서, 일반적인 뷰에 비해 코드량이 얼마나 줄어드는지 확인하겠습니다.

Show Comments