에러메세지
- 이전 프로젝트에서는 각 앱별로
exceptions.py를 만들어서 관리했는데 이번 프로젝트에서는 core에서는 관리
메시지 정의
에러 발생
- 비즈니스 로직에서
raise BaseCustomException(ErrorMessage.LOGIN_FAILED)를 호출
포맷 변환
handler.py가 이를 가로채서
- 클라이언트가 보기 편하게
error_detail과 code가 포함된 JSON으로 변환
에러 메시지 정의
class ErrorMessage(Enum):
"""
프로젝트 전체 에러 메시지 정의 (상태 코드, 식별 코드, 메시지)
"""
SYSTEM_ERROR = (
status.HTTP_500_INTERNAL_SERVER_ERROR,
"server_error",
"서버 내부 오류가 발생했습니다.",
)
INVALID_INPUT = (
status.HTTP_400_BAD_REQUEST,
"invalid_input",
"유효하지 않은 입력값입니다.",
)
PERMISSION_DENIED = (
status.HTTP_403_FORBIDDEN,
"permission_denied",
"권한이 없습니다.",
)
NOT_FOUND = (
status.HTTP_404_NOT_FOUND,
"not_found",
"요청한 리소스를 찾을 수 없습니다.",
)
...
@property
def status_code(self): return self.value[0]
@property
def code(self): return self.value[1]
@property
def message(self): return self.value[2]
공통 예외 클래스
- Enum을 받아 에러를 던지는 도구를 만듭니다.
from rest_framework.exceptions import APIException
class BaseCustomException(APIException):
def __init__(self, error_enum):
self.status_code = error_enum.status_code
self.default_code = error_enum.code
self.default_detail = error_enum.message
super().__init__(detail=self.default_detail, code=self.default_code)
핸들러
logger = logging.getLogger("django")
def custom_exception_handler(exc, context):
response = exception_handler(exc, context)
if response is None:
logger.error(f"[System Error] {exc}", exc_info=True)
return Response(
{
"error_detail": ErrorMessage.SYSTEM_ERROR.message,
"code": ErrorMessage.SYSTEM_ERROR.code
},
status=ErrorMessage.SYSTEM_ERROR.status_code,
)
custom_data = {
"error_detail": response.data.get("detail", "유효하지 않은 요청입니다."),
"code": getattr(exc, "default_code", "error")
}
if response.status_code == 400 and "detail" not in response.data:
custom_data["errors"] = response.data
response.data = custom_data
return response
handler 비교
def custom_exception_handler(
exc: Exception, context: dict[str, Any]
) -> Optional[Response]:
response = exception_handler(exc, context)
if response is None:
logger.error(f"[System Error] {exc}", exc_info=True)
return Response(
{"error_detail": "서버 내부 오류가 발생했습니다.", "code": "server_error"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
if isinstance(exc, ValidationError):
view = context.get("view")
message = getattr(
view, "validation_error_message", "유효하지 않은 데이터입니다."
)
response.data = {"error_detail": message, "errors": response.data}
if isinstance(response.data, dict):
if isinstance(exc, (NotAuthenticated, AuthenticationFailed)):
response.data = {"error_detail": "로그인이 필요한 서비스입니다."}
else:
if "detail" in response.data:
response.data = {"error_detail": str(response.data["detail"])}
if hasattr(exc, "default_code"):
response.data["code"] = exc.default_code
return response
——————————————————————————————————————[비교]—————————————————————————————————————————
def custom_exception_handler(exc, context):
response = exception_handler(exc, context)
if response is None:
logger.error(f"[System Error] {exc}", exc_info=True)
return Response(
{"error_detail": ErrorMessage.SYSTEM_ERROR.message, "code": ErrorMessage.SYSTEM_ERROR.code},
status=ErrorMessage.SYSTEM_ERROR.status_code,
)
custom_data = {
"error_detail": response.data.get("detail", "유효하지 않은 요청입니다."),
"code": getattr(exc, "default_code", "error")
}
if response.status_code == 400 and "detail" not in response.data:
custom_data["errors"] = response.data
response.data = custom_data
return response
코드 수정(에러 적용)
service
LOGIN_FAILED = (
status.HTTP_401_UNAUTHORIZED,
"login_failed",
"이메일 또는 비밀번호가 일치하지 않습니다."
)
USER_INACTIVE = (
status.HTTP_403_FORBIDDEN,
"user_inactive",
"해당 계정은 비활성화 상태입니다."
)
———————————————————————————————————————————[준비물]———————————————————————————————————————————————
class UserService:
@staticmethod
def authenticate_user(email, password):
user = authenticate(email=email, password=password)
if not user:
raise exceptions.AuthenticationFailed(
"이메일 또는 비밀번호가 일치하지 않습니다."
)
if not user.is_active:
raise exceptions.PermissionDenied("해당 계정은 비활성화 상태입니다.")
————————————————————————————————————————————[비교]————————————————————————————————————————————————
class UserService:
@staticmethod
def authenticate_user(email, password):
user = authenticate(email=email, password=password)
if not user:
raise BaseCustomException(ErrorMessage.LOGIN_FAILED)
if not user.is_active:
raise BaseCustomException(ErrorMessage.USER_INACTIVE)
serializer
class SignupSerializer(serializers.ModelSerializer):
...
def validate_email(self, value):
"""
이메일 중복 여부를 검증합니다.
Model의 unique=True가 있지만, 여기서 명시적인 에러 메시지를 주는 것이 UX에 좋습니다.
"""
if User.objects.filter(email=value).exists():
raise serializers.ValidationError("이미 존재하는 이메일입니다.")
return value
——————————————————————————————————————[비교]—————————————————————————————————————————
class SignupSerializer(serializers.ModelSerializer):
...
def validate_email(self, value):
if User.objects.filter(email=value).exists():
raise BaseCustomException(ErrorMessage.EMAIL_ALREADY_EXISTS)
return value
테스트
