Django Rest Framework를 사용한 API 버전 관리

허용된
Google에서 "REST API 버전 관리"를 빠르게 검색하면 요청에 버전을 지정하는 방법에 대한 많은 논의가 나타납니다. RESTful 원칙과 URL에 버전 번호를 삽입하는 것, 사용자 정의 요청 헤더를 사용하는 것, 기존 승인 헤더를 활용하는 것이 최선의 방법인지에 대한 장단점에 대한 열정적인 논쟁이 부족하지 않습니다. 안타깝게도 없습니다. 모두를 만족시킬 수 있는 하나의 정답. 어떤 접근 방식을 사용하게 되든 일부 캠프에서는 귀하가 잘못하고 있다고 주장할 것입니다. 사용되는 접근 방식에 관계없이 요청에서 버전이 구문 분석되면 다양한 버전에서 사용되는 다양한 스키마를 지원하기 위해 API 코드를 관리하는 방법에 대한 또 다른 더 큰 질문이 있습니다. 놀랍게도 이를 가장 잘 수행하는 방법에 대한 논의는 거의 없습니다.
Rescale에서는 다음과 같은 API를 구축했습니다. Django Rest 프레임워크. 다가오는 3.1 릴리스에서는 일부 API 버전 관리 지원을 제공할 예정입니다. 프로젝트 리더는 API 빌더가 요청에 버전을 지정하기 위한 다양한 전략 중에서 선택할 수 있는 프레임워크를 제공하여 버전 관리 논쟁을 회피하기로 현명하게 결정했습니다. 그러나 코드에서 이 버전을 처리하는 모범 사례에 대한 공식적인 지침은 많지 않습니다. 실제로 문서에서는 "행동을 어떻게 변화시키는지는 귀하에게 달려 있습니다"라고 말함으로써 대부분 문제에 대해 설명합니다.
Stripe의 엔지니어 중 한 명이 좋은 글을 올렸습니다. 대략적인 요약 Stripe이 API와의 하위 호환성을 처리하는 방법에 대해 설명합니다. 저자는 요청과 응답이 전달되는 변환 파이프라인을 설명합니다. 이 접근 방식의 주요 매력 중 하나는 핵심 API 논리가 항상 API의 현재 버전을 처리하고 다양한 버전을 처리하기 위한 별도의 "호환성" 레이어가 있다는 것입니다. 클라이언트의 요청은 "요청 호환성" 레이어를 통과하여 핵심 API 로직으로 전달되기 전에 현재 스키마로 변환됩니다. 핵심 API 로직의 응답은 "응답 호환성" 레이어를 통해 전달되어 응답을 요청된 버전의 스키마 형식으로 다운그레이드합니다.
이 게시물에서는 DRF를 통해 이러한 유형의 변환 파이프라인을 지원하기 위한 잠재적인 접근 방식을 살펴보고 싶습니다. 그런 다음 DRF에 변환 논리를 삽입하는 자연스러운 위치는 요청 데이터(Serializer.to_internal_value)의 유효성을 검사하고 APIView 메서드(Serializer.to_representation)에서 반환할 응답 데이터를 준비하는 데 관여하는 Serializer 내입니다.
일반적인 아이디어는 요청 데이터에 적용되어 현재 스키마로 변환할 일련의 변환을 생성하는 것입니다. 마찬가지로 응답 데이터는 역순으로 변환을 통해 전달되어 현재 스키마의 데이터를 클라이언트가 요청한 버전으로 변환합니다. 이는 결국 데이터베이스 마이그레이션의 정방향 및 역방향 방법과 매우 유사해 보입니다.
응답 변환이 사용되는 방법에 대한 기본 예로서 다음은 메일링 목록 이름과 구독자 이메일 목록을 반환하는 간단한 직렬 변환기입니다.

클래스 MailingListSerializer(serializers.ModelSerializer): 구독자 = serializers.SerializerMethodField('_subscribers') def _subscribers(self, obj): return [obj.subscribers_set.all()의 s에 대한 s.email] 클래스 메타: 모델 = models.MailingList 필드 = ('이름', '설명', '구독자')

이 직렬 변환기를 사용하는 엔드포인트의 페이로드는 다음과 같은 형식의 JSON을 반환할 수 있습니다.

{'이름': '고양이 사실', '설명': '고양이에 관한 재미있는 사실', '구독자': ['joe@email.com', 'jane@email.com']}

얼마 후 우리는 이 끝점이 각 구독자가 메일링 목록에 등록한 날짜도 반환해야 한다고 결정했습니다. 구독자 배열의 각 요소가 이제 간단한 문자열이 아닌 객체가 되므로 이는 원본 버전의 API를 사용하는 모든 클라이언트에 대한 주요 변경 사항이 될 것입니다.

{'name': '고양이에 관한 사실', 'description': '고양이에 관한 재미있는 사실', '구독자': [ {'email': 'joe@email.com', 'date_subscribed': '2015-01-15T00: 01:34Z'}, {'이메일': 'jane@email.com', 'date_subscribed': '2015-02-18T04:57:56Z'} ]}

이 새로운 형식으로 데이터를 반환하려면 직렬 변환기를 업데이트해야 합니다. API의 원본 버전과의 하위 호환성을 지원하려면 VersioningMixin 클래스에서 파생되도록 수정하고 Transform 클래스의 위치를 ​​지정해야 합니다(자세한 내용은 나중에 설명).

class MailingListSerializer(VersioningMixin, serializers.ModelSerializer): 변환_base = 'api.transforms.mailinglist.MailingListSerializerTransform' 구독자 = serializers.SerializerMethodField('_subscribers') def _subscribers(self, obj): return [{'email': s.email, 'date_subscribed': s.date_subscribed} for s in obj.subscribers_set.all()] 클래스 메타: 모델 = models.MailingList 필드 = ('name', 'subscribers')

이 직렬 변환기에 대한 새 API 버전이 도입될 때마다 api.transforms.mailinglist 모듈에 새로운 번호가 지정된 변환 클래스를 추가해야 합니다. 각 변환은 응답 데이터 dict를 삭제하여 버전 N을 버전 N-1로 다운그레이드하는 작업을 처리합니다.

class MailingListSerializerTransform0001(Transform): def update_response_data(self, data, request, source): # 원시 v2 입력 사전을 v1 사전으로 변환 data['subscribers'] = [s['email'] for s in data['subscribers ']]

Transform 클래스는 스키마 마이그레이션과 유사하며 요청 및 응답 데이터를 변환하는 메서드를 포함합니다. 각 변환 클래스 이름에는 접미사로 숫자 값이 있어야 합니다. VersioningMixin 클래스는 이를 사용하여 요청 또는 응답 데이터에 변환이 적용되어야 하는 순서를 식별합니다.

class Transform(object): def update_request_data(self, data, request): """data는 클라이언트의 요청을 구문 분석한 결과인 기본 사전입니다. """ pass def update_response_data(self, data, request, source ): """data는 렌더러에 전달되어 클라이언트로 다시 반환되는 기본 사전입니다. 소스는 기본 사전으로 변환되는 원본 개체입니다. """ pass

VerisoningMixin 클래스는 요청을 현재 API 버전으로 변환하거나 응답을 현재 API 버전에서 요청한 버전. 다음 코드 조각에서 settings.API_VERSION은 최신 API 버전 번호를 나타내며 request.version 필드는 클라이언트에서 요청한 API 버전으로 설정됩니다.

class VersioningMixin(object): def to_internal_value(self, data, *args, **kwargs): """현재 API 버전으로 업데이트될 때까지 지정된 요청 데이터에 대해 변환 파이프라인을 적용합니다. """ if data: request = self.context['request'] for v in range(request.version, settings.API_VERSION): self._get_transform(v).update_request_data(data, request) return super(VersioningMixin, self).to_internal_value(data, * args, **kwargs) def to_representation(self, obj, *args, **kwargs): """원하는 API 버전으로 다운그레이드될 때까지 지정된 응답 데이터에 대해 변환 파이프라인을 적용합니다. """ data = super (VersioningMixin, self).to_representation(obj, *args, **kwargs) if obj: request = self.context['request'] for v in range(settings.API_VERSION, request.version, -1): t = self ._get_transform(v - 1) t.update_response_data(data, request, obj) 반환 데이터 def _get_transform(self, version): 변환_dict = {v: c for v, c in self._transform_classes()} TransformKlass = 변환_dict.get( version, Transform) return TransformKlass() def _transform_classes(self): module, base = self.transform_base.rsplit('.', 1) mod = import_module(module) for name, Klass inspect.getmembers(mod): if name .startswith(base) 및 issubclass(Klass, Transform): m = re.search('\d+ 이 접근 방식의 주요 이점은 APIView(일반적으로 핵심 API 논리를 구현하고 요청/응답 처리를 위해 직렬 변환기를 사용하는 클래스)입니다. ) 최신 스키마 버전만 걱정하면 됩니다. 또한 변환을 작성하려면 API의 현재 및 이전 버전에 대한 지식이 필요합니다. 특정 응답의 버전 10을 생성할 때 v10과 v9 사이에 생성해야 하는 변환은 하나만 있습니다. v7을 요청하는 요청은 먼저 새로운 변환을 통해 v10에서 v9로 변환됩니다. 기존 v9에서 v8로, v8에서 v7로의 변환이 나머지를 처리합니다. 우리는 이것이 앞으로 발생할 모든 이전 버전과의 호환성 문제에 대한 만병통치약이라고 믿지 않습니다. 잠재적으로 비용이 많이 드는 일련의 변환을 통해 요청과 응답을 지속적으로 실행해야 하는 경우 고려해야 할 몇 가지 성능 문제가 확실히 있습니다. 또한 데이터베이스 스키마 변경에 대한 역방향 마이그레이션을 생성하는 것이 때때로 불가능한 것과 마찬가지로 이 접근 방식으로는 쉽게 해결할 수 없는 더 복잡한 주요 API 변경 사항이 있습니다. 그러나 기본 API 변경의 경우 이는 문제를 격리하고 버전 관리 목적으로 핵심 API 논리 내부에 조건부 goop을 포함하지 않는 좋은 방법이 될 수 있는 것처럼 보입니다. , 이름) if m: Yield (int(m.group(0)), Klass)

이 접근 방식의 주요 이점은 APIView(일반적으로 핵심 API 논리를 구현하고 요청/응답 처리를 위해 직렬 변환기를 사용하는 클래스)가 최신 스키마 버전에 대해서만 걱정하면 된다는 것입니다. 또한 변환을 작성하려면 API의 현재 및 이전 버전에 대한 지식이 필요합니다. 특정 응답의 버전 10을 생성할 때 v10과 v9 사이에 생성해야 하는 변환은 하나만 있습니다. v7을 요청하는 요청은 먼저 새로운 변환을 통해 v10에서 v9로 변환됩니다. 기존 v9에서 v8로, v8에서 v7로의 변환이 나머지를 처리합니다.
우리는 이것이 앞으로 발생할 모든 이전 버전과의 호환성 문제에 대한 만병통치약이라고 믿지 않습니다. 잠재적으로 비용이 많이 드는 일련의 변환을 통해 요청과 응답을 지속적으로 실행해야 하는 경우 고려해야 할 몇 가지 성능 문제가 확실히 있습니다. 또한 데이터베이스 스키마 변경에 대한 역방향 마이그레이션을 생성하는 것이 때때로 불가능한 것과 마찬가지로 이 접근 방식으로는 쉽게 해결할 수 없는 더 복잡한 주요 API 변경 사항이 있습니다. 그러나 기본 API 변경의 경우 이는 문제를 격리하고 버전 관리 목적으로 핵심 API 논리 내부에 조건부 goop을 포함하지 않는 좋은 방법이 될 수 있는 것처럼 보입니다.

비슷한 게시물