2025.7.21: drf-spectacular (1)

jiyongg·2025년 7월 21일

TIL: Today I Learned

목록 보기
4/30

지난 미니 해커톤 회고록에서 OpenAPI Specification을 직접 작성했었는데, 오랜 시간이 걸렸다. 게다가 지금 생각해보니, 인증 관련 명세를 까먹고 안 넣었었다!

이처럼, OpenAPI Specification을 직접 작성하면, 시간도 오래 잡아먹고, 무언가를 까먹고 안 넣는 일이 생길 수도 있다.

그래서 실무에서는 API 문서를 자동으로 생성한다고 한다.

사실, 미니 해커톤 이전에 drf-spectacular의 존재는 알고 있었다. 하지만, 내용을 정리해두질 않아서 이 라이브러리를 프로젝트에서 실제로 활용해보진 못했다.

이번에 멋사에서 한 달 간 중앙 해커톤이 진행되는데, 그런 노가다를 반복하지 않기 위해 이번 기회에 drf-spectacular의 내용을 정리해 보고자 한다.

내용은 대체로 공식 문서를 참고하였고, 공식 문서에서 설명이 없는 부분은 소스 코드를 살펴 보았다.

1. ⚙️ 설치 및 구성

설치

pip를 이용해서 설치한다.

$ pip install drf-spectacular

settings.py 수정

라이브러리를 설치 후, settings.py에서 3가지 부분을 수정한다.

INSTALLED_APPS에 추가

새로운 라이브러리를 설치했으니 Django 프로젝트 설정의 INSTALLED_APPS에 추가해주어야 한다.

INSTALLED_APPS = [
    ...
    'drf_spectacular',
]

⚠️ 설치 때와는 달리 앱 이름에 대시(-)가 아니라 언더바(_)가 있다는 점에 주의해야 한다.

REST_FRAMEWORK 설정 수정

drf-spectacularAutoSchema가 DRF의 뷰를 분석해서 뷰의 스키마를 만들어 낸다. DRF(REST_FRAMEWORK)의 설정을 수정해서 기본적으로 스키마 생성에 사용되는 뷰 분석 클래스(DEFAULT_SCHEMA_CLASS)를 drf-spectacularAutoSchema로 설정한다.

REST_FRAMEWORK = {
    ...
    'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
}

SPECTACULAR_SETTINGS

drf-spectacular의 설정은 SPECTACULAR_SETTINGS에서 할 수 있다.

예시 외에도 다양한 많은 설정이 존재하는데, 이는 문서를 참고하길 바란다.

SPECTACULAR_SETTINGS = {
    'TITLE': 'API 제목',
    'DESCRIPTION': 'API 설명',
    'VERSION': 'API 버전',
    'SERVE_INCLUDE_SCHEMA': False,
}
SERVE_INCLUDE_SCHEMA

SERVE_INCLUDE_SCHEMA에 대해 공식 문서에서는 include schema endpoint into schema 라고 설명하고 있는데, 나는 솔직히 이게 무슨 뜻인지 잘 이해가 가질 않았다.

SERVE_INCLUDE_SCHEMA는 스키마 정보에 대한 엔드포인트를 스키마에 추가한다.

이렇게 이야기하면 무슨 뜻인지 이해가 가지 않으므로, 직접 SERVE_INCLUDE_SCHEMA를 키고 끈 후 생성되는 스키마를 비교해 보았다.

SERVE_INCLUDE_SCHEMA를 켰을 때 문서의 마지막 부분이다.

SERVE_INCLUDE_SCHEMA를 껐을 때 문서의 마지막 부분이다.

두 문서는 모두 python manage.py spectacular --color --file schema.yml이라는 똑같은 명령어를 통해 생성된 문서이다.

보다시피, 켰을 때보다 껐을 때 문서가 짧아지는 것을 볼 수 있다.

그렇다면 왜 켰을 때 문서가 더 길어지는 것일까?

이것은 SERVE_INCLUDE_SCHEMA를 켰을 때의 문서의 맨 첫 부분이다.

이것은 SERVE_INCLUDE_SCHEMA를 껐을 떄의 문서의 앞 부분이다.

그렇다. SERVE_INCLUDE_SCHEMA를 키게 되면, 스키마 문서에 스키마 정보를 볼 수 있는 엔드포인트(여기에서는 /api/schema/)가 포함된다.

실제로, paths/api/schema/ 부분을 지워보면 두 문서의 내용과 길이가 같아진다.

urls.py 수정

drf-spectacular에는 생성된 스키마를 서비스하는 뷰가 있다.

이 뷰를 url에 연결해준다.

from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView, SpectacularRedocView

urlpatterns = [
    ...
    # 필수
    path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
    # 선택
    path('api/schema/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
    path('api/schema/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
]
  • SpectacularAPIView를 연결한 URL에 들어가면 응답으로 스키마 파일이 반환되는데, 이 파일은 yaml이나 json으로 제공된다. 기본적으로 yaml로 제공된다.
    • SpectacularAPIView의 소스 코드를 보면 renderer_classesyaml 관련 Renderer가 json 관련 Renderer보다 우선한다. renderer_classesAccept*/*일 때, 리스트의 가장 앞에 있는 Renderer를 사용한다.
    • yaml 대신 json을 받는 방법은 아래의 실험을 참고하길 바란다.
  • SpectacularSwaggerViewSpectacularRedocViewSpectacularAPIView가 연결된 뷰에서 스키마 문서를 얻어 Swagger UIRedoc 스타일의 API 문서를 제공한다.
    • as_view 메소드 안의 url_name 인자의 값은 SpectacularAPIView가 연결된 뷰의 name 인자의 값과 같아야 한다.

yaml 대신 json 받기

SpectacularAPIView는 기본적으로 스키마를 yaml로 제공한다.

하지만, Accept 헤더를 변경하면 json을 제공받을 수 있다.

서버가 켜진 상태에서 Postman을 킨다.

먼저 Accept 헤더가 */*인 상황에서 스키마 URL로 GET 요청을 보내면

이처럼 yaml 파일이 반환된다.

하지만, Accept 헤더를 application/json으로 바꾸고 GET 요청을 보내면

이처럼 json 파일이 반환된다.

2. 📖 기본적인 사용법

설정을 끝냈다면, drf-spectacular를 한 번 사용해본다.

사용법은 간단하다. 이 상태에서 그냥 서버를 돌리면 된다.

$ python manage.py runserver

서버를 돌리고 SpectacularAPIView에 연결된 URL로 요청이 들어오면 스키마 제너레이터가 프로젝트의 뷰와 시리얼라이저를 분석해 자동으로 스키마를 생성한다.

drf-spectacularmanage.py를 통해서도 사용할 수 있다.

만약, 스키마 문서를 프로젝트 폴더에 yml(yaml) 파일로 저장하고 싶다면 아래의 명령어를 입력한다.

$ python manage.py spectacular --file schema.yml

여기에서 --file schema.yml 없이 실행하면 스키마 문서의 내용이 터미널에 출력된다.

$ python manage.py spectacular

python manage.py spectacular에서 사용할 수 있는 옵션들은 drf_spectacular/management/commands/spectacular.pyCommand 클래스의 소스 코드를 살펴 보면 알 수 있다.

class Command(BaseCommand):
    help = dedent("""
        Generate a spectacular OpenAPI3-compliant schema for your API.

        The warnings serve as a indicator for where your API could not be properly
        resolved. @extend_schema and @extend_schema_field are your friends.
        The spec should be valid in any case. If not, please open an issue
        on github: https://github.com/tfranzel/drf-spectacular/issues

        Remember to configure your APIs meta data like servers, version, url,
        documentation and so on in your SPECTACULAR_SETTINGS."
    """)

    def add_arguments(self, parser):
        parser.add_argument('--format', dest="format", choices=['openapi', 'openapi-json'], default='openapi', type=str)
        parser.add_argument('--urlconf', dest="urlconf", default=None, type=str)
        parser.add_argument('--generator-class', dest="generator_class", default=None, type=str)
        parser.add_argument('--file', dest="file", default=None, type=str)
        parser.add_argument('--fail-on-warn', dest="fail_on_warn", default=False, action='store_true')
        parser.add_argument('--validate', dest="validate", default=False, action='store_true')
        parser.add_argument('--api-version', dest="api_version", default=None, type=str)
        parser.add_argument('--lang', dest="lang", default=None, type=str)
        parser.add_argument('--color', dest="color", default=False, action='store_true')
        parser.add_argument('--custom-settings', dest="custom_settings", default=None, type=str)
    
    # 각 옵션에 따른 처리 로직이 있음. 길어서 생략
    def handle(self, *args, **options):
        ...
        
    ...

여기에서 알 수 있는 옵션들은 다음과 같다.

  • --format: openapiyaml 형식, openapi-jsonjson 형식인 것으로 보인다.
  • --urlconf: 지정된 모듈에 있는 URL 패턴들(urlconf)을 바탕으로 스키마를 생성한다. 기본적으로 프로젝트의 루트 URL 모듈에 있는 URL 패턴들을 바탕으로 생성된다.
  • --generator-class: 처리에서 사용할 제너레이터 클래스를 정한다.
  • --file: 파일명과 함께 사용해서 해당 파일명으로 스키마 문서를 저장한다.
  • --fail-on-warn: 경고 발생시 SchemaGenerationError가 발생해 실패한다.
  • --validate: 생성된 api 스키마에 대해 유효성 검증을 한다.
  • --api-version: 스키마 생성에 사용할 현재 프로젝트의 api 버전이다.
    • ⚠️ OpenAPI 버전이 아님에 주의해야 한다.
    • 이 옵션을 사용하면 스키마 문서의 맨 위쪽의 info 부분에서 version: 원래버전 (api-version)으로 나타난다.
  • --lang: 언어를 지정할 수 있다. (ex: 한국어(ko), 일본어(ja), 영어(en), ...) 그러면 다국어를 지원하는 API의 경우 지정된 언어를 바탕으로 설명이 생성된다.
  • --color: 이 옵션이 포함되어 있으면 오류나 경고 발생 시 조금 더 색으로 구분된 출력 메시지를 볼 수 있다.
  • --custom-settings: 커스텀 설정이다. 문자열에 해당하는 모듈을 불러온다.

3. 🖌️ 커스터마이징

drf-spectacular는 뷰를 분석할 때 클래스의 querysetserializer_class에서 많은 정보를 얻는다.

그래서, queryset이나 serializer_class가 지정되어 있지 않은 뷰이거나 추가적인 정보가 많은 뷰에 대해서 스키마 생성의 결과가 만족스럽지 못할 수 있다.

이 경우 drf-spectacular에서 제공하는 데코레이터를 통해 API의 정보를 커스터마이징할 수 있다.

아래에 있는 예시 코드들은 타입 힌팅을 제외하면 모두 공식 문서의 코드들이다.

모듈의 구조

공식 문서를 살펴보면, 여러 데코레이터가 등장하고 여러 클래스가 등장하는데, 어디에서 import해야 하는지는 설명해주지 않는다. 그래서 Package Overview에서 하나하나 찾아보아야 하는 불편함이 있었다.

그런 불편함을 줄이기 위해 이 글에서는 아래에서 사용할 데코레이터와 클래스들에 한해 미리 모듈의 구조를 요약해 두고자 한다.

  • drf_spectacular.utils
    • @extend_schema, @extend_schema_view, @extend_schema_field, @extend_schema_serializer
    • OpenApiParameter
      • 위치(location, OpenAPI Specification의 in에 해당): COOKIE, HEADER, PATH, QUERY
    • OpenApiExample
    • inline_serializer
  • drf_spectacular.types
    • OpenApiTypes: NUMBER, STR, BYTE, PASSWORD, INT, ...
  • drf_spectacular.extensions
    • OpenApiViewExtension

@extend_schema

먼저 살펴볼 것은 @extend_schema이다.

@extend_schema는 함수형 뷰나 메소드에 붙여서 뷰나 메소드의 정보를 더해준다.

class PersonView(viewsets.GenericViewSet):
    @extend_schema(
        parameters=[
            QuerySerializer,
            OpenApiParameter("nested", QuerySerializer),
            OpenApiParameter("queryparam1", OpenApiTypes.UUID, OpenAPIParameter.QUERY),
            OpenApiParameter("pk", OpenApiTypes.UUID, OpenAPIParameter.PATH),
        ],
        request=YourRequestSerializer,
        responses=YourResponseSerializer,
        ...
    )
    def retrieve(self, request, pk, *args, **kwargs):
        ...
  • retrieve 메소드에 @extend_schema 데코레이터를 이용하여 정보를 추가하고 있다.
  • 매개변수로 시리얼라이저를 넣으면, 시리얼라이저의 각 필드가 매개변수로 변환된다.
  • 위 예에서 OpenApiParameter
    • nestednametype을 positional argument로 주었다.
    • queryparam1pkname, type, location을 positional argument로 주었다.
  • 위 예에서는 requestresponses로 요청용 시리얼라이저와 응답용 시리얼라이저를 분리하였다.
    • requestresponses에는 시리얼라이저뿐만 아니라, 다양한 값이 들어갈 수 있다. 이에 대해서는 공식 문서를 참고하길 바란다.

inline_serializer

응답의 내용이 간단해서 시리얼라이저 클래스를 정의하지 않은 경우에는, inline_serializer를 사용할 수 있다.

인자로는 name, fields, kwargs를 받는데, name은 시리얼라이저의 이름, fields는 필드 이름이 키이고 필드 타입이 값인 딕셔너리, kwargs는 그 외에 시리얼라이저 초기화에 사용되는 옵션들이다.

@extend_schema_view

만약, 정보를 수정하거나 추가하고 싶은 메소드가 이미 부모 클래스에서 구현되어 있고 이 메소드를 오버라이딩 없이 그대로 사용하고 있다면 어떻게 해야 할까?

@extend_schema를 사용하려면, 자식 클래스가 이를 다시 정의해서 부모 클래스의 메소드를 그대로 사용하게 해야 한다.

예를 들어 ListModelMixin을 상속한 XViewsetlist에 대해서 @extend_schema를 사용하고 싶다면 어떻게 해야 할까?

class XViewset(mixins.ListModelMixin, viewsets.GenericViewSet):
    @extend_schema(description='text')
    def list(self, request, *args, **kwargs):
        return super().list(request, *args, **kwargs)

list에 대해 @extend_schema를 사용하자고 코드 두 줄을 더 써버리고 말았다. 자식의 list 메소드에 추가적인 행동이 존재하는 것도 아니다.

이러한 경우에 @extend_schema_view를 사용하면 굳이 list 에 대한 정의를 또 쓸 필요가 없게 된다.

@extend_schema_view(
    list=extend_schema(description='text')
)
class XViewset(mixins.ListModelMixin, viewsets.GenericViewSet):
    ...

@extend_schema_view는 클래스의 데코레이터로 사용한다. 키워드 인자를 받는데, 인자의 이름은 메소드 이름이고, 인자의 값은 extend_schema를 실행한 결과이다.

위의 코드와 똑같은 역할을 하는데, list를 재정의하지 않아도 된다!

ViewSet의 action에 사용

@extend_schema_view는 ViewSet의 action에 대해서도 사용할 수 있다.

@extend_schema_view(
    notes=extend_schema(description='text')
)
class XViewset(mixins.ListModelMixin, viewsets.GenericViewSet):
    @action(detail=False)
    def notes(self, request):
        ...

이 코드에서 notes 메소드는 ViewSet의 action으로 정의되었다. list@extend_schema_view를 사용하던 것처럼, 인자의 이름은 action의 이름, 인자의 값은 extend_schema를 실행한 결과이다.

@extend_schema_field

시리얼라이저에서 커스텀 필드를 만든 경우나 필드가 SerializerMethodField인 경우 사용한다.

커스텀 필드의 경우

@extend_schema_field(OpenApiTypes.BYTE)
class CustomField(serializers.Field):
    def to_representation(self, value):
        return urlsafe_base64_encode(b'\xf0\xf1\xf2')
  • 필드 클래스에 데코레이터를 사용한다.
  • 데코레이터의 인자로는 시리얼라이저, OpenApiType 타입, 파이썬 기본 타입, 또는 여기에서 각 데이터 타입에 사용할 수 있는 키워드들을 키로 하는 딕셔너리를 받는다.
  • urlsafe_base64_encode는 Django의 함수로, 바이트 문자열을 base64 문자열로 변환한다.

SerializerMethodField의 경우

class ErrorDetailSerializer(serializers.Serializer):
    field_custom = serializers.SerializerMethodField()
    
    @extend_schema_field(OpenApiTypes.DATETIME)
    def get_field_custom(self, object):
        return '2020-03-06 20:54:00.104248'
  • SerializerMethodField로 지정된 field_custom이 호출하는 get_field_custom에 데코레이터를 사용했다.

타입 힌팅

class CommentResponseSerializer(serializers.ModelSerializer):
    ...
    @extend_schema_field(str)
    def get_nickname(self, obj):
        return obj.user_id.nickname

이것은 미니 해커톤 코드 중 응답용 댓글 시리얼라이저 부분에 @extend_schema_field를 적용해 본 코드이다.

@extend_schema_field의 인자로 파이썬 기본 타입인 str을 줬는데, 타입 힌팅을 @extend_schema_field 없이 get_nickname의 타입 힌팅을 통해 반환 타입을 str로 명시해도 같은 결과가 나온다.

class CommentResponseSerializer(serializers.ModelSerializer):
    ...
    def get_nickname(self, obj) -> str:
        return obj.user_id.nickname

@extend_schema_serializer

시리얼라이저의 필드뿐만 아니라, 아예 시리얼라이저에도 정보를 추가할 수 있다.

@extend_schema_serializer는 시리얼라이저에 사용하는 데코레이터이다.

@extend_schema_serializer(
    exclude_fields=('single',),
    examples = [
        OpenApiExample(
            'Valid example 1',
            summary='short summary',
            description='longer description',
            value={
                'songs': {'top10': True},
                'single': {'top10': True}
            },
            request_only=True,
            response_only=True, 
        ),
    ]
)
class AlbumSerializer(serializers.ModelSerializer):
    songs = SongSerializer(many=True)
    single = SongSerializer(read_only=True)
    
    class Meta:
        fields = '__all__'
        model = Album 
  • @extend_schema_serializer에서 사용 가능한 인자 등은 공식 문서를 참조하면 된다.
  • OpenApiExamplevalue 인자는 필드명이 키이고 예시값이 값인 딕셔너리를 받는다.
  • request_onlyresponse_only는 해당 예시가 각각 요청, 응답에만 적용되는지에 대한 여부이다.

OpenApiViewExtension

모든 뷰가 Generic View인 것은 아닐 수 있다. drf-spectacularqueryset이나 serializer_class에서 많은 정보를 얻는다고 했는데, querysetserializer_class는 Generic View(GenericAPIView)에 정의되어 있기 때문에, APIView만 상속한 뷰나 @api_view 데코레이터를 사용한 함수형 뷰에서는 사용할 수 없다.

그렇다면 이런 상황에서는 어떻게 해야 할까?

OpenApiViewExtension을 사용해서 원래 뷰에 추가적인 정보를 제공할 수 있다.

위의 데코레이터들과는 다르게, 스키마를 위한 클래스를 만들어서, 해당 클래스에 상속해서 사용하는 방식이다.

class Fix4(OpenApiViewExtension):
    target_class = 'oscarapi.views.checkout.UserAddressDetail'

    def view_replacement(self):
        from oscar.apps.address.models import UserAddress

        class Fixed(self.target_class):
            queryset = UserAddress.objects.none()
        return Fixed
  • OpenApiViewExtension을 상속한 후, view_replacement 메소드를 정의한다.
    • target_class에 대상이 되는 뷰를 지정해준다.
    • 이때, view_replacement 메소드는 APIView의 서브 클래스를 반환해야 한다.
    • Fixed라는 클래스를 만들었다. 클래스는 self.target_class를 상속한 뒤 추가적으로 queryset을 가지고 있다.
  • 원본(target_class) 뷰 인스턴스는 self.target으로 접근할 수 있다.

이외에도, 공식 문서에는 OpenApiAuthenticationExtension, OpenApiSerializerExtension 등 다양한 Extension이 존재하고, 전처리 훅이나 후처리 훅에 대한 내용도 있다. 하지만, 일단 내가 자주 사용하게 될 것 같은 기능들만 먼저 정리해 두고자 한다.

4. 🔚 결론

사실 이 글을 쓰기 시작한 건 월요일 저녁 7시 반쯤이었다. 그런데 정리를 해도해도 끝이 나지 않고, 글의 내용도 만족스럽지 않아 7월 21일의 TIL을 22일에 업로드하는 사태를 맞이하게 되었다. 왜 벌써 새벽 5시인 것일까..?

TIL 톡방에는 임시로 정리한 노션 문서를 올렸기 때문에, 벌금은 내지 않았지만, 21일의 TIL이 22일에 올라가니 좀 이상하달까..

처음에 이 글을 쓸 때에는, 공식 문서에 있는 예시 코드를 그대로 사용하기보다는 실제로 내가 실습을 해본 코드를 이용해 예시를 구성해 보고자 했다. 그래서 DRF 튜토리얼의 코드를 바탕으로 예시를 구상해 보려 했는데, 예시의 퀄리티가 좋게 나오지 못해서 글의 내용을 갈아엎고 공식 문서의 예시 코드를 그대로 이용하는 것을 선택했다.

아마 다음 글에선 실습 느낌으로 미니 해커톤 프로젝트에 drf-spectacular를 적용시켜 보는 방향으로 진행해 볼 것 같다.

profile
그냥 쓰고 싶은 것 쓰는 개발(?) 블로그

0개의 댓글