Skip to content

Commit

Permalink
[FEAT] 전역 예외 처리 로직 추가 및 표준화
Browse files Browse the repository at this point in the history
- CustomException 외 다양한 예외 처리 로직 추가
- 표준화된 오류 응답 구현

Resolves: TICO-229
Ref: TICO-199, TICO-200
Related to: TICO-198
  • Loading branch information
bu119 committed Jul 19, 2024
1 parent e819f4b commit 90e13b8
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 26 deletions.
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
package com.tico.pomoro_do.global.base;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Getter;

@Getter
public class ErrorResponseDTO {
private int status;
private String name;
private int code;
private String code;
private String message;
// private final String data = ""; // 응답 데이터

@Builder
public ErrorResponseDTO(int status, String name, int code, String message) {
public ErrorResponseDTO(int status, String name, String code, String message) {
this.status = status;
this.name = name;
this.code = code;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ public class SuccessResponseDTO<T> {

@Schema(description = "상태 코드", nullable = false, example = "200")
private final int status; // 응답 코드 200번대
@Schema(description = "성공 코드", nullable = false, example = "Success")
private final String code = "SUCCESS";
@Schema(description = "상태 메세지", nullable = false, example = "성공하였습니다.")
private final String message; // 메시지
@Schema(description = "해당 API의 응답 데이터", nullable = false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,36 +16,48 @@ public enum CustomErrorCode {
/* 409 CONFLICT : Resource의 현재 상태와 충돌. 보통 중복된 데이터 존재 */

//유저 관련 에러: -100번대
EMAIL_EXIST(HttpStatus.BAD_REQUEST, -100, "이미 사용 중인 이메일입니다."),
NICKNAME_NULL(HttpStatus.BAD_REQUEST, -101, "닉네임을 입력해주세요."),
USER_NOT_FOUND(HttpStatus.NOT_FOUND, -102, "가입된 사용자가 아닙니다."),
PROFILE_UPLOAD_FAILED(HttpStatus.FORBIDDEN, -103, "프로필 이미지 변경에 실패했습니다."),
USER_ALREADY_REGISTERED(HttpStatus.CONFLICT, -104, "이미 등록된 사용자입니다."),
EMAIL_EXIST(HttpStatus.BAD_REQUEST, "U-100", "이미 사용 중인 이메일입니다."),
NICKNAME_NULL(HttpStatus.BAD_REQUEST, "U-101", "닉네임을 입력해주세요."),
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "U-102", "가입된 사용자가 아닙니다."),
PROFILE_UPLOAD_FAILED(HttpStatus.FORBIDDEN, "U-103", "프로필 이미지 변경에 실패했습니다."),
USER_ALREADY_REGISTERED(HttpStatus.CONFLICT, "U-104", "이미 등록된 사용자입니다."),

//Token 관련 에러 -200번대
ACCESS_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, -201, "액세스 토큰이 만료되었습니다."),
REFRESH_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, -202, "리프레시 토큰이 만료되었습니다."),
INVALID_ACCESS_TOKEN(HttpStatus.FORBIDDEN, -203, "액세스 토큰이 유효하지 않습니다."),
INVALID_REFRESH_TOKEN(HttpStatus.FORBIDDEN, -204, "리프레시 토큰이 유효하지 않습니다."),
MISSING_ACCESS_TOKEN(HttpStatus.BAD_REQUEST, -205, "액세스 토큰이 없습니다."),
MISSING_REFRESH_TOKEN(HttpStatus.BAD_REQUEST, -206, "리프레시 토큰이 없습니다."),
MISSING_REFRESH_TOKEN_IN_DB(HttpStatus.BAD_REQUEST, -207, "DB에 리프레시 토큰이 없습니다."),
REFRESH_TOKEN_MISMATCH(HttpStatus.BAD_REQUEST, -208, "데이터베이스의 리프레시 토큰과 일치하지 않습니다."),
INVALID_AUTHORIZATION_HEADER(HttpStatus.BAD_REQUEST, -209, "AUTHORIZATION 헤더의 토큰이 유효하지 않습니다."),
UNAUTHORIZED_ACCESS(HttpStatus.UNAUTHORIZED, -210, "인증되지 않은 접근입니다."),
ACCESS_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "T-201", "액세스 토큰이 만료되었습니다."),
REFRESH_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "T-202", "리프레시 토큰이 만료되었습니다."),
INVALID_ACCESS_TOKEN(HttpStatus.FORBIDDEN, "T-203", "액세스 토큰이 유효하지 않습니다."),
INVALID_REFRESH_TOKEN(HttpStatus.FORBIDDEN, "T-204", "리프레시 토큰이 유효하지 않습니다."),
MISSING_ACCESS_TOKEN(HttpStatus.BAD_REQUEST, "T-205", "액세스 토큰이 없습니다."),
MISSING_REFRESH_TOKEN(HttpStatus.BAD_REQUEST, "T-206", "리프레시 토큰이 없습니다."),
MISSING_REFRESH_TOKEN_IN_DB(HttpStatus.BAD_REQUEST, "T-207", "DB에 리프레시 토큰이 없습니다."),
REFRESH_TOKEN_MISMATCH(HttpStatus.BAD_REQUEST, "T-208", "데이터베이스의 리프레시 토큰과 일치하지 않습니다."),
INVALID_AUTHORIZATION_HEADER(HttpStatus.BAD_REQUEST, "T-209", "AUTHORIZATION 헤더의 토큰이 유효하지 않습니다."),
UNAUTHORIZED_ACCESS(HttpStatus.UNAUTHORIZED, "T-210", "인증되지 않은 접근입니다."),

//구글 토큰 관련 에러: -300번대
GOOGLE_TOKEN_VERIFICATION_FAILED(HttpStatus.UNAUTHORIZED, -300, "구글 ID 토큰 검증에 실패했습니다."),
GOOGLE_TOKEN_INVALID(HttpStatus.UNAUTHORIZED, -301, "구글 ID 토큰이 유효하지 않습니다."),
GOOGLE_TOKEN_VERIFICATION_FAILED(HttpStatus.UNAUTHORIZED, "G-300", "구글 ID 토큰 검증에 실패했습니다."),
INVALID_GOOGLE_TOKEN_HEADER(HttpStatus.BAD_REQUEST, "G-301", "GOOGLE_ID_TOKEN 헤더의 토큰이 유효하지 않습니다."),

//관리자 관련 에러: -400번대
NOT_AN_ADMIN(HttpStatus.FORBIDDEN, -400, "관리자 권한이 없습니다."),
INVALID_ADMIN_EMAIL(HttpStatus.BAD_REQUEST, -401, "허용되지 않은 관리자 이메일입니다."),
ADMIN_EMAIL_ONLY(HttpStatus.FORBIDDEN, -402, "관리자 이메일만 접근할 수 있습니다."),
ADMIN_LOGIN_FAILED(HttpStatus.BAD_REQUEST, -403, "관리자 정보가 일치하지 않습니다. 관리자 로그인에 실패했습니다.");
NOT_AN_ADMIN(HttpStatus.FORBIDDEN, "A-400", "관리자 권한이 없습니다."),
INVALID_ADMIN_EMAIL(HttpStatus.BAD_REQUEST, "A-401", "허용되지 않은 관리자 이메일입니다."),
ADMIN_EMAIL_ONLY(HttpStatus.FORBIDDEN, "A-402", "관리자 이메일만 접근할 수 있습니다."),
ADMIN_LOGIN_FAILED(HttpStatus.BAD_REQUEST, "A-403", "관리자 정보가 일치하지 않습니다. 관리자 로그인에 실패했습니다."),

//Validation 관련 에러: -500번대
VALIDATION_FAILED(HttpStatus.BAD_REQUEST, "V-500", "유효성 검증에 실패했습니다."),
MISSING_REQUEST_BODY(HttpStatus.BAD_REQUEST, "V-501", "요청 본문이 누락되었습니다."),
MISSING_REQUEST_HEADER(HttpStatus.BAD_REQUEST, "V-502", "요청 헤더가 누락되었습니다."),

//서버 내부 관련 에러: -900번대
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "S-900", "서버 내부 오류가 발생했습니다."),
RESOURCE_NOT_FOUND(HttpStatus.NOT_FOUND, "S-901", "요청한 자원을 찾을 수 없습니다."),
INVALID_ARGUMENT(HttpStatus.BAD_REQUEST, "S-902", "유효하지 않은 인수입니다."),
ACCESS_DENIED(HttpStatus.FORBIDDEN, "S-903", "접근이 거부되었습니다.");


private final HttpStatus httpStatus; // HttpStatus
private final int code; // -100
private final String code; // U-100
private final String message; // 설명

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,17 @@
@Schema(description = "Error Response")
public class ErrorResponseEntity {
//Custom Error 내용을 담을 Response Entity를 생성한다.

@Schema(description = "HTTP 상태 코드")
private int status;
@Schema(description = "에러 이름")
private String name;
@Schema(description = "커스텀 에러 코드")
private int code;
private String code;
@Schema(description = "에러 메시지")
private String message;
// @Schema(description = "응답 데이터: 빈값")
// private final String data = ""; // 응답 데이터

public static ResponseEntity<ErrorResponseEntity> toResponseEntity(CustomErrorCode e){
return ResponseEntity
Expand All @@ -35,7 +38,7 @@ public static ResponseEntity<ErrorResponseEntity> toResponseEntity(CustomErrorCo
//{
// "status": 404,
// "name": "USER_NOT_FOUND",
// "code": -1000,
// "code": "USER-100“,
// "message": "사용자를 찾을 수 없습니다."
//}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,134 @@
package com.tico.pomoro_do.global.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingRequestHeaderException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;

import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;

@ControllerAdvice // 모든 @Controller 즉, 전역에서 발생할 수 있는 예외를 잡아 처리한다.
public class GlobalExceptionHandler {
//Controller 전역에서 발생하는 Custom Error를 잡아줄 Handler를 생성한다.

// CustomException 처리
@ExceptionHandler(CustomException.class) //발생한 CustomException 예외를 잡아서 하나의 메소드에서 공통 처리한다.
protected ResponseEntity<ErrorResponseEntity> handleCustomException(CustomException e) {
return ErrorResponseEntity.toResponseEntity(e.getErrorCode());
}

// Validation 오류 처리
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseEntity<ErrorResponseEntity> handleValidationExceptions(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
for (FieldError error : ex.getBindingResult().getFieldErrors()) {
errors.put(error.getField(), error.getDefaultMessage());
}

ErrorResponseEntity errorResponse = ErrorResponseEntity.builder()
.status(HttpStatus.BAD_REQUEST.value())
.name("Validation Error")
.code(CustomErrorCode.VALIDATION_FAILED.getCode())
.message(errors.toString())
.build();

return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
}

// 요청 본문이 잘못된 경우 처리
@ExceptionHandler(HttpMessageNotReadableException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseEntity<ErrorResponseEntity> handleHttpMessageNotReadableException(HttpMessageNotReadableException ex) {
ErrorResponseEntity errorResponse = ErrorResponseEntity.builder()
.status(HttpStatus.BAD_REQUEST.value())
.name("Bad Request")
.code(CustomErrorCode.MISSING_REQUEST_BODY.getCode())
.message("Required request body is missing.")
.build();

return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
}


// 요청 헤더가 누락된 경우 처리
@ExceptionHandler(MissingRequestHeaderException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseEntity<ErrorResponseEntity> handleMissingRequestHeaderException(MissingRequestHeaderException ex) {
ErrorResponseEntity errorResponse = ErrorResponseEntity.builder()
.status(HttpStatus.BAD_REQUEST.value())
.name("Bad Request")
.code(CustomErrorCode.MISSING_REQUEST_HEADER.getCode())
.message("Required request header is missing: " + ex.getHeaderName())
.build();

return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
}

// ElementNotFound 예외 처리
@ExceptionHandler(NoSuchElementException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ResponseEntity<ErrorResponseEntity> handleNoSuchElementException(NoSuchElementException ex) {
ErrorResponseEntity errorResponse = ErrorResponseEntity.builder()
.status(HttpStatus.NOT_FOUND.value())
.name("Not Found")
.code(CustomErrorCode.RESOURCE_NOT_FOUND.getCode())
.message("The requested resource was not found.")
.build();

return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND);
}

// IllegalArgumentException 처리
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseEntity<ErrorResponseEntity> handleIllegalArgumentException(IllegalArgumentException ex) {
ErrorResponseEntity errorResponse = ErrorResponseEntity.builder()
.status(HttpStatus.BAD_REQUEST.value())
.name("Bad Request")
.code(CustomErrorCode.INVALID_ARGUMENT.getCode())
.message(ex.getMessage())
.build();

return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
}

// 권한이 없는 접근 시 처리
@ExceptionHandler(AccessDeniedException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public ResponseEntity<ErrorResponseEntity> handleAccessDeniedException(AccessDeniedException ex) {
ErrorResponseEntity errorResponse = ErrorResponseEntity.builder()
.status(HttpStatus.FORBIDDEN.value())
.name("Forbidden")
.code(CustomErrorCode.ACCESS_DENIED.getCode())
.message("Access to the resource is denied.")
.build();

return new ResponseEntity<>(errorResponse, HttpStatus.FORBIDDEN);
}

// 기타 모든 예외 처리
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ResponseEntity<ErrorResponseEntity> handleException(Exception ex) {
ErrorResponseEntity errorResponse = ErrorResponseEntity.builder()
.status(HttpStatus.INTERNAL_SERVER_ERROR.value())
.name("Internal Server Error")
.code(CustomErrorCode.INTERNAL_SERVER_ERROR.getCode())
.message("An unexpected error occurred: " + ex.getMessage())
.build();

return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
}

//@ControllerAdvice + @ExceptionHandler
//모든 컨트롤러에서 발생하는 CustomException을 catch한다.
}

0 comments on commit 90e13b8

Please sign in to comment.