Search

Django API Custom error exception standardization

카테고리
Back-end
태그
Django
게시일
2022/12/14
수정일
2024/02/24 09:41
시리즈
1 more property

1. Error exception standard

Django Rest Framework를 이용하여 증권사와 연동하는 FEP 서버를 개발하면서 여러 에러를 경험하게 되었습니다. 다양한 상황에서 발생하는 에러를 캐치하고 때에 따라 Front-end와 에러가 발생할 경우 필요시 에러 문구 워싱작업이 필요하다는 요구사항을 받아, 이러한 에러를 처리하기 위한 표준을 잡을 필요가 있음을 인지하였습니다.

Error Format

에러 포맷의 형태는 다음과 같이 잡았습니다.
code의 경우 Front-end 혹은 로깅된 에러의 발생 위치를 찾기 위함
message 에러에 대한 요약 메시지를 나타냄
detail 상세한 에러 메시지를 나타내며, 메시지 워싱 작업이 없을 경우 해당 메시지를 보여줌
{ "code": "FEP-NONE-00000", "message": "Message of this error.", "detail": "Detail Message of this error." }
JSON
복사

Code

코드 규칙은 아래와 같이 설정하였습니다. MSA 서비스 형태로 운영되고 있어, 각 API 서버마다의 기능, 넘버링을 고려하여 설정하였습니다.
code : service "-" feature "-" number service : MSA API Service feature : Feature of API Service number : Error numbering
Plain Text
복사

Message

메시지의 경우 실제 Detail의 요약 정리된 내용으로, 간략한 내용만 정리하는 영역입니다.

Detail

DRF에서는 APIException의 처리를 아래와 같이 공통으로 묶어 처리하고 있습니다. default_detail의 경우 HTTP body 부분에 detail에 포함하여 내려주는 부분입니다.
class APIException(Exception): """ Base class for REST framework exceptions. Subclasses should provide `.status_code` and `.default_detail` properties. """ status_code = status.HTTP_500_INTERNAL_SERVER_ERROR default_detail = _('A server error occurred.') default_code = 'error'
Python
복사

2. Customizing

CustomAPIException

아래의 코드는 CustomAPIException 코드를 작성한 내용과 해당 클래스를 상속받아 사용하는 예시를 작성한 내용입니다.
from rest_framework import status from rest_framework.exceptions import APIException from enum import Enum class Contexts(Enum): code = 0 message = 1 detail = 2 @classmethod def get_members(cls): return cls.__members__.keys() @classmethod def get_context(cls, context): return cls.__getitem__(context).value class CustomAPIException(APIException): status_code = 400 _msg = [] _map = dict( code="FEP-NONE-00000", message="없는 에러 메시지", detail="정의된 에러 메시지가 없습니다." ) def __init__(self, **kwargs): if kwargs: self._update_map(items=kwargs.items()) if self._msg: for i, k in enumerate(self._map.keys()): self._map[k] = self._msg[i] self.detail = self._map def _update_map(self, items): """ :param items: dict to items() :comment: if any `Context` in items, update it * Context : `code` or `message` or `detail` """ for k, v in items: if k in Contexts.get_members(): self._msg[self._get_context_value(k)] = v @staticmethod def _get_context_value(context): return Contexts.get_context(context)
Python
복사
CustomAPIException 코드는 기존 APIException 코드를 상속받아 self.detail에 에러 메시지를 업데이트하는 코드입니다.
위의 코드를 보면 어색한 코드 부분이 있으리라 생각됩니다. 개발하고 동료와 논의할 때 Class 내의 변수인 _map: dict, _msg: list 변수들에 대한 선언이 반드시 필요한지 의견을 물어보았습니다.
이렇게 정의한 이유는 아래와 같습니다.
코드의 효율성 보다는 상속받을 다른 Exception 클래스를 정의할 때 보다 간결하게 정의하고자 함
코드를 한 눈에 보고 파악하는데, dict 형태로 코드를 작성하게 되면 난잡해보여 이와 같이 list 형태의 변수를 추가로 작성하여 사용
CustomAPIException내의 list 변수를 사용하기 위해 어쩌면 불필요해 보에는 Contexts 클래스를 필요로 하게 되었습니다. 그러나 여러 Exception 코드를 선언하고 사용하고자 할 때, 코드를 한 눈에 보기 쉽게 작업하기 위해 필요하다고 판단하였고, 적용했을 때 만족스러운 결과라고 생각되었습니다.

Usage with _msg variable

from .base import CustomAPIException class UnknownErrorException(CustomAPIException): status_code = status.HTTP_403_FORBIDDEN class VendorAuthorizationFailed(CustomAPIException): status_code = status.HTTP_403_FORBIDDEN _msg = ["FEP-SEC-01001", "증권사 정보 에러", "증권사 인증 정보를 가져오는 과정에서 에러가 발생하였습니다."] # ....
Python
복사

Usage without _msg variable

from .base import CustomAPIException class UnknownErrorException(CustomAPIException): status_code = status.HTTP_403_FORBIDDEN class VendorAuthorizationFailed(CustomAPIException): status_code = status.HTTP_403_FORBIDDEN _map = dict( code="FEP-SEC-01001", message="증권사 정보 에러", detail="증권사 인증 정보를 가져오는 과정에서 에러가 발생하였습니다." ) # ....
Python
복사

CustomException

위와 같이 CustomAPIException을 내부에 정의하고 API를 사용할 때 예외가 발생하는 경우, 구분이 가능하도록 하였습니다. 만약 API를 호출하여 에러가 발생했을 때, 예외처리 시 Json 형태를 가진 응답값을 기대할 수 있게 되었습니다. 이러한 가정을 통해 배치 스크립 서비스의 로깅도 좀 더 상세하게 할 수 있을 것입니다.
사내의 배치 스크립트 서비스가 있고 해당 스크립트가 API를 호출할 때, 에러가 발생하는 경우는 다양하게 발생합니다.
API 호출 시 필요한 데이터가 잘못되었을 때
API 호출 시 조건이 잘못되었을 때
연결된 FEP(대외계 서비스)의 응답이 원활하지 않을 때
위의 예시와 다른 다양한 원인이 있을 수 있지만, 이러한 원인을 어느정도 파악하고 이슈를 처리하는 것이 바람직하다고 생각됩니다. 그러기 위해서는 어떠한 원인으로 이슈가 발생했는지 사전 정보의 중요도가 높다고 판단하였습니다.
from enum import Enum class CustomException(Exception): _map = dict( code="SYNC-NONE-00000", message="없는 에러 메시지", detail="정의된 에러 메시지가 없습니다." ) def __init__(self, **kwargs): if kwargs: self._update_map(items=kwargs.items()) def __str__(self): return f"{self._map}" def _update_map(self, items): """ :param items: dict to items() :comment: if any `Context` in items, update it * Context : `code` or `message` or `detail` """ for k, v in items: self._map[k] = v
Python
복사
CustomAPIException과 다르게, CustomException의 경우 기본 Exception을 base로 작성하였습니다. 이는 python requests 모듈을 이용하여 API 통신시 API 서버에서 발생할 수 있는 에러를 배치 내에 로깅하기 위한 코드입니다.

Usage

from .base import CustomException class ParameterUnMatched(CustomException): pass class FailedToRefreshCache(CustomException): pass
Python
복사

Batch examples

만약 배치 작업 과정에서 특정 데이터가 반영되지 못했는지 체크하기 위해 try except 구문을 이용하여 로깅이 가능합니다. 그렇지 않고, 배치 작업 과정에서 이슈가 있을 시 멈춰야 한다면, raise Exception 형태로 에러를 확인하고 배치를 실패처리 할 수도 있습니다.
아래의 코드는 예시로 작성한 배치 코드입니다.
import logging from .common.exceptions import ( ParameterUnMatched, FailedToRefreshedCache ) from fep_manager import FEPManager logger = logging.getLogger() def main(): fep_manager = FEPManager(url="SOME-URL", token="TOKEN") page = 1 data = [] success = 0 failed = 0 while res is None or (res.get("links") and res.get("links").get("next")): res = fep_manager.get_data(page) data += res.get("records") page += 1 for d in data: try: response = fep_manager.set_something(_json=d) logger.info(f"{d} is successfully set.") success += 1 except FailedToSetSomething as e: logger.error(f"Failed to set `{d}` - {e}") failed += 1 logger.info(f"FEP setting : Total( {success+failed} ), Success( {success} ), Failed( {failed} )") logger.info(f"Exit.")
Python
복사

3. Review

Django Rest-Framework를 이용하여 MSA 형태로 서비스를 개발하면서, 에러가 발생할 때 각 서비스별로 구분할 수 있어야 한다는 니즈가 생겼습니다. 아무래도 운영 이슈를 확인하고 처리하는 데 있어서, 1차적으로 어떠한 에러가 어디서 발생했는지 확인하는 것이 중요하기 때문입니다. 이러한 필요성을 느껴 에러에 대한 표준화작업을 하는 것을 목표로 코드화 작업을 진행했습니다.
기존에 서버를 개발하며 외부 증권사와 통신할 때 발생하는 에러의 경우 API Response에 에러 코드를 내려주는 작업으로, Serializer를 사용하였습니다. 그러나 추후 인프라가 고도화됨에 따라 이러한 방법으로 에러를 핸들링하는 것은 적절하지 않다고 판단하게 되었습니다. 때문에 각 서비스별 에러 로깅을 위하여 다음과 같은 내용을 고려하여 CustomAPIException 으로 코드를 작성하고자 하였습니다.
ELK 등 로깅 과정에서 분류를 쉽게 하기 위함
코드 레벨에서 각 에러코드로 구분하지 않고 Exception 클래스 이름으로 구분하여 보다 빠른 처리가 가능하기 위함
필요하다면 code를 이용하여 프론트 레벨에서 메시지 워싱이 가능하도록 하기 위함
각 API 서비스별 기능과 넘버링을 통해 에러 코드와 메시지를 구분할 수 있도록 하고, 보다 직관적인 코드 작업을 위해 이를 CustomAPIException을 정의하였습니다. 이를 통해 추후 서비스 고도화와 적절한 에러 트래킹이 가능하도록 구현해보고자 했습니다.