drf-yasg를 이용한 Swagger 문서 자동화

·2020년 8월 16일
24

Swagger 문서

목록 보기
1/2
post-thumbnail

Swagger?


API 서버를 자동으로 문서화 시켜주는 툴이다. API 주소나 각 API의 메소드를 명시해주고, query string이나 body 같은 payload도 문서 내에서 사용 가능하게 해준다.

Django 프레임워크를 사용한 경우, drf-yasg 라이브러리를 이용하여 python 코드로 더욱 쉽게 문서 자동화가 가능하다는 것 같다. 자세한 내용은 drf-yasg 공식문서, drf-yasg 공식에서 제공하는 샘플코드 등을 참고하길 바란다.


간단한 예제


여기서는 간단한 POST, GET 메소드 API를 만들어 로컬 서버에서 swagger 문서로 띄우는 과정을 설명한다.

최종 결과물 링크


라이브러리 설치 및 환경설정

drf-yasg 라이브러리를 설치한다.
간단한 API를 만들 것이니 rest-framework도 설치해주자.

(.venv) $ pip install drf-yasg             # .venv는 가상환경임.
(.venv) $ pip install djangorestframework

환경설정 파일(디폴트 settings.py)의 INSTALLED_APPS에 설치한 라이브러리의 app을 추가한다.

# 프로젝트_루트/프로젝트_이름/settings.py
# -----------------------------------------------------

# ...

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'drf_yasg',        # 추가
    'rest_framework',  # 추가
]

# ...

swagger를 위한 엔드포인트를 추가한다. 이걸 명시해야 Swagger 문서를 UI로 볼 수 있다.
urls.py 파일을 app 단위로 나누었다면, 최상위 urls.py 파일에 명시하면 된다.

# 프로젝트_루트/프로젝트_이름/urls.py
# --------------------------------------------------

from django.contrib import admin
from django.urls import path, re_path
from django.conf import settings
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="Swagger Study API", 
        default_version="v1", 
        description="Swagger Study를 위한 API 문서", 
        terms_of_service="https://www.google.com/policies/terms/", 
        contact=openapi.Contact(name="test", email="test@test.com"), 
        license=openapi.License(name="Test License"), 
    ), 
    public=True, 
    permission_classes=(permissions.AllowAny,), 
)

urlpatterns = [
    path('admin/', admin.site.urls),
]

# 이건 디버그일때만 swagger 문서가 보이도록 해주는 설정이라는 듯. urlpath도 이 안에 설정 가능해서, debug일때만 작동시킬 api도 설정할 수 있음.
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'),    ]



Model 생성

api 앱 생성

(.venv) $ python manage.py startapp api

settings.py에 앱 추가

# 프로젝트_루트/프로젝트_이름/settings.py
# -----------------------------------------------------

# ...

INSTALLED_APPS = [
    # ...
    'api',   # 새 앱 추가
]

# ...

생성한 앱에 간단한 모델 생성

# 프로젝트_루트/api/models.py
# -----------------------------------------------------

from django.db import models 

# Create your models here. 

class Music(models.Model):
    ONE_STAR = 1
    TWO_STAR = 2
    THREE_STAR = 3
    STARS = (
        (ONE_STAR, '좋음'),
        (TWO_STAR, '매우 좋음'),
        (THREE_STAR, '고귀함'),
    )

    CATEGORY = (
        ('JPOP', '제이팝'),
        ('POP', '팝송'),
        ('CLASSIC', '클래식'),
        ('ETC', '기타등등'),
    )

    id = models.BigAutoField(primary_key=True, verbose_name='music_id')
    created_at = models.DateTimeField(auto_now_add=True, verbose_name='추가된 날짜')
    updated_at = models.DateTimeField(auto_now=True, verbose_name='업뎃 된 날짜')
    deleted_at = models.DateTimeField(blank=True, null=True, verbose_name='삭제된 날짜')
    singer = models.CharField(null=False, max_length=128, verbose_name='가수명')
    title = models.CharField(null=False, max_length=128, verbose_name='곡명')
    category = models.CharField(blank=True, null=True, max_length=32, choices=CATEGORY, verbose_name='범주')
    star_rating = models.PositiveSmallIntegerField(blank=True, null=True, choices=STARS, default=ONE_STAR, verbose_name='곡 선호도')

    class Meta:
        # abstract = True  # sqlite3 사용 시 어째선지 이게 있으면 마이그레이션이 안 됨.
        managed = True
        db_table = 'musics'
        app_label = 'api'
        ordering = ['star_rating', ]
        verbose_name_plural = '음악'

마이그레이션

(.venv) $ python manage.py makemigrations  # 마이그레이션 파일 만들기
(.venv) $ python manage.py migrate         # db에 마이그레이션 파일 적용

# DB에 접속해서 확인해봄.
$ sqlite3 DB명    # 맥os 일 때
$ .\sqlite3 DB명  # 윈도우 일 때

# 접속 후
sqlite> .tables
auth_group                  django_admin_log
auth_group_permissions      django_content_type       
auth_permission             django_migrations
auth_user                   django_session
auth_user_groups            musics  # 잘 추가되어 있는 것을 확인
auth_user_user_permissions



클래스형 View와 Serializer 작성

GET, POST url 추가

앱 내부 urls

# 프로젝트_루트/api/urls.py
# --------------------------------------------------

from django.urls import path 
from django.conf import settings 
from .views import MusicViewSet

urlpatterns = [ 
    path("v1/music", MusicViewSet.as_view({"get": "list", "post": "add"}), name="musics"),
    path("v1/music/<int:music_num>", MusicViewSet.as_view({"get": "list"}), name="music"),
]

프로젝트 urls

# 프로젝트_루트/프로젝트_이름/urls.py
# --------------------------------------------------

# ...

urlpatterns = [
    path('admin/', admin.site.urls),
    path("api/", include(("api.urls", "api"))), # 편의를 위해(?) api 앱의 세부 url은 api앱 내부의 urls.py 파일에서 관리할 것
]

# ...

swagger 문서 자동화를 구현한 예제들을 보면, 다들 클래스형 View로 구현한다. 아마 클래스 변수로서 serializer_class 를 선언하여 시리얼라이저를 받아줘야 하기 때문인 것 같다.

from rest_framework import status, viewsets, mixins 
from rest_framework.response import Response 
from django.views import View 
from django.http import Http404

from .models import Music 
from .serializers import MusicSerializer

# Create your views here. 

class MusicViewSet(viewsets.GenericViewSet, 
                mixins.ListModelMixin, 
                View): 

    serializer_class = MusicSerializer   # 이 클래스형 view 에서 사용할 시리얼라이저를 선언

    def get_queryset(self):
    	conditions = {
            'id': self.kwargs.get("music_num", None),
            'title__contains': self.request.GET.get('title', None),
            'star_rating': self.request.GET.get('star_rating', None),
            'singer__contains': self.request.GET.get('singer', None),
            'category__in': [self.request.GET.get('category_'+str(i+1), None) for i in range(4)],
            'created_at__lte': self.request.GET.get('created_at', None),
        }
        conditions = {key: val for key, val in conditions.items() if val is not None}

        musics = Music.objects.filter(**conditions)
        if not musics.exists():
            raise Http404()

        return musics

    def add(self, request): 
        musics = Music.objects.filter(**request.data)
        if musics.exists():
            return Response(status=status.HTTP_406_NOT_ACCEPTABLE)

        music_serializer = MusicSerializer(data=request.data, partial=True)
        if not music_serializer.is_valid():
            return Response(status=status.HTTP_406_NOT_ACCEPTABLE)

        music = music_serializer.save()

        return Response(MusicSerializer(music).data, status=status.HTTP_201_CREATED)
        

시리얼라이저 작성

from .models import *
from rest_framework import serializers

class MusicSerializer(serializers.ModelSerializer):
    class Meta:
        music = Music.objects.all()
        model = Music
        fields = '__all__'  # __all__ 을 줄 경우, 모든 필드가 사용됨.
        # fields = ('id', 'created_at', 'title', 'category', 'star_rating',)  # req, res 시 사용되길 원하는 필드(컬럼)만 적어줘도 됨.

대략적인 구성은 이걸로 끝이다. 이제 서버를 켜서 실제 Swagger 문서가 잘 나오는지 확인해보자. 로컬에서 django의 기본 포트로 구동할 경우 url은 http://127.0.0.1:8000/swagger 이다.



추가사항


대부분의 API는 request를 받을 때 데이터를 요구한다. drf-yasg는 API 요청 시에 필요한 데이터를 Swagger 문서에서 보여주고, 사용하게 해주는 기능을 제공한다. 해당 기능은 라이브러리에서 제공하는 데코레이터에 시리얼라이저만 넘겨주면 구현된다. 정말 간단하다.

여기서는 POST 시의 body, GET 시의 Query String, 그리고 Header 를 설정하는 방법을 설명한다. 앞선 샘플 프로젝트에 이어서 구현한다.


POST 메소드의 Body 추가

Swagger는 POST API에 body를 제공한다. 그리고 body에는 해당 API의 시리얼라이저에 선언된 필드가 나온다. 문제는 response에는 필요할지 몰라도, request 시에는 필요하지 않은 것도 포함되어 있다는 것이다.

예를 들어, 앞서 구현한 POST 메소드에서 body의 deleted_at은 response 때는 보여주고 싶어도 request 때는 받고 싶지 않다.

deleted_at을 없애기 위한 커스텀 body를 만들어보자.


POST를 담당하는 메소드에 @swagger_auto_schema 데코레이터를 추가한다.
이 데코레이터는 파라미터로 request_body를 받는데, 해당 파라미터 값으로는 시리얼라이저 타입만 와야 한다. (혹은 Schema도 가능하다는데, 이쪽은 잘 모르겠다.)

# /api/views.py
# ---------------------------------------------------------------------

# ...

from drf_yasg.utils import swagger_auto_schema    # 임포트
from .serializers import MusicSerializer, MusicBodySerializer  # body를 위한 시리얼라이저 임포트. 이후에 만들거임.

# ...

class MusicViewSet(viewsets.GenericViewSet, 
                mixins.ListModelMixin, 
                View):

    serializer_class = MusicSerializer   # 원래 선언해뒀던 이 시리얼라이저 클래스와는 별개로 어노테이션 안에 request_body를 위한 시리얼라이저를 따로 추가함.

    # ...

    @swagger_auto_schema(request_body=MusicBodySerializer)  # request_body에 별도의 시리얼라이저 추가
    def add(self, request): 
        # ...

이렇게 할 경우, 기존에 선언해두었던 시리얼라이저는 response만 담당하고, request는 새로 추가해준 시리얼라이저가 맡게 된다.

시리얼라이저를 작성하자. 시리얼라이저 작성 시 serializers.Serializer를 상속 받아서 구현하는 점에 주의한다.

# /api/serializers.py
# ---------------------------------------------------------------------

# ...

class MusicBodySerializer(serializers.Serializer):
    singer = serializers.CharField(help_text="가수명")
    title = serializers.CharField(help_text="곡 제목")
    category = serializers.ChoiceField(help_text="곡 범주", choices=('JPOP', 'POP', 'CLASSIC', 'ETC'))
    star_rating = serializers.IntegerField(help_text="1~3 이내 지정 가능. 숫자가 클수록 좋아하는 곡")

이걸로 구현은 끝났다. 서버를 켜서 확인해보면, request body만 바뀌어서 나오는 걸 확인할 수 있다.



POST 메소드의 Body 삭제

body가 필요없는 경우엔 body 항목 자체가 뜨지 않도록 설정할 수 있다. 이것도 데코레이터를 사용한다.

# /api/views.py 
# ---------------------------------------------------------------------

from drf_yasg.utils import swagger_auto_schema, no_body   # no_body 를 import

# ...

class MusicViewSet(viewsets.GenericViewSet, 
                mixins.ListModelMixin, 
                View): 
    # ... 

    # 테스트 용도로 다른 POST 메소드를 만듦. 
    @swagger_auto_schema(request_body=no_body)  # request_body에 no_body를 넣어줌.
    def add_for_no_body(self, request):
        return Response(status=status.HTTP_201_CREATED)

확인해보면, body가 나오지 않는다.



header 추가하기

header는 데코레이터에 manual_parameters 인자를 이용하여 구현한다. 해당 인자는 list만 받는다. list 값으로 openapi.Parameter() 를 넣어주면 된다.

# /api/views.py 
# --------------------------------------------------------------------- 

from drf_yasg.utils import swagger_auto_schema    # import 잊지 말자

# ... 

class MusicViewSet(viewsets.GenericViewSet, 
                mixins.ListModelMixin, 
                View): 
    # ... 

    @swagger_auto_schema(
        request_body=MusicBodySerializer,
        manual_parameters=[openapi.Parameter('header_test', openapi.IN_HEADER, description="a header for  test", type=openapi.TYPE_STRING)]
    )
    def add(self, request): 

        # ...
        

Parameter()는 name, _in 인자는 필수값이다.(위의 예제에서 첫 번째, 두 번째 인자이다.)
name은 api 파라미터의 이름이며 문서에 표시된다.
_in에는 파라미터의 종류를 적어준다. header는 openapi.IN_HEADER 라고 선언하면 된다.
description은 말그대로 해당 파라미터에 대한 설명을 보여준다.
type은 필수값은 아니지만, _in에 header를 지정했을 경우 반드시 선언해주어야 한다.


이걸로 헤더도 완성이다. 문서를 확인해보면, data 밑에 header가 추가된 것이 보인다.



GET의 Query String 추가하기

query string도 swagger_auto_schema에 query_serializer 인자를 이용해서 구현한다. 이 인자도 값은 시리얼라이저 객체 타입을 받는다.

# /api/views.py 
# --------------------------------------------------------------------- 
from drf_yasg.utils import swagger_auto_schema    # import 잊지 말자 

# ... 

class MusicViewSet(viewsets.GenericViewSet, 
                mixins.ListModelMixin, 
                View): 
    serializer_class = MusicSerializer

    # get_queryset에 데코레이터를 붙이면 인식을 못 하기 때문에 list를 상속 받아서 구현했다.
    # get_queryset은 list() 에서 불리는 함수에 불과하다.
    # 실제 response 하는 메소드는 list 이기 때문에 list를 상속받아서 데코레이터를 붙여준다.
    @swagger_auto_schema(query_serializer=MusicQuerySerializer)
    def list(self, request, *args, **kwargs):
        return super().list(request, *args, **kwargs)

    def get_queryset(self):

        # ...

시리얼라이저 작성

# /api/serializers.py 
# --------------------------------------------------------------------- 

# ...

class MusicQuerySerializer(serializers.Serializer):
    title = serializers.CharField(help_text="곡 제목으로 검색", required=False)
    star_rating = serializers.ChoiceField(help_text="곡 선호도로 검색", choices=(1, 2, 3), required=False)
    singer = serializers.CharField(help_text="가수명으로 검색", required=True)
    category_1 = serializers.ChoiceField(help_text="카테고리로 검색", choices=('JPOP', 'POP', 'CLASSIC', 'ETC'), required=False)
    category_2 = serializers.ChoiceField(help_text="카테고리로 검색", choices=('JPOP', 'POP', 'CLASSIC', 'ETC'), required=False)
    category_3 = serializers.ChoiceField(help_text="카테고리로 검색", choices=('JPOP', 'POP', 'CLASSIC', 'ETC'), required=False)
    category_4 = serializers.ChoiceField(help_text="카테고리로 검색", choices=('JPOP', 'POP', 'CLASSIC', 'ETC'), required=False)
    created_at = serializers.DateTimeField(help_text="입력한 날짜를 기준으로 그 이전에 추가된 곡들을 검색", required=False)


위에서 ChoiceField를 사용했는데, 인자인 choices에 리스트나 튜플을 넘겨주면 Swagger 문서 내에 셀렉트 박스가 생성된다. 참고로 1개 값만 주고 싶을 경우, 그냥 string으로 넘겨줘도 문제는 없었다.

required 인자는 Swagger 문서에서 필수값 여부를 표시해준다. 필드에 명시하지 않으면 디폴트 값인 True가 적용된다. 그러면 문서에 빨간색 글자로 reqired가 표시되고, 문서에서 해당 필드에 값을 넣어야만 api 요청이 가능해진다. 이 인자는 모든 필드에서 선언할 수 있다.



마치며


데코레이터를 이용하는 방법 이외에도 SwaggerAutoSchema 를 상속 받아서 직접 Scheam를 구현할수도 있다. 아무래도 API가 많아지다보면, 데코레이터가 덕지덕지 붙어 있는 것이 지저분해 보인다. 이 방법을 이용할 경우 데코레이터를 붙이지 않아 좀 더 깔끔하게 보이게 만들 수 있을 것이다. 다만 drf-yasg 라이브러리를 심도있게 파야 한다는 큰 문제가...

profile
드디어 1년 찍은 비전공 뉴비 개발자

1개의 댓글

comment-user-thumbnail
2021년 2월 5일

잘 보고 갑니다 !

답글 달기