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를 구현하게 됐다.
우선 설정 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_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 에 명시한 반환 포맷을 유지하는 것을 확인할 수 있다.
