drf-spectacular를 이용한 API 문서화

Dongwoo Kim·2024년 3월 19일
0

DRF

목록 보기
9/9
post-thumbnail
post-custom-banner

개요

DRF에서 OpenAPI 3.0 규격에 맞춰 API 문서를 자동 생성해주는 라이브러리

설치 & 설정

  • command
     pip install drf-spectacular
  • settings.py
    INSTALLED_APPS = [
    	# ALL YOUR APPS
    	'drf_spectacular',
    ]
    
    ...
    
    REST_FRAMEWORK = {
        "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
    }
    SPECTACULAR_SETTINGS = {
        "TITLE": "Core System API",
        "DESCRIPTION": "Core System API Documents (240318)",
        "VERSION": "1.0.0",
        "SERVE_INCLUDE_SCHEMA": False,
        # OTHER SETTINGS
    }
  • urls.py
    from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
    urlpatterns = [
        # YOUR PATTERNS
        path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
        # Optional UI:
        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'),
    ]

사용 시나리오

Workflow & schema customization — drf-spectacular documentation

기본 방식

: 자동으로 데이터 스키마를 파악하여 문서화하기위해서 DRF에서 제공하는 Serializer 이용

  1. 각 View 클래스에 serializer_class 를 정의하면 해당 Serializer 클래스가 가진 스키마를 이용하여 자동으로 API별 문서를 제공해준다.

  2. @extend_schema 데코레이션을 이용해서 뷰함수별로 스키마를 커스텀하여 정의할 수 있다.

  3. inline_serailizer 를 이용하면 간단한 요청 및 응답 바디에 따로 Serialzer 클래스를 정의할 필요없이 필드를 명시할 수 있다.

  4. OpenApiParameter 를 통해 각종 요청 파라미터를 명시할 수 있다.

    (location=OpenApiParameter.PATH 와 같은 enum값을 통해 파라미터 종류별로 정의 가능)

  5. 응답값은 status code 별로 달리 정의할 수 있다.

  6. OpenApiExample 를 이용하면 요청 및 응답 예시를 만들 수 있다.

    (안해도 정의된 필드 기준으로 기본 예시 제공)

  7. @extend_schema_view , @extend_schema_serializer 데이코레이터를 사용하면 View 클래스, Serializer 클래스 단계에서 스키마 정의 가능하다. @extend_schema 가 정의되어 있다면 @extend_schema 가 우선된다.

    (우선순위: @extend_schema > @extend_schema_view > @extend_schema_serializer )

  8. @extend_schema_field 를 이용하면 Serializer 클래스에 정의된 serializers.SerializerMethodField() 도 필드를 명시해서 문서에 포함시킬 수 있다.

적용 예시

  1. 사전 기능 정의

    APIView를 이용한 Product 모델을 조회, 생성하는 기능

    • urls.py
      # /main
      urlpatterns = [
          path("api/products/", include("product.urls")),
      ]
      
      # /product
      urlpatterns = [
          path("", Product__View.as_view(), name="product_view"),
          path("<int:id>", Product__DetailView.as_view(), name="product_detail_view"),
      ]
    • models.py
      class Product(models.Model):
          number = models.IntegerField(unique=True)
          name = models.CharField(max_length=255)
          price = models.FloatField()
          stock = models.IntegerField()
      
      class Category(models.Model):
          name = models.CharField(max_length=255)
          products = models.ManyToManyField(Product, related_name="categories")
    • views.py
      class Product__DetailView(APIView):
          def get(self, request, id):
              product = Product.objects.get(id=id)
      
              return Response(
                  Product__ResponseSerializer(product).data,
                  status=status.HTTP_200_OK,
              )
      
      class Product__View(APIView):
          def post(self, request):
              data = {
                  "number": 1,
                  "name": request.data.get("name"),
                  "price": request.data.get("price"),
                  "stock": 10,
              }
              product = Product.objects.create(**data)
      
              return Response(
                  Product__ResponseSerializer(product).data,
                  status=status.HTTP_201_CREATED,
              )
      
    • serializers.py
      class Category__Serializer(serializers.ModelSerializer):
          class Meta:
              model = Category
              fields = "__all__"
      
      class Product__ResponseSerializer(serializers.ModelSerializer):
          category_list = serializers.SerializerMethodField()
          categories = Category__Serializer(many=True)
      
          def get_category_list(self, obj):
              return list(obj.categories.values_list("id", flat=True))
      
          class Meta:
              model = Product
              fields = "__all__"
      
  2. drf-spectacular 적용

    • urls.py
      from drf_spectacular.views import (
          SpectacularAPIView,
          SpectacularRedocView,
          SpectacularSwaggerView,
      )
      
      # /main
      urlpatterns = [
          path("api/products/", include("product.urls")),
          path("api/schema/", SpectacularAPIView.as_view(), name="schema"),
          # Optional UI:
          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",
          ),
      ]
      
      # /product
      urlpatterns = [
          path("", Product__View.as_view(), name="product_view"),
          path("<int:id>", Product__DetailView.as_view(), name="product_detail_view"),
      ]
      
    • views.py
      from drf_spectacular.utils import (
          extend_schema,
          OpenApiExample,
          OpenApiParameter,
          inline_serializer,
      )
      
      class Product__DetailView(APIView):
          @extend_schema(
              operation_id="get product detail",
              tags=["Product"],
              description="상품 상세조회",
              parameters=[
                  OpenApiParameter(
                      name="id",
                      type=str,
                      required=True,
                      description="프로덕트 아이디",
                      location=OpenApiParameter.PATH,
                  ),
              ],
              responses={
                  200: Product__ResponseSerializer,
              },
              examples=[
                  OpenApiExample(
                      name="example",
                      response_only=True,
                      value={
                          "shop_id": "1",
                          "service_item_ids": [
                              1,
                              2,
                              3,
                          ],
                      },
                  ),
              ],
          )
          def get(self, request, id):
              product = Product.objects.get(id=id)
      
              return Response(
                  Product__ResponseSerializer(product).data,
                  status=status.HTTP_200_OK,
              )
      
      class Product__View(APIView):
          @extend_schema(
              operation_id="create product",
              tags=["Product"],
              description="상품 생성",
              request=inline_serializer(
                  name="custom",
                  fields={
                      "shop_id": serializers.CharField(
                          max_length=100,
                          help_text="쇼핑몰 아이디",
                          required=True,
                      ),
                      "service_item_ids": serializers.ListField(
                          child=serializers.IntegerField(),
                          help_text="서비스 아이템 아이디",
                          required=True,
                      ),
                  },
              ),
              responses={
                  200: Product__ResponseSerializer,
              },
              examples=[
                  OpenApiExample(
                      name="example",
                      request_only=True,
                      value={
                          "shop_id": "1",
                          "service_item_ids": [
                              1,
                              2,
                              3,
                          ],
                      },
                  ),
              ],
          )
          def post(self, request):
              data = {
                  "number": 1,
                  "name": request.data.get("name"),
                  "price": request.data.get("price"),
                  "stock": 10,
              }
              product = Product.objects.create(**data)
      
              return Response(
                  Product__ResponseSerializer(product).data,
                  status=status.HTTP_201_CREATED,
              )
      
    • serializers.py
      from drf_spectacular.utils import (
          extend_schema_serializer,
          OpenApiExample,
          extend_schema_field,
      )
      
      class Category__Serializer(serializers.ModelSerializer):
          class Meta:
              model = Category
              fields = "__all__"
      
      @extend_schema_serializer(
          examples=[
              OpenApiExample(
                  name="Example",
                  value={
                      "name": "노트북",
                      "price": "100",
                  },
              )
          ]
      )
      class Product__ResponseSerializer(serializers.ModelSerializer):
          category_list = serializers.SerializerMethodField()
          categories = Category__Serializer(many=True)
      
          @extend_schema_field(
              serializers.ListField(
                  child=serializers.IntegerField(),
                  help_text="카테고리 아이디 리스트",
              )
          )
          def get_category_list(self, obj):
              return list(obj.categories.values_list("id", flat=True))
      
          class Meta:
              model = Product
              fields = "__all__"
      
  3. API 문서 확인

    • get product detail
    • create product
  4. 부가 설명

    1. urls.py 에서 볼 수 있듯이 redoc외에 swagger-ui로도 확인할 수 있다.

      redoc은 스키마 파악이 용이하고 swagger-ui로는 api를 excute할 수 있다.

    2. serializers.py 에서 @extend_schema_serializer 를 이용해 예시를 만들었지만 @extend_schema 이 우선되어 예시가 적용되지 않는 걸 볼 수 있다.

    3. @extend_schema 에서 정의한 예시는 실제 Serializer 클래스 스키마와 많이 다르더라도 정의한대로 문서화됨을 알 수 있다.

    4. inline_serializer , @extend_schema_field 등에서 필드를 정의할 때 serializers.Field 클래스를 이용했다. 공식문서에는 OpenApiTypes 또는 기본 파이썬 타입도 이용할 수 있다고 나와있다.

      Workflow & schema customization — drf-spectacular documentation

    5. get product detailcreate product api는 각각 다른 View 클래스로 정의되었지만 같은 tags=["product"]를 명시하여 문서화에는 같은 태크로 묶여있다.

    6. create product api 문서의 응답 스키마를 보면 categories 필드로 정의된 Category__Serializer 클래스의 스키마도 표시됨을 볼 수 있다.

    7. @extend_schema 에서는 decortion= , serializers.Field() 에서는 help_text= 으로 설명을 덧붙일 수 있고 Serializer 클래스에서 따로 명시하지않는 필드들은 models.py 에서 정의한 필드의 verbose_name= 을 따라간다.

관련 문서

profile
kimphysicsman
post-custom-banner

0개의 댓글