최근에 여행 다녀오느라 장기간 블로그가 방치되어 있었더니 가슴이 아팠는데, 오랜만에 DRF 포스팅을 하게 됐다.
사용법 자체는 너무 쉬워서 허무할 정도라 가볍게 읽어도 괜찮을 듯 하다.
목차
1. What is Swagger?
2. drf-yasg
3. API Document
4. body 커스텀
5. header 커스텀
5-1. header의 Authorization에 jwt_token 넣기
6. Query String
1. What is Swagger?
API 서버를 개발해본 사람은 알겠지만, 백엔드에서 중요한 작업 중 하나로 API Document 작성이 있다.
Client에서 자원을 얻기 위해 API 사용법(메서드)을 알려주는 것이라 보면 되는데, 나는 예전에 무식하게 노션에 전부 적어놨었다.
그런데 최근 DevOps를 공부하다가 Swagger라는 기술이 계속 언급이 되어 찾아보니 API 문서화 도구라고 한다.
end-pint, parameter, request/response, header 등을 문서화할 수 있고, UI로 표시하여 개발자가 쉽게 접근할 수 있도록 도와준다.
즉, 위에 저 내용들을 모두 자동으로 생성하고 보기 좋게 정리까지 해주는 툴이다.
2. drf-yasg
swagger는 Django에 국한된 기술이 아니기 때문에 DRF에서는 drf-yasg라는 라이브러리를 사용한다.
설치하려는 프로젝트로 이동하여 가상환경을 실행시키고 터미널에 다음과 같이 입력한다.
pip install drf-yasg
설치가 정상적으로 완료됐다면, settings.py의 INSTALLED_APPS에 추가해준다.
// settings.py
INSTALLED_APPS = [
(...)
# drf
'rest_framework',
# swagger
'drf_yasg'
]
swagger로 작성된 API Document를 UI로 확인하기 위해서는 end-point를 지정해주어야 한다.
경로는 최상위 urls.py에서 명시한다.
from django.contrib import admin
from django.urls import path, include, re_path
from django.conf import settings
from django.conf.urls.static import static
from rest_framework import permissions
from drf_yasg.views import get_schema_view
from drf_yasg import openapi
schema_view = get_schema_view(
openapi.Info(
title='프로젝트 이름',
default_version='프로젝트 버전',
description='API Doc 설명',
terms_of_service="https://www.google.com/policies/terms/",
contact=openapi.Contact(email="a@a.com"), # 부가 정보
license=openapi.License(name="test")
),
public=True,
permission_classes=[permissions.AllowAny]
)
urlpatterns = [
path('admin/', admin.site.urls),
(...)
]
if settings.DEBUG:
urlpatterns += [
re_path(r'^swagger(?P<format>\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name="schema-json"),
re_path(r'^swagger/$', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
re_path(r'^redoc/$', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc')]
urlpatterns에 바로 추가하지 않고 조건문을 넣은 이유는 api 문서는 외부에 노출되어서는 안 되는 정보다.
따라서 디버깅하는 경우에만 문서가 보여지도록 설정한다.
놀랍게도 이게 끝이다.
이제 결과물을 확인해보자.
3. API Document
포트 번호를 변경하지 않았다면 "http://127.0.0.1:8000/swagger/"로 접속했을 때 확인할 수 있다.
닫혀있던 토글을 클릭하면 이렇게 메서드 별로 정리가 잘 되어 있는 것을 확인할 수 있는데, 아무거나 하나 눌러보면
이렇게 url 별로 response 정보를 알려준다.
더 놀라운 기능은 테스트를 하기 위해서 서버를 실행하지 않고도 해당 페이지에서 바로 테스트가 가능하다는 점.
우측 상단의 Try it out을 누르고 excute를 누르면 response가 돌아온다.
4. Body 커스텀
Request 요청 시에는 body에 정보를 담아야 하는 post 요청같은 것들이 존재한다.
Swagger가 기본으로 제공하기는 하지만 기준이 해당 view에서 사용한 Serializer 기반으로 작성된다.
만약 Serializer로 직렬화한 데이터가 있지만 그 필드는 request 시에 필요하지 않을 수도 있고,
내 경우에는 로그인을 위한 정보와 리턴받는 정보가 완전히 다르다 보니 Serializer를 아예 사용하지 않아서 Swagger가 자동으로 body를 생성해주지 않는 문제도 있었다.
이럴 때는 커스텀을 해줄 필요가 있다.
내가 작성했던 유저 모델을 다음과 같다.
class SignInUserView(APIView):
permission_classes = [AllowAny]
def post(self, request):
nickname = request.data.get("nickname")
password = request.data.get("password")
if not nickname or not password:
return Response(status=status.HTTP_400_BAD_REQUEST)
user = authenticate(
nickname=nickname,
password=password,
)
if user:
token = TokenObtainPairSerializer.get_token(user)
refresh_token = str(token)
access_token = str(token.access_token)
res = Response(
{
"refresh": str(token),
"access": str(token.access_token),
},
status=status.HTTP_200_OK,
)
res.set_cookie("access", access_token, httponly=True)
res.set_cookie("refresh", refresh_token, httponly=True)
return res
return Response(status=status.HTTP_401_UNAUTHORIZED)
유저 정보를 받고 토큰을 돌려주니 Serializer 과정이 필요없어 사용하지 않는 덕에 Parameters가 생성이 되지 않았다.
그럼 "serializer를 사용해주면 끝나는 문제 아니냐?"
틀린 말은 아니지만 필요도 없는 직렬화 과정을 동반하는 것은 성능과 가독성을 모두 저해하는 수단이다.
이럴 땐 라이브러리에서 지원하는 기능을 이용하자.
# api/users/views.py
from drf_yasg.utils import swagger_auto_schema
from .swaggers import CustomUserBodySerializer
class SignInUserView(APIView):
permission_classes = [AllowAny]
@swagger_auto_schema(request_body=CustomUserBodySerializer)
def post(self, request):
(...)
swagger_auto_schema 어노테이션을 걸어주고 request_body에 필요한 정보를 받도록 Serializer를 명시해준다.
serializers.py에 정의하면 너무 지저분해질 것 같아서 swaggers.py를 따로 만들어서 사용했다.
from rest_framework import serializers
class CustomUserBodySerializer(serializers.Serializer):
nickname = serializers.CharField(help_text="닉네임")
password = serializers.CharField(help_text="패스워드")
닉네임과 패스워드만 받으면 되니까 간단히 설정해주고 다시 API 문서를 확인해보자.
정상적으로 body에 정보를 담을 수 있게 되었고 실행을 해보면
토큰 정보를 리턴해오는 것을 확인할 수 있다.
이런 기능을 하는 API 문서를 만들어 놓으면 프론트 엔드랑 조금은 덜 싸우고 평화롭게 개발할 수 있을 뿐 아니라,
정작 자신이 개발해놓고 무슨 기능을 하는 메서드인지 잊어먹는 경우가 꽤 흔하다.
나 자신을 위해서라도 문서화는 게을리하지 말자.
참고로 POST 메서드에 body 항목 자체가 필요 없는 경우에는
@swagger_auto_schema(request_body=no_body)
라고 정의해주면 된다.
5. Header 커스텀
권한 외에 헤더를 추가할 일은 많이 없겠지만, 방법이 조금 달라서 내용을 분리시켜버렸다.
헤더 추가는 그렇게 어렵지 않다.
class SignOutUserView(APIView):
permission_classes = [IsAuthenticated]
@swagger_auto_schema(manual_parameters=[openapi.Parameter('Authorization', openapi.IN_HEADER, description="access token", type=openapi.TYPE_STRING)])
def post(self, request, *args):
user = RefreshTokenSerializer(data=request.data)
user.is_valid(raise_exception=True)
user.save()
reset = ""
res = Response({"message": "logout success"}, status=status.HTTP_204_NO_CONTENT)
res.set_cookie("access", reset)
res.set_cookie("refresh", reset)
return Response({"message": "logout success"}, status=status.HTTP_204_NO_CONTENT)
name 인자로 'Authorization'을 _in 인자로 openapi.IN_HEADER를 던져주어 헤더를 명시하면 된다.
(쓰다가 사진이 날아가버렸는데 여기까진 별 중요한 내용이 아니니 넘기자.)
문제는 개발자가 헤더를 작성하는 경우는 보통 'Authorization' 필드를 사용하기 위함인데,
여기에 어떻게 JWT Token의 Access Token을 넣을 수 있을까?
5-1. Header의 Authorization에 JWT Token 넣기
진짜 이것 때문에 너무 열받아서 스트레스 잔뜩 받고 있었는데, 내가 그냥 바보짓을 연속으로 2번이나 하고 있었다.......
차근차근 알아보자.
만약, 로그인한 유저만 자원을 얻을 수 있도록 권한 설정을 한 경우 헤더에 정보를 실어서 보내야 하는 경우도 있다.
대부분의 서비스에서 로그인 기능은 필수적이므로 모르는 사람은 없을 것이라 생각한다.
예를 들어, 나는 JWT Token을 이용하여 유저 관리를 하고 있으므로 로그아웃을 하려면 access token 정보를 던져주어야 한다.
하지만 현재 상태에서 확인해보면 access token을 던져줄 헤더가 없어서 권한 문제가 생긴다.
공식 문서에 따르면 HTTP 기본 인증 및 Authorization 헤더 API 토큰을 수락하는 정의는 다음과 같다.
SWAGGER_SETTINGS = {
'SECURITY_DEFINITIONS': {
'Basic': {
'type': 'basic'
},
'Bearer': {
'type': 'apiKey',
'name': 'Authorization',
'in': 'header'
}
}
}
SECURITY_DEFINITIONS 설정을 추가해서 API에서 지원하는 모든 인증 체계를 선언함으로써 drf-yasg에게 알려주어야 하는 것이다.
이게 어떤 거냐면, swagger-ui를 처음 열어보면 session DB로 관리하는 로그인 폼을 지원하는데 이 기능을 끄고 JWT 방식 로그인으로 바꿔주면 이후에 헤더가 알아서 부가된다.
# settings.py
# Swagger
SWAGGER_SETTINGS = {
'USE_SESSION_AUTH': False,
'SECURITY_DEFINITIONS': {
'BearerAuth': {
'type': 'apiKey',
'name': 'Authorization',
'in': 'header',
'description': "JWT Token"
}
},
'SECURITY_REQUIREMENTS': [{
'BearerAuth': []
}]
}
우선 settings.py에 이렇게 선언을 해주면 Session 인증 방식을 끄고 token 인증 방식으로 활성화 시키도록 한다.
대부분의 경우 이렇게만해도 이후에 문제없이 동작한다.
UI 가장 위로 올라와보자.
우측 하단에 Authorize 버튼으로 바뀐 것을 볼 수 있는데, 눌러보면 token 정보를 입력하라고 나온다.
signin 경로로 받은 access token을 넣어주자.
이제 권한 인증이 필요한 메서드를 아무거나 실행시켜 보면 무사히 작동한다.
다만, 로그아웃 기능의 경우에는 drf-yasg에서 관리하는 토큰 정보를 삭제하기 위해 별다른 조치가 필요할 것 같은데 솔직히 그런 기능까지 구현할 필요가 있을까 싶어서 만져보다가 관두기로 했다.
6. Query String
Query String은 단순하게 구현 가능하지만, 마찬가지로 Serializer를 필요로 한다.
@swagger_auto_schema(query_serializer=TestSerializer)
어노테이션을 추가해주고 TestSerializer 안에는 body 정보를 추가할 때처럼 작성해주면 된다.
class TestSerializer(serializers.Serializer):
id = serializers.CharField(required=True)
name = serializers.CharField(required=False)
query1 = serializers.ChoiceField(required=False)
query2 = serializers.ChoiceField(required=False)
query3 = serializers.ChoiceField(required=False)
require 속성은 Swagger 문서 작성 시, 필수로 입력받아야 하는 값인지를 알려주는 용도다.