1. 목표
시스템 전반에서 발생할 수 있는 다양한 예외를 포괄적으로 관리하고, 일관된 방식으로 예외를 처리하기 위해 예외 처리 로직이 필요합니다.
에러 핸들링을 위한 예외 처리 로직 구현 과정을 정리해 보았습니다.
*아직 리팩토링이 진행중인 상태로, 생성되지 않은 예외들이 있어서 추후 에러코드들이 거의 완성된다면 추가 해두겠습니다.
2. 예외 코드 정의
먼저, 클라이언트와 서버에서 발생할 수 있는 다양한 예외 상황에 대해 정의합니다.
에러의 경우 서버와 클라이언트에서 생성되는 2가지의 오류가 있기 때문에 ClientErrorCode
와 ServerErrorCode
를 enum 클래스로 나누어 정의해 주었습니다.
이 때, ErrorCodeModel
이라는 인터페이스를 생성해주었습니다.
ErrorCodeModel 인터페이스의 장점
- 추후 새로 생성될 xxxErrorCode들의 구조를 지정해 줄 수 있습니다.
- 또한 여러 xxxErrorCode 클래스들이 생성되어도 예외 처리시
ErrorCodeModel
타입을 이용하여 하나의 처리 메서드에서 다룰 수 있어 유연한 예외 처리가 가능합니다.
ClientErrorCode Enum
public enum ClientErrorCode implements ErrorCodeModel {
UNAUTHORIZED(401, "UNAUTHORIZED", "인증이 필요합니다. 로그인 해주세요."),
AUTHENTICATION_FAILED(401, "AUTHENTICATION_FAILED", "인증을 실패했습니다."),
AUTH_INVALID_ACCESS_TOKEN(401, "AUTH_INVALID_ACCESS_TOKEN", "잘못된 액세스 토큰입니다."),
AUTH_EXPIRED_ACCESS_TOKEN(401, "AUTH_EXPIRED_ACCESS_TOKEN", "액세스 토큰이 만료되었습니다."),
AUTH_INVALID_SIGNATURE(401, "AUTH_INVALID_SIGNATURE", "잘못된 시그니처입니다."),
AUTH_JWT_ERROR(401, "AUTH_JWT_ERROR", "JWT 관련 오류가 발생했습니다."),
AUTH_MISSING_ID_IN_ACCESS_TOKEN(401, "AUTH_MISSING_ID_IN_ACCESS_TOKEN", "JWT 액세스 토큰에 ID가 누락되었습니다."),
AUTH_TOKEN_EXTRACTION_FAILED(401, "AUTH_TOKEN_EXTRACTION_FAILED", "액세스 토큰 추출에 실패했습니다."),
AUTH_SESSION_EXPIRED(440, "AUTH_SESSION_EXPIRED", "세션이 만료되었습니다. 다시 로그인 해주세요."),
MEMBER_NOT_FOUND_ERR(404, "MEMBER_NOT_FOUND_ERR", "요청하신 사용자를 찾을 수 없습니다."),
DIARY_ALREADY_EXISTS(409, "DIARY_ALREADY_EXISTS", "이미 해당 날짜에 작성된 일기가 있습니다."),
DIARY_NOT_FOUND_ERR(404, "DIARY_NOT_FOUND_ERR", "요청하신 다이어리를 찾을 수 없습니다."),
DIARY_LIKE_NOT_FOUND(404, "DIARY_LIKE_NOT_FOUND", "해당 다이어리에 좋아요를 누르지 않았습니다."),
DIARY_LIKE_ALREADY_EXISTS(409, "DIARY_LIKE_ALREADY_EXISTS", "다이어리에 좋아요를 이미 눌렀습니다."),
INVALID_IMAGE_FORMAT(400, "INVALID_IMAGE_FORMAT", "허용되지 않는 파일 형식입니다."),
IMAGE_FILE_EMPTY(400, "IMAGE_FILE_EMPTY", "이미지 파일이 비어 있습니다."),
// 아래는 추후에 사용할 수도 있을 것 같은 예외들을 정의 해두었다.
VALIDATION_ERR(400, "VALIDATION_ERR", "잘못된 입력입니다. 올바른 값을 입력해주세요."),
PERMISSION_ERR(403, "PERMISSION_ERR", "접근 권한이 없습니다. 관리자에게 문의하세요."),
TIMEOUT_ERR(408, "TIMEOUT_ERR", "요청 시간이 초과되었습니다. 다시 시도해주세요."),
ILLEGAL_ACCESS(400, "ILLEGAL_ACCESS", "잘못된 요청입니다. 올바른 경로로 접근해주세요."),
BAD_REQUEST_ERR(400, "BAD_REQUEST_ERR", "요청 형식이 잘못되었습니다. 올바른 형식으로 요청해주세요."),
UNSUPPORTED_MEDIA_ERR(415, "UNSUPPORTED_MEDIA_ERR", "지원되지 않는 미디어 타입입니다. 올바른 형식으로 요청해주세요."),
MISSING_PARAM_ERR(400, "MISSING_PARAM_ERR", "필수 파라미터가 누락되었습니다. 모든 필드를 입력해주세요."),
CONFLICT_ERR(409, "CONFLICT_ERR", "데이터 충돌이 발생했습니다. 요청을 다시 확인해주세요."),
PASSWORD_POLICY_ERR(400, "PASSWORD_POLICY_ERR", "비밀번호가 정책을 위반합니다. 다른 비밀번호를 사용해주세요."),
RATE_LIMIT_EXCEEDED(429, "RATE_LIMIT_EXCEEDED", "API 요청 한도를 초과했습니다. 잠시 후 다시 시도해주세요."),
ACCOUNT_LOCKED(423, "ACCOUNT_LOCKED", "사용자 계정이 잠겼습니다. 관리자에게 문의하세요.");
}
ServerErrorCode Enum
public enum ServerErrorCode implements ErrorCodeModel {
SERVER_ERR(500, "SERVER_ERR", "서버 내부 오류가 발생했습니다. 잠시 후 다시 시도해주세요."),
IMAGE_UPLOAD_FAILED(500, "IMAGE_UPLOAD_FAILED", "이미지 업로드에 실패했습니다."),
REWARD_HISTORY_SAVE_FAILED(500, "REWARD_HISTORY_SAVE_FAILED", "일기 히스토리 저장에 실패했습니다."),
// 아래는 추후에 사용할 수도 있을 것 같은 예외들을 정의 해두었다.
DB_CONNECTION_ERR(503, "DB_CONNECTION_ERR", "서비스를 이용할 수 없습니다. 나중에 다시 시도해주세요."),
EXTERNAL_API_ERR(502, "EXTERNAL_API_ERR", "외부 서비스와의 통신에 문제가 발생했습니다. 잠시 후 다시 시도해주세요."),
OUT_OF_MEMORY(500, "OUT_OF_MEMORY", "서버에 메모리 부족이 발생했습니다. 잠시 후 다시 시도해주세요."),
NOT_IMPLEMENTED(501, "NOT_IMPLEMENTED", "현재 지원되지 않는 기능입니다."),
SERVICE_UNAVAILABLE(503, "SERVICE_UNAVAILABLE", "현재 서비스가 사용 불가합니다. 나중에 다시 시도해주세요.");
}
3. 예외 클래스 정의
각 예외 상황을 처리해줄 수 있는 클래스를 작성합니다.
RuntimeException
을 상속하여 해당 예외를 잡을 수 있게 해주었습니다.
xxxErrorCode 클래스를 ErrorCodeModel
를 이용해 작성해주었기 때문에 하나의 클래스로 처리가 가능하게 되었습니다.
시스템의 전반적인 예외를 담당하기 위해 SystemException
이라는 클래스 명으로 지어주었습니다.
SystemException 클래스
@Getter
public class SystemException extends RuntimeException {
private final ErrorCodeModel errorCode;
public SystemException(ErrorCodeModel errorCode) {
super(errorCode.getErrorMessage());
this.errorCode = errorCode;
}
public SystemException(ErrorCodeModel errorCode, Throwable cause) {
super(errorCode.getErrorMessage(), cause);
this.errorCode = errorCode;
}
}
4. GlobalExceptionHandler 정의
글로벌 예외 핸들러를 작성하여, 시스템 전반에서 발생하는 예외를 처리할 수 있도록 합니다.
@RestControllerAdvice
애너테이션을 사용하여 모든 컨트롤러에서 발생하는 예외를 하나의 중앙집중식 클래스에서 처리할 수 있도록 해줍니다.
GlobalExceptionHandler 클래스
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(SystemException.class)
public ResponseEntity<Response> handleCustomException(SystemException exception, WebRequest request) {
ErrorCodeModel errorCode = exception.getErrorCode();
Response response = Response.error(errorCode);
return new ResponseEntity<>(response, HttpStatus.valueOf(errorCode.getStatusCode()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<Response> handleGlobalException(Exception exception, WebRequest request) {
Response response = new Response(
HttpStatus.INTERNAL_SERVER_ERROR.value(), // status: 500
"INTERNAL_SERVER_ERROR",
"예기치 않은 오류가 발생했습니다."
);
return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
5. 예외 발생 시 로직 수정
필요했던 각 예외 상황에 맞는 예외를 던지도록 기존 로직을 수정하였습니다.
예외 발생을 위한 구현체 전달 방법(예시)
// 클라이언트에서 요청한 사용자를 찾을 수 없을 때
throw new SystemException(ClientErrorCode.MEMBER_NOT_FOUND_ERR)
// 클라이언트에서 인증되지 않은 사용자가 요청을 할 때
new SystemException(ClientErrorCode.UNAUTHORIZED)
// 서버가 이미지 저장을 실패할 때
throw new SystemException(ServerErrorCode.IMAGE_UPLOAD_FAILED)
6. 예외 리스트 정리
클라이언트와 서버에서 발생할 수 있는 예외를 표로 정리하여, 팀원들과 함께 볼 수 있도록 공유하였습니다.
참고
- 예외 발생시 클라이언트로 전송되는 값의 예시
- 연관된 PR입니다: [refactor/exception] 예외 처리 클래스 생성 #92
'프로젝트 > 프로젝트 과정' 카테고리의 다른 글
[잔디일기] 랜덤 값 만들기 (0) | 2024.07.28 |
---|---|
깃허브 Organigation 레포지토리 복구하기 (2) | 2024.07.23 |
[잔디일기] 코드 리팩토링을 해보자 (0) | 2024.05.22 |
[잔디일기] 패키지 구조에 대한 고민 - 적용결과 (0) | 2024.05.21 |
[잔디일기] 패키지 구조에 대한 고민 (0) | 2024.05.10 |