[REAL Python – Django] – “DRF 로 에러 코드 문서화하기 feat. drf-spectacular”
[REAL Python – Django] – “DRF 로 에러 코드 문서화하기 feat. drf-spectacular”
문제 상황
Django, DRF
로 백엔드 API 개발을 하던 중 발생할 수 있는 에러를 문서화해야 하는 상황입니다. 문서화 도구로서 drf-spectacular
를 이용하고 있습니다. 문제는 drf-spectacular
가 자동으로 뷰 단에서 발생하는 에러를 잡아 문서화해주지 않는다는 것입니다.
저는 보통 문제를 맞닥뜨리면 이용 중인 라이브러리의 문서를 뒤져 보고, 그래도 해결방법이 나오지 않는다면 이용 중인 라이브러리의 깃허브 저장소의 이슈에서 키워드를 넣어 검색해 보고, 그래도 없다면 직접 이슈를 남기고, 그래도 안 된다면 처음부터 다시 시작해 보는 식입니다. 그리고 지금 마지막 단계까지 왔습니다.
문제 해결 과정
왜 이 문제가 해결되어야 하는가?
문제의 시작은 구글 로그인을 위한 API
를 구현하는 것이었습니다. djangorestfamework-simplejwt
와 dj-rest-auth
를 이용해서 구글 로그인을 구현하고 있었고, 저는 access token, refresh token
을 같이 발급해 줄 생각이었죠. refresh token
으로 액세스 토큰을 다시 발급받는 뷰를 구현하고 있었습니다.
from rest_framework_simplejwt.views import TokenRefreshView as _TokenRefreshView
class TokenRefreshView(_TokenRefreshView):
"""
클라이언트가 가지고 있는 refresh token 을 이용하여 새로운 access token 을 발급합니다.
아래의 두 가지 상황에 대해서 401 상태 코드를 반환합니다.
- 유효한 토큰이 아닌 경우.
- 토큰은 유효하지만 만료된 경우.
"""
@extend_schema(
summary="클라이언트가 가지고 있는 refresh token 을 이용하여 새로운 access token 을 발급합니다.",
)
def post(self, request, *args, **kwargs):
return super().post(request, *args, **kwargs)
위와 같은 뷰를 만들었습니다. 그리고 drf-spectacular
는 아래와 같은 멋진 OpenAPI
사양을 만들어냈죠.
맞습니다. 토큰 재발급을 위해서 단순히 refresh token 만을 요구합니다. 이 상황에 대해서는 문서화가 잘 되어 있죠. 하지만 이러한 문서화는 조금 부족할 수 있습니다. 토큰 재발급을 하는 중에는 여러 가지 예외가 발생할 수 있는 노릇이기 때문입니다. 예컨대 아래와 같을 수 있습니다.
- 가장 단순하게 “토큰 자체가 유효하지 않은 경우” 를 생각해볼 수 있습니다.
- 아니면 이런 경우도 있겠죠? “토큰은 유효한데, 만료기간을 지나버린 경우”.
저는 백엔드 개발자로서 위의 상황들을 적절히 문서화하여 프론트엔드 개발을 맡고 있는 사람들에게 이를 알려줄 책임을 가지고 있습니다. 만약 이런 상황을 알려주지 않는다면 개발이 늦어질 수도 있고, 적절한 예외 처리가 이루어지지 못할 수도 있기 때문이었죠. 문서 없는 api 를 사용하는 상상을 해 보셨습니까? 저는 일일이 날아오는 응답을 console.log
로 찍을 자신이 없습니다.
사실 문서화가 안 되어 있을 뿐이지 제 api 는 적절한 json 응답을 해 주기는 합니다. 만약 적절하지 못한 토큰과 함께 응답을 받는다면 아래와 같은 응답을 줄 겁니다.
그러면 또 궁굼증이 생깁니다!
401 Error 는 어디에서 오는 것인가?
그러면 대체 위의 메시지는 어디에서 오고, 저건 어떻게 처리되길래 무미건조한 json
으로 변환되어 응답이 오는 걸까요? 무언가 깔끔한 처리지만 내가 한 게 없다는 것은 프레임워크에서 무언가 마법을 수행하고 있다는 신호입니다. 그리고 우리가 사는 세계에서 마법이란 존재하지 않죠.. 코드가 존재할 뿐입니다.
이는 djangorestframework-simplejwt
속으로 들어가 봐야 알 수 있습니다.
제가 작성한 뷰는 위와 같은 상속 관계를 가집니다. 위로 타고 타고 올라가다 보면 rest framework
의 generic
뷰를 상속받아 사용하고 있는 것을 볼 수 있네요. 중요한 것은 맨 위 TokenViewBase
의 post
메서드입니다.
post 메서드에서 serializer
는 is_valid
를 호출합니다. 메서드명이 말해주듯 is_valid
메서드는 serializer
에서 제공하는 메서드입니다. 이는 어떤 것도 아닌 django REST framework
의 나와바리 안에 있는 메서드죠. 이 포스팅에서 is_valid
가 어떻게 동작하는지 깊게 다루지 않을 것이지만, 대략적으로 아래와 같은 동작을 합니다. 요약하자면 is_valid
는 serializer
내부의 validate
를 호출해 데이터의 유효성을 검증합니다.
그렇다면 serializer 의 validate
가 어떻게 생겼는지가 중요하겠습니다. 아래와 같이 생겼네요. validate
메서드가 시작하고 나서 self.token_class
로 리프레시 토큰 인스턴스를 만드는 것부터 시작합니다! 토큰 인스턴스는 RefreshToken
이죠?
그리고 djangorestframework-simplejwt
의 RefreshToken
클래스는 Token
을 상속받아 구현되었습니다.
토큰 인스턴스는 위의 과정을 통해서 만들어집니다. 우리가 본 에러 코드가 보이네요. settings.py
에 정의된 토큰 백엔드를 이용해서 들어온 값을 검증합니다. self.payload = token_backend.decode()
그리고 그것에 대해서 검증이 실패하면 TokenError
를 발생시키죠. 이제 마음놓고 돌아가 봅시다.
위의 글을 읽고 나면, 이제 post
메서드가 어떤 일을 하는지 잘 보이실 겁니다. post
메서드는 해당 뷰 객체가 가지고 있는 serializer
로 클라이언트로부터 들어온 토큰을 검증한 다음, 토큰이 문제가 없다면 액세스 토큰과 리프레시 토큰 (이 경우는 simplejwt
의 TokenRefreshView
가 자신의 serializer
로 TokenRefreshSerializer
를 가지고 있기 때문에 동작하는 것입니다. 위의 TokenViewBase
가 직접 그 역할을 하는 것은 아니겠죠?) 을 발급하는 과정을 거치게 됩니다. 그리고 발급하는 과정에서, 클라이언트가 아래와 같은 잘못된 refresh token
을 가져온다면 InvalidToken
을 raise
하게 되죠.
그리고 InvalidToken
예외는 아래처럼 생겼습니다. 내부적으로는 drf 의 APIException
을 상속받아 만들어져 있습니다.
우리가 봤던 문구는 저기서 왔던 거군요. drf-spectacular
는 serializer
를 찾아 문서화를 수행합니다. 우리의 serializer
들에는 에러 상황을 처리하는 코드는 없었으므로 문서화가 안 되었던 거였겠네요.
Django REST Framework 의 error handler
하지만 그렇다고 해서 우리 의문이 완전히 해결된 건 아닙니다. python
에서 raise
는 분명히 예외를 발생시키는 코드이기 때문입니다. 예외가 발생한다는 것 == 해당 예외가 가지고 있는 메시지들을 적절하게 json 으로 응답해 주는 것 은 아니죠. 이는 Django REST Framework
에서 적절하게 그것들을 처리해 주기 때문입니다.
https://www.django-rest-framework.org/api-guide/exceptions/
Django REST Framework
는 APIView
에서 발생하는 예외를 처리하기 위한 가장 기본 클래스인 APIException
클래스를 제공합니다. 아래와 같이 생겼죠. 개발자들은 이 클래스를 상속해 REST framework 가 처리할 수 있는 예외를 정의할 수 있습니다.
위처럼 발생한 예외는 Django REST Framework
의 rest_framework.views.exception_handler
에 의해 Response
객체로 만들어집니다. 기본 예외 핸들러는 아래와 같이 생겼는데, APIException
을 상속받은 예외가 발생하면 exc.detail
과 header
, status_code
를 받아 Response
객체를 만드는 과정을 수행하죠. 만약 발생한 예외가 REST Framework 의 것이 아니면, 바로 Django 의 것을 반환하는 것을 볼 수 있습니다. (맨 처음 2개의 분기문)
그러면 예외가 어떻게 생기고, 어떻게 http 응답으로 변환되는지까지 배웠습니다. 이제 할 것은, 이걸 적절하게 문서화하는 겁니다.
예외 전용 Serializer 작성하기
어차피 drf-spectacular
로 문서화를 할 것이라면 Serializer 는 필수로 작성해야 합니다. 어차피 모든 예외는 details
라는 속성을 가지고 있습니다. exc.data 에 들어있는 정보들을 잘 모아서, 우리가 원하는 형태의 serializer 를 정의할 수 있겠어요.
class BaseExceptionSerializer(serializers.Serializer):
exception_class = exceptions.APIException
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["code"].default = self.exception_class.default_code
self.fields["status_code"].default = self.exception_class.status_code
self.fields["detail"].default = self.exception_class.default_detail
code = serializers.CharField()
status_code = serializers.IntegerField()
detail = serializers.CharField()
먼저 이런 형태의 시리얼라이저를 작성해 줍시다. 자신의 exception_class 에 의해서 필드의 기본값을 정해줄 겁니다.
class InvalidTokenExceptionSerializer(BaseExceptionSerializer):
exception_class = InvalidToken
그리고 위처럼 exception_class 를 정해주면, code, status_code, detail
에 대한 기본값들이 정해지고, 그것은 drf-spectacular
에 의해 문서화될 겁니다.
이제 이렇게 원하는 상태 코드와 시리얼라이저를 작성하면,
딱 우리가 원하는 방향으로 문서화가 된 것을 확인할 수 있네요!
주의할 점
이 방법은 문서화가 잘 먹히지만, WAS가 내뱉는 응답이 정확히 그것과 일치하는지 확인해야 합니다. 실제로 저의 경우에도 문서화된 대로 응답이 오지 않습니다.
이렇게 exception 으로부터, 원하는 형태의 응답을 받기 위해서는 Django REST Framework
가 제공하는 기본 error handler
를 재정의할 수 있습니다. 아마 다음 포스팅은 그것이 될 것 같네요. 사실 현재는 500번대 에러가 발생하면, 그리 보기 좋지 않은 형식으로 에러가 발생하기 때문입니다.
완벽하게는 다음에!