django์— swagger ๋งŒ๋“ค๊ธฐ, drf-spectacular

     
class KakaoAuthorize(PublicView):
    @extend_schema(
        parameters=[
            OpenApiParameter(
                name="code",
                type=OpenApiTypes.STR,
                location=OpenApiParameter.QUERY,
                description="Authorization code",
                required=True,
            )
        ],
        responses={
            status.HTTP_200_OK: LoginTokenSerializer(),
            status.HTTP_400_BAD_REQUEST: BAD_REQUEST_RESPONSE
        },
    )
    def get(self, request, *args, **kwargs):

 

 

 

๐Ÿ“•API ์ŠคํŽ™ ๊ณต์œ ๋ฅผ ์œ„ํ•œ swagger

๊ฐœ๋ฐœ์ž๋“ค์—๊ฒŒ swagger๋Š” ์•„์ฃผ ์นœ์ˆ™ํ•œ ์ด๋ฆ„์ด๋‹ค. ํ”„๋ก ํŠธ์—”๋“œ์™€ ๋ฐฑ์—”๋“œ ๋ชจ๋‘ ์ž์ฃผ ํ™œ์šฉํ•˜๋Š” ๊ธฐ๋Šฅ์ธ๋ฐ, API ์ŠคํŽ™์— ๋Œ€ํ•œ ๋ช…์„ธ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋Š” '๊ณต์œ  ๋ฌธ์„œ'๋ผ๊ณ  ์ƒ๊ฐํ•˜๋ฉด ๋œ๋‹ค. ์›น ๋ธŒ๋ผ์šฐ์ ธ๋ฅผ ์ด์šฉํ•ด GUIํ˜•ํƒœ๋กœ ์ œ๊ณต๋˜๊ธฐ ๋•Œ๋ฌธ์— ๋ณด๊ธฐ ํŽธํ•˜๊ณ , ์‹ค์ œ๋กœ API๋ฅผ ํ˜ธ์ถœํ•ด ๋ณผ ์ˆ˜ ์žˆ๊ธฐ๋„ ํ•˜๋‹ค. (๋ณด์•ˆ ๋•Œ๋ฌธ์— ์‹ค์ œ ์šด์˜ ์„œ๋ฒ„์—์„œ๋Š” swagger๋กœ ํ˜ธ์ถœ์„ ๋ง‰์•„๋†“๊ธฐ๋Š” ํ•œ๋‹ค.)

 

์˜ˆ์ „์— ์žฅ๊ณ  ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„ํ–‰ํ•  ๋•Œ๋Š” drf-yasg๋ฅผ ์‚ฌ์šฉํ–ˆ์—ˆ๋Š”๋ฐ, ์ตœ๊ทผ์— drf-spectacular๋ผ๋Š”๊ฒŒ ๋” ์ž˜ ๋‚˜๊ฐ€๋Š” ํŒจํ‚ค์ง€์ธ ๊ฒƒ ๊ฐ™๋‹ค. ์ง€์›ํ•˜๋Š” ๋ฒ„์ „์ด ๋” ๋†’์€๊ฐ€ ๋ณด๋‹ค. drf-spectacular๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด์„œ ์ด๊ฒƒ์ €๊ฒƒ ์„ธํŒ…์„ ์ข€ ํ•ด๋ณด๋ฉด์„œ ์‚ฌ์šฉ๋ฒ•์„ ์ •๋ฆฌํ•ด ๋ณธ๋‹ค.

 

swagger ์‹คํ–‰ ํ™”๋ฉด

 

 

โฌ‡๏ธ ์„ค์น˜ํ•˜๊ธฐ

๋จผ์ € ํŒจํ‚ค์ง€๋ฅผ ์„ค์น˜ํ•ด์•ผํ•œ๋‹ค. 

pip install drf-spectacular

 

ํ˜„์žฌ๊ธฐ์ค€ 0.27.1๋ฒ„์ „์ด ์ตœ์‹ ๋ฒ„์ „์ธ๋ฐ, ๊ณ„์†ํ•ด์„œ ๋ฒ„์ „ ์—…๋˜๊ณ  ์žˆ๋Š” ๋ชจ์–‘์ด๋‹ค. ๊นƒํ—™๋งํฌ : https://github.com/tfranzel/drf-spectacular

 

 

ํŒจํ‚ค์ง€๋ฅผ ์„ค์น˜ํ•˜๊ณ  ๋ช‡๊ฐ€์ง€ ์„ธํŒ…์ด ํ•„์š”ํ•œ๋ฐ, ์ผ๋‹จ settings.py์— ์•„๋ž˜์™€ ๊ฐ™์€ ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ด์•ผ ํ•œ๋‹ค.

 

# settings.py

INSTALLED_APPS = [
    ...
    'drf_spectacular',   # installed_app์— ์ถ”๊ฐ€
    ...
]

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



SPECTACULAR_SETTINGS = {
    # General schema metadata. Refer to spec for valid inputs
    # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#openapi-object
    'TITLE': 'drf-spectacular API Document',
    'DESCRIPTION': 'drf-specatular ๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๋งŒ๋“  API ๋ฌธ์„œ์ž…๋‹ˆ๋‹ค.',
    'SWAGGER_UI_SETTINGS': {
        'dom_id': '#swagger-ui',
        'layout': 'BaseLayout', 
        'deepLinking': True,  
        'displayOperationId': True,
        'filter': True,
    },
   
    'LICENSE': {
        'name': 'MIT License',
    },
    'VERSION': '1.0.0',
    'SERVE_INCLUDE_SCHEMA': False,

    'SWAGGER_UI_DIST': '//unpkg.com/swagger-ui-dist@3.38.0',
}

 

 

SPECTACULAR_SETTINGS๊ฐ€ ์‹ค์ œ๋กœ swaggerํ™”๋ฉด์— ์ด๊ฒƒ์ €๊ฒƒ ์˜ต์…˜์„ ๋„ฃ๋Š” ์„ค์ •์ธ๋ฐ, ์ž์„ธํ•œ ์„ค์ •๊ฐ’๋“ค์€ ์š”๊ธฐ๋ฅผ ๊ฐ€๋ฉด ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

 

์ด์ œ ๋‹ค์Œ์œผ๋กœ urls.py๋ฅผ ์ˆ˜์ •ํ•˜๋ฉด ์Šค์›จ๊ฑฐ ํ™”๋ฉด์„ ๋„์šธ ์ˆ˜ ์žˆ๋‹ค.

 

# urls.py
from drf_spectacular.views import SpectacularJSONAPIView, SpectacularYAMLAPIView, SpectacularSwaggerView, \
    SpectacularRedocView

urlpatterns = [
    path("swagger.json/", SpectacularJSONAPIView.as_view(), name="schema-json"),
    path("swagger.yaml/", SpectacularYAMLAPIView.as_view(), name="swagger-yaml"),
    path("swagger/", SpectacularSwaggerView.as_view(url_name="schema-json"), name="swagger-ui", ),
    path("redoc/", SpectacularRedocView.as_view(url_name="schema-json"), name="redoc", ),
    ...
]

 

 

์ผ๋ฐ˜์ ์ธ url path๋Š” ~/swagger ์ง€๋งŒ json์ด๋‚˜ yamlํ˜•ํƒœ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›๊ณ  ์‹ถ์€ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— swagger.json, swagger.yaml๋„ ์ถ”๊ฐ€ํ–ˆ๋‹ค. 

 

์ด์ œ django ํ”„๋กœ์ ํŠธ๋ฅผ ์‹คํ–‰ํ•˜๋ฉด swaggerํ™”๋ฉด์ด ๋œจ๋Š”๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค!!

 

 

๐Ÿง‘‍๐Ÿ’ป ์ปค์Šคํ„ฐ๋งˆ์ด์ง•

 

์œ„์ฒ˜๋Ÿผ ์„ค์น˜๋งŒ ํ•˜๊ณ  swagger๋ฅผ ๋„์šฐ๋ฉด ๋‹จ์ˆœํžˆ API ๋ฆฌ์ŠคํŠธ๋งŒ ๋‚˜์˜ค๊ณ  ๋‹ค๋ฅธ ์ •๋ณด๋“ค์€ ํ‘œ๊ธฐ๋˜์ง€ ์•Š๋Š”๋‹ค. ์ด๋Ÿฐ ๊ฒƒ๋“ค๊นŒ์ง€ ์ž๋™์œผ๋กœ ์ฐพ์•„์„œ ๋ณด์—ฌ์ค€๋‹ค๋ฉด ์•„์ฃผ ์ข‹๊ฒ ์ง€๋งŒ, ๊ฐ API๋งˆ๋‹ค request๋‚˜ response type์„ ์ผ์ผ์ด ์ง€์ •ํ•ด ์ค˜์•ผ ์ข€ ๋” swagger๋ฅผ ์ž˜ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. 

 

์•„๋ž˜๋Š” ์นด์นด์˜ค ์ธ์ฆ์„ ํ•˜๋Š” API์— ๋Œ€ํ•ด parameter๋ฅผ ์„ธํŒ…ํ•œ ์˜ˆ์‹œ์ด๋‹ค.

 

 

class KakaoAuthorize(PublicView):
    @extend_schema(
        parameters=[
            OpenApiParameter(
                name="code",
                type=OpenApiTypes.STR,
                location=OpenApiParameter.QUERY,
                description="Authorization code",
                required=True,
            )
        ],
        responses={
            status.HTTP_200_OK: LoginTokenSerializer(),
            status.HTTP_400_BAD_REQUEST: BAD_REQUEST_RESPONSE
        },
    )
    def get(self, request, *args, **kwargs):
        ...

 

 

๊ธฐ๋ณธ์ ์œผ๋กœ parameters, request, responses 3๊ฐœ๋งŒ ์ž˜ ์„ธํŒ…ํ•˜๋ฉด ๋œ๋‹ค. parameters๋Š” ํ—ค๋”๋‚˜ path์— ์„ธํŒ…๋˜๋Š” ๊ฐ’, request๋Š” body์— ์„ธํŒ…๋˜๋Š” ๊ฐ’, response๋Š” ์‘๋‹ต๊ฐ’์ด๋‹ค.

 

parameters๊ฐ™์€ ๊ฒฝ์šฐ OpenApiParameter๋ฅผ ์‚ฌ์šฉํ•ด ํ‘œ๊ธฐํ•  ์ˆ˜ ์žˆ๊ณ , request์™€ response๋Š” serializer๋กœ ํ‘œํ˜„ํ•  ์ˆ˜ ์žˆ๋Š”๋ฐ, serializer๋กœ ํ‘œํ˜„ํ•˜๋ฉด ์–ด๋–ค ํƒ€์ž…์˜ field์ธ์ง€๋งŒ ๋ณด์—ฌ์ค„ ์ˆ˜ ์žˆ๋‹ค. drf-spectacular์—์„œ ์ œ๊ณตํ•˜๋Š” OpenApiRequest์™€ OpenApiResponse๋ฅผ ํ™œ์šฉํ•˜๋ฉด serializer์ •๋ณด + ์˜ˆ์ œ ์š”์ฒญ/์‘๋‹ต๊ณผ ๊ฐ™์€ ์ถ”๊ฐ€์ ์ธ ์ •๋ณด๋ฅผ ์„ธํŒ…ํ•  ์ˆ˜ ์žˆ๋‹ค.

 

์œ„์— responses์—์„œ 200์ธ๊ฒฝ์šฐ๋Š” LoginTokenSerializer๋ผ๋Š” serializer์ •๋ณด๋ฅผ ๋‚ด๋ ค์ฃผ๊ณ , 400 ์‘๋‹ต์ผ ๊ฒฝ์šฐ BAD_REQUEST_RESPONSE๋ผ๋Š” OpenApiResponse ํด๋ž˜์Šค๋ฅผ ๋‚ด๋ ค์ฃผ๋Š”๋ฐ, ๊ฐ ํด๋ž˜์Šค ๊ตฌํ˜„์€ ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

 

class LoginTokenSerializer(serializers.ModelSerializer):
    class Meta:
        model = LoginToken
        depth = 1
        fields = ['access_token', 'refresh_token', 'expires_in', 'refresh_token_expires_in', 'secret_key']



class BadRequestResponse(OpenApiResponse):
    def __init__(self):
        super().__init__(
            inline_serializer(
                name="BadRequest",
                fields={
                    "error_code": serializers.CharField(),
                    "message": serializers.CharField(),
                    "display_type": serializers.CharField(),
                },
                allow_null=True,
            ),
            description="Bad request",
            examples=[
                OpenApiExample(
                    name="Bad JSON passed in",
                    value={
                        "error_code": "notification",
                        "message": "",
                        "display_type": "event_type and event_parameters fields are required",
                    },
                    status_codes=["400"],
                ),
                OpenApiExample(
                    name="Not good JSON",
                    value={
                        "detail": "JSON parse error - Expecting ':' delimiter: line 2 column 17 (char 18)"
                    },
                    status_codes=["400"],
                ),
            ],
        )

 

 

์œ„ ์ฝ”๋“œ๊ฐ€ swagger์—์„œ ํ‘œํ˜„๋˜๋Š” ํ™”๋ฉด์€ ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

 

์‹ค์ œ๋กœ ๋…ธ์ถœ๋˜๋Š” ํ™”๋ฉด

 

200์ด serializer, 400์ด OpenApiResponse๋ฅผ ์‚ฌ์šฉํ•œ ํ•ญ๋ชฉ์ด๋‹ค. serializer์˜ ๊ฒฝ์šฐ ๊ฐ field์™€ type์ด ์ ํ˜€์žˆ๊ธด ํ•˜์ง€๋งŒ, ๊ตฌ์ฒด์ ์œผ๋กœ ์–ด๋–ค ๊ฐ’์ด ๋‚˜์˜ค๋Š”์ง€๋Š” ํ™•์ธํ•  ์ˆ˜ ์—†๋‹ค. (์‚ฌ์‹ค ์ด ์ •๋„๋งŒ ํ•ด๋„...)   400์˜ ๊ฒฝ์šฐ๋Š” ์‹ค์ œ๋กœ field์— ์–ด๋–ค ๊ฐ’์ด ๋“ค์–ด๊ฐˆ์ง€ ์„ธํŒ…ํ•  ์ˆ˜ ์žˆ์–ด์„œ ๊ฐœ๋ฐœํ•  ๋•Œ ๋” ํšจ๊ณผ์ ์ด๋‹ค.

 

์ •๋ง ์™„๋ฒฝํ•˜๊ฒŒ swagger๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” OpenApiResponse๋ฅผ ๊ฐ API๋งˆ๋‹ค ๋งŒ๋“ค์–ด์ฃผ๋Š”๊ฒŒ ์ข‹๊ฒ ์ง€๋งŒ, ๋Œ€์ถฉ ์–ด๋Š type์ธ์ง€๋งŒ ์•Œ์•„๋„ ๊ฐœ๋ฐœ์ด ๊ฐ€๋Šฅํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๊ณ ๋ฏผํ•ด ๋ณผ ํ•„์š”๊ฐ€ ์žˆ๋‹ค. ๊ฐœ๋ฐœ ํŒ€์˜ ์ปค๋ฎค๋‹ˆ์ผ€์ด์…˜ ๋นˆ๋„๊ฐ€ ๋†’๋‹ค๋ฉด ๊ตณ์ด swagger์— ๋ชจ๋“  ์ •๋ณด๋ฅผ ๋…ธ์ถœํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค๋Š”, ๋”ฑ ํ•„์š”์ •๋ณด๋งŒ ๋ณด์—ฌ์ฃผ๊ณ  ๋‹ค๋ฅธ ๊ฑด ๋‹ค๋ฅธ ์˜์‚ฌ์†Œํ†ต ์ฑ„๋„์„ ํ†ตํ•ด ์ง„ํ–‰ํ•˜๋Š” ๊ฒŒ ๋” ์ข‹์„ ์ˆ˜ ์žˆ๋‹ค. ๋ฐ˜๋Œ€๋กœ ํšŒ์˜ Cost๊ฐ€ ๋†’๋‹ค๊ณ  ์ƒ๊ฐํ•œ๋‹ค๋ฉด (๋ฌผ๋ฆฌ์ ์ธ ์œ„์น˜๊ฐ€ ๋ฉ€์–ด์„œ ?) ์™„๋ฒฝํ•œ Swagger๋ฌธ์„œ๋ฅผ ๊ณต์œ ํ•˜๋Š” ๊ฒƒ์ด ๋” ๋ฐ”๋žŒ์งํ•  ์ˆ˜ ์žˆ๋‹ค. ์ง„๋ฆฌ์˜ ์ผ€๋ฐ”์ผ€

 

 

 

๐Ÿ“ ๋งˆ์น˜๋ฉฐ

 

์ด์ „์— ์‚ฌ์šฉํ–ˆ๋˜ drf-yasg์™€ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์—๋Š” ํฐ ์ฐจ์ด๊ฐ€ ์—†๋‹ค. ํ•˜์ง€๋งŒ OpenApi 3.0 ์ŠคํŽ™์„ ์ œ๊ณตํ•˜๋Š” ๊ฒƒ์€ drf-spectacular๋งŒ ์ œ๊ณตํ•˜๋ฉฐ, drf-yasg๋ฌธ์„œ์—๋Š” drf-spectacular๋ฅผ ์‚ฌ์šฉํ•˜๋ผ๊ณ  ์ ํ˜€์žˆ๋‹ค ใ…Žใ…Ž 

 

swagger๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์ด์œ ๋Š” ๊ฐœ๋ฐœ ํŽธ์˜์ด๊ธฐ ๋•Œ๋ฌธ์—, swagger๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด์„œ ๊ฐœ๋ฐœ์ผ์ •์ด ๊ธธ์–ด์ง„๋‹ค๊ฑฐ๋‚˜ ์ž‘์—…์ด ์–ด๋ ค์šด ๊ฒฝ์šฐ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๊ฒŒ ๋ฐ”๋žŒ์งํ•˜๋‹ค. ์ผ์„ ์œ„ํ•œ ์ผ์„ ํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์ด ์ œ์ผ ์ข‹๋‹ค๊ณ  ์ƒ๊ฐํ•œ๋‹ค.

 

ํ•˜์ง€๋งŒ ํŠน์ • ํด๋ผ์ด์–ธํŠธ๋“ค์€ swagger๋ฅผ ๋ณด๊ณ  ์ž๋™์œผ๋กœ API ํ˜ธ์ถœ ์ฝ”๋“œ๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ๋„ ํ•œ๋‹ค. ๋”ฐ๋ผ์„œ ๊ผญ ํ•„์š”ํ•˜๊ฑฐ๋‚˜, ๊ฐœ๋ฐœ ์ƒ์‚ฐ์„ฑ์ด ๋†’์•„์ง„๋‹ค๋ฉด ๋ฌด์กฐ๊ฑด ์‚ฌ์šฉํ•˜๋Š”๊ฒŒ ์ด๋“์ด๋ผ๊ณ  ์ƒ๊ฐํ•œ๋‹ค.

 

 

๋ฐ˜์‘ํ˜•

๋Œ“๊ธ€

Designed by JB FACTORY