Django 프로젝트에서 drf-spectacular을 사용할 때, 전체 API 스키마와 별도로 오직 특정 엔드포인트만 포함한 Swagger UI를 구성하고 싶을 때가 있습니다. (ex. 외부 협력사에 제공할 API, 인증이 필요 없는 공개 API만 따로 구성)
이번 글에서는 drf-spectacular(v0.27.2)의 내부 구성 요소와 이를 확장하여 특정 엔드포인트만 포함된 Swagger UI를 구성하는 방법을 소개하겠습니다.
drf-spectacular 구성 요소
drf-spectacular에서 스키마 생성은 크게 다음 클래스를 통해 이루어집니다.
구성 요소 | 설명 |
EndpointEnumerator | 등록된 모든 Django URL 패턴을 순회하며 API 엔드포인트 수집 |
SchemaGenerator | 수집된 API 엔드포인트를 기반으로 OpenAPI 스펙(JSON 구조)으로 변환하는 역할 |
SpectacularAPIView | 스키마 JSON 반환 API |
SpectacularSwaggerView | Swagger UI 제공 뷰 (정적 스키마 URL을 바탕으로 보여줌) |
위 구성 요소를 보았을 때, EndpointEnumerator를 오버라이딩하여 원하는 엔드포인트만으로 Swagger를 구성할 수 있을 것으로 판단했습니다.
기본 EndpointEnumerator 살펴보기
EndpointEnumerator의 실제 내부 로직은 아래와 같습니다.
class EndpointEnumerator(BaseEndpointEnumerator):
...
def get_api_endpoints(self, patterns=None, prefix=''):
api_endpoints = self._get_api_endpoints(patterns, prefix)
for hook in spectacular_settings.PREPROCESSING_HOOKS:
api_endpoints = hook(endpoints=api_endpoints)
api_endpoints_deduplicated = {}
for path, path_regex, method, callback in api_endpoints:
if (path, method) not in api_endpoints_deduplicated:
api_endpoints_deduplicated[path, method] = (path, path_regex, method, callback)
api_endpoints = list(api_endpoints_deduplicated.values())
if callable(spectacular_settings.SORT_OPERATIONS):
return sorted(api_endpoints, key=spectacular_settings.SORT_OPERATIONS)
elif spectacular_settings.SORT_OPERATIONS:
return sorted(api_endpoints, key=alpha_operation_sorter)
else:
return api_endpoints
...
def _get_api_endpoints(self, patterns, prefix):
"""
Return a list of all available API endpoints by inspecting the URL conf.
Only modification the DRF version is passing through the path_regex.
"""
if patterns is None:
patterns = self.patterns
api_endpoints = []
for pattern in patterns:
path_regex = prefix + str(pattern.pattern)
if isinstance(pattern, URLPattern):
path = self.get_path_from_regex(path_regex)
callback = pattern.callback
if self.should_include_endpoint(path, callback):
for method in self.get_allowed_methods(callback):
endpoint = (path, path_regex, method, callback)
api_endpoints.append(endpoint)
elif isinstance(pattern, URLResolver):
nested_endpoints = self._get_api_endpoints(
patterns=pattern.url_patterns,
prefix=path_regex
)
api_endpoints.extend(nested_endpoints)
return api_endpoints
get_api_endpoints 메서드에서 기본적으로 등록된 모든 URL을 순회하며 포함 여부를 판단하고,
기본적으로는 모든 DRF 기반 엔드포인트를 (path, path_regex, method, callback) 튜플로 수집하여 Swagger 문서에 포함합니다.
path는 실제로 API 문서에 표시될 경로 (ex. /users/) 를 의미하고 path_regex는 정규식 형태의 경로,
method는 http 메서드, callback은 이 URL에 매핑된 View 함수나 클래스를 의미합니다.
EndpointEnumerator 오버라이딩
이제 전체 URL 중 일부만 Swagger 문서에 포함되도록 EndpointEnumerator를 오버라이드합니다.
from drf_spectacular.generators import EndpointEnumerator
class CustomEndpointEnumerator(EndpointEnumerator):
CUSTOM_ENDPOINTS = {
"/open/users/": ["GET"],
"/open/contents/": ["GET"],
}
def get_api_endpoints(self, patterns=None, prefix=''):
api_endpoints = self._get_api_endpoints(patterns, prefix)
for hook in spectacular_settings.PREPROCESSING_HOOKS:
api_endpoints = hook(endpoints=api_endpoints)
api_endpoints_deduplicated = {}
for path, path_regex, method, callback in api_endpoints:
if (path, method) not in api_endpoints_deduplicated:
# 커스텀 엔드포인트만 수집하도록 로직 추가
if self.CUSTOM_ENDPOINTS.get(path) and method.upper() in self.CUSTOM_ENDPOINTS[path]:
api_endpoints_deduplicated[path, method] = (path, path_regex, method, callback)
return sorted(api_endpoints_deduplicated.values(), key=alpha_operation_sorter)
- CUSTOM_ENDPOINTS: 포함할 API 목록을 딕셔너리로 관리 (path: [허용 메서드 리스트])
path를 key, http method를 value로 하는 CUSTOM_ENDPOINTS라는 dict를 만들어서 해당 path, method에 일치하는 엔드포인트만 swagger 문서에 포함되도록 get_api_endpoints 메서드를 변경했습니다.
커스텀 Schema Generator 및 Swagger View 구성
from drf_spectacular.generators import SchemaGenerator
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
class CustomSchemaGenerator(SchemaGenerator):
endpoint_inspector_cls = CustomEndpointEnumerator
class CustomSchemaView(SpectacularAPIView):
generator_class = CustomSchemaGenerator
class CustomSwaggerView(SpectacularSwaggerView):
url_name = "custom-schema"
SchemaGenerator를 상속한 CustomSchemaGenerator는 Swagger 문서에 포함할 엔드포인트를 필터링하기 위해 CustomEndpointEnumerator를 사용하도록 구성하였습니다.
이 커스텀 제너레이터를 사용하는 CustomSchemaView는 필터링된 API만 포함된 OpenAPI JSON 스펙을 반환합니다.
CustomSwaggerView는 Swagger UI를 렌더링하는 뷰로, 내부적으로 url_name="custom-schema"를 통해 CustomSchemaView에서 생성된 JSON 스펙을 가져와 UI에 출력하도록 설정되어 있습니다.
URL 연결
# urls.py
...
urlpatterns += [
path("docs/custom/schema/", CustomSchemaView.as_view(
custom_settings={
'TITLE': 'CUSTOM API',
'DESCRIPTION': 'CUSTOM API Docs',
}
), name="custom-schema"), # JSON API
path("docs/custom/swagger/", CustomSwaggerView.as_view(), name="custom-swagger"), # Swagger UI
]
위에서 CustomSwaggerView는 url_name="custom-schema"를 사용하므로, CustomSchemaView는 반드시 name="custom-schema"로 등록되어야 Swagger UI가 JSON 스펙을 올바르게 가져올 수 있습니다.
이와 같은 구조를 통해 원하는 엔드포인트만 포함한 Swagger UI를 구성할 수 있습니다.
공개용 API 문서, 파트너용 스펙, 내부용 문서를 구분해 제공하고자 할 때 유용하게 활용할 수 있습니다.
단, 이 구성은 EndpointEnumerator를 오버라이딩하므로 향후 drf-spectacular의 버전이 업그레이드될 경우 내부 구현 변경에 따라 호환성 문제가 발생할 수 있음에 유의해야 합니다.
감사합니다.
'DRF' 카테고리의 다른 글
DRF Serializer에서 트리 구조 데이터 캐싱 전략 (9) | 2024.12.28 |
---|