[Django] Custom Exception Handler 구현하기

김동욱·2024년 2월 7일

Django

목록 보기
2/6
post-thumbnail

DRF로 API 개발을 하면서 에러 응답 포맷의 통일화 대한 필요성을 느꼈다. 보통의 경우 에러 발생 시 다음과 같은 응답을 반환했지만

HTTP 400 Bad Request
Content-Type: application/json

{
    "detail": "Invalid data received"
}

이런 식으로 포맷이 다른 경우도 있었다.

HTTP 400 Bad Request
Content-Type: application/json

{
    "username": [
        "This field is required."
    ],
    "password": [
        "This field is required."
    ]
}

Django 자체와 Django와 관련된 모듈들, 특히 Django REST Framework(DRF) 같은 서드파티 모듈에서는 에러 응답 포맷이 다를 수 있기 때문에, 통일된 응답 포맷을 위해 Custom Exception Handler를 구현하게 됐다.

Custom Exception Handler 구현하기

우선 설정 REST_FRAMEWORK 설정에 다음과 같이 구현한 custom_exception_handler 위치를 설정한다. 필자는 Django 프로젝트의 설정 디렉토리 내에 utils 모듈을 생성하여 내부에 custom_exception_handler 를 구현했다. 따라서 해당 위치를 작성한다.

REST_FRAMEWORK = {
	...
    
    'EXCEPTION_HANDLER': 'config.utils.custom_exception_handler',
    
    ...
}

custom_exception_handler 를 다음과 같이 구현했다. 아래의 커스텀 예외처리 핸들러를 요약하자면 기존 예외처리 내부에 detail 키가 존재하지 않는다면 에러 문구를 통합한다. 통합된 에러 문구는 일관된 형식으로 작성되어 반환된다.

def custom_exception_handler(exc, context) -> Response:
    response = exception_handler(exc, context)

    if isinstance(exc, APIException):
        message: str = ""
        if response is not None:
            if "detail" not in response.data:
                error_messages: list = []
                for messages in response.data.values():
                    if isinstance(messages, str):
                        error_messages.append(messages)
                    elif isinstance(messages, list):
                        error_messages.extend(messages)
                    else:
                        error_messages.append(str(messages))
                message = " ".join(error_messages)
            else:
                message = response.data.get("detail")

        detail: str = exc.detail if not message else message
        custom_response_data: dict[str, Union[int, str]] = {
            "status_code": exc.status_code,
            "code": exc.default_code,
            "detail": detail
        }
        
        return Response(custom_response_data, status=exc.status_code)

Custom Exception raise 하기

위에서 작성한 custom_exception_handler 는 예상치 못한 에러에 대해 일관된 포맷을 유지한다. 모든 예외 처리 사항을 모듈에 맡기기 보다는, 특정 상황에 맞춰 예외를 발생시키는 것이 문제를 더 효과적으로 파악하고 해결할 수 있다. 이를 위해 프로젝트의 설정 디렉토리 내에 exceptions 모듈을 생성하고, 여기에 커스텀 예외 클래스들을 정의할 수 있다. 이 클래스들은 Django REST Framework의 APIException을 상속받아야 한다. 이렇게 하면 DRF의 예외 처리 시스템과의 호환성을 보장하고, 필요한 경우 DRF의 기능을 활용하여 예외를 더욱 효과적으로 처리할 수 있다.

[프로젝트 설정 디렉토리/exceptions.py]

class UserNotExistException(APIException):
    status_code = status.HTTP_404_NOT_FOUND
    default_detail = "존재하지 않는 사용자입니다."
    default_code = "user_not_exist"


class NotExistException(APIException):
    status_code = status.HTTP_404_NOT_FOUND
    default_detail = "존재하지 않는 자원입니다."
    default_code = "not_exist"


class InternalServerException(APIException):
    status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
    default_detail = "서버 내부에서 발생한 오류입니다."
    default_code = "internal_server_error"

이렇게 작성한 예외는 아래와 같이 사용할 수 있으며 에러 발생 상황을 더 명확하게 표현할 수 있다.

@action(detail=True, methods=["patch"])
def accept(self, request: Request, pk: Optional[int] = None, *args: Any, **kwargs: Any) -> Response:
      attendance_object: Optional[Attendance] = Attendance.objects.filter(id=pk).first()

      if not attendance_object:
            raise NotExistException()

      if attendance_object.request_processed_status is not None:
            raise InvalidFieldStateException("이미 처리된 요청입니다.")
      ...

이렇게 발생시킨 예외처리는 custom_exception_handler 에 명시한 반환 포맷을 유지하는 것을 확인할 수 있다.

profile
안녕하세요! 질문과 피드백은 언제든지 환영입니다:)

0개의 댓글