Feat: 예외처리 및 커스텀 에러 처리

This commit is contained in:
김용수 2024-08-26 22:40:44 +09:00
parent 2e6776fd10
commit c0661f03d5
8 changed files with 338 additions and 0 deletions

View File

@ -24,15 +24,25 @@ repositories {
}
dependencies {
// Spring Boot
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
// Lombok
compileOnly 'org.projectlombok:lombok'
// MySQL
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
// Test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
// GJson
implementation 'com.google.code.gson:gson:2.7'
}
tasks.named('test') {

View File

@ -0,0 +1,44 @@
package com.worlabel.global.advice;
import com.worlabel.global.exception.CustomException;
import com.worlabel.global.exception.ErrorCode;
import com.worlabel.global.response.ErrorResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@Slf4j
@RestControllerAdvice
public class CustomControllerAdvice {
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse handleException(Exception e) {
log.error("", e);
return ErrorResponse.of(new CustomException(ErrorCode.SERVER_ERROR));
}
@ExceptionHandler({HttpMessageNotReadableException.class})
public ErrorResponse handleReadableException(Exception exception) {
log.error("",exception);
return ErrorResponse.of(new CustomException(ErrorCode.BAD_REQUEST));
}
@ExceptionHandler(CustomException.class)
public ResponseEntity<ErrorResponse> handleCustomException(CustomException e) {
log.error("", e);
return ResponseEntity.status(e.getErrorCode().getStatus())
.body(ErrorResponse.of(e));
}
@ExceptionHandler({MissingServletRequestParameterException.class})
public ErrorResponse handleRequestParameterException(Exception e) {
log.error("",e);
return ErrorResponse.of(new CustomException(ErrorCode.EMPTY_REQUEST_PARAMETER));
}
}

View File

@ -0,0 +1,34 @@
package com.worlabel.global.exception;
import lombok.Getter;
import org.springframework.validation.Errors;
import java.util.Objects;
@Getter
public class CustomException extends RuntimeException{
private final ErrorCode errorCode;
private Errors errors;
public CustomException(ErrorCode errorCode) {
this(errorCode, errorCode.getMessage(), null);
}
public CustomException(ErrorCode errorCode, String message) {
this(errorCode, message, null);
}
public CustomException(ErrorCode errorCode, Errors errors) {
this(errorCode, errorCode.getMessage(), errors);
}
public CustomException(ErrorCode errorCode, String message, Errors errors) {
super(message);
this.errorCode = errorCode;
this.errors = errors;
}
public boolean hasErrors(){
return Objects.nonNull(errors) && errors.hasErrors();
}
}

View File

@ -0,0 +1,31 @@
package com.worlabel.global.exception;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
@Getter
@RequiredArgsConstructor
public enum ErrorCode {
// Common - 1000
SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 9999, "서버 에러입니다. 관리자에게 문의해주세요."),
INVALID_INPUT_VALUE(HttpStatus.BAD_REQUEST, 1000, "올바르지 않은 입력 값입니다. 다시 한번 확인해주세요."),
EMPTY_FILE(HttpStatus.BAD_REQUEST, 1001, "빈 파일입니다."),
BAD_REQUEST(HttpStatus.BAD_REQUEST, 1002, "잘못된 요청입니다. 요청을 확인해주세요."),
EMPTY_REQUEST_PARAMETER(HttpStatus.BAD_REQUEST, 1003, "필수 요청 파라미터가 입력되지 않았습니다."),
// Auth & User - 2000
USER_NOT_FOUND(HttpStatus.NOT_FOUND, 2000, "해당 ID의 사용자를 찾을 수 없습니다."),
ACCESS_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, 2001, "만료된 액세스 토큰입니다."),
REFRESH_TOKEN_MISSING(HttpStatus.BAD_REQUEST, 2002, "리프레시 토큰이 누락되었습니다."),
REFRESH_TOKEN_EXPIRED(HttpStatus.BAD_REQUEST, 2003, "리프레시 토큰이 만료되었습니다."),
INVALID_REFRESH_TOKEN(HttpStatus.BAD_REQUEST, 2004, "유효하지 않은 리프레시 토큰입니다."),
UNAUTHORIZED(HttpStatus.UNAUTHORIZED, 2005, "인증에 실패하였습니다."),
;
private final HttpStatus status;
private final int code;
private final String message;
}

View File

@ -0,0 +1,61 @@
package com.worlabel.global.response;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.gson.Gson;
import lombok.Getter;
import lombok.ToString;
import java.util.List;
import java.util.ArrayList;
/**
* 응답 형식
*
* @param <T> 응답 데이터 형식
*/
@Getter
@ToString
public abstract class BaseResponse<T> {
/**
* 성공 여부
*/
@Getter(onMethod_ = @JsonProperty("isSuccess"))
private boolean isSuccess;
/**
* 응답 코드
*/
private int code;
/**
* 응답 메시지
*/
private String message;
/**
* 응답 데이터
*/
protected T data;
/**
* 에러 리스트
*/
protected List<CustomError> errors;
public BaseResponse(boolean isSuccess, int code, String message) {
this.isSuccess = isSuccess;
this.code = code;
this.message = message;
this.data = null;
this.errors = new ArrayList<>();
}
/**
* Json으로 변환 -> 추후 테스트 코드를 위해 존재
*
* @return 문자열로 변환된 객체
*/
public String toJson() {
return new Gson().toJson(this);
}
}

View File

@ -0,0 +1,31 @@
package com.worlabel.global.response;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 사용자 설정 에러
*/
@Getter
@AllArgsConstructor
public class CustomError {
/**
* 에러 발생 필드
*/
private String field;
/**
* 에러 코드
*/
private String code;
/**
* 에러 메시지
*/
private String message;
/**
* 에러 발생 객체
*/
private String objectName;
}

View File

@ -0,0 +1,74 @@
package com.worlabel.global.response;
import com.worlabel.global.exception.CustomException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.validation.Errors;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
/**
* 에러 발생시 리턴 에러 응답 객체
*/
@Slf4j
public class ErrorResponse extends BaseResponse<Void> {
public ErrorResponse(boolean isSuccess, int code, String message, Errors errors) {
super(isSuccess, code, message);
super.errors = parseErrors(errors);
}
public ErrorResponse(CustomException exception) {
this(false, exception.getErrorCode().getCode(), exception.getMessage(), exception.getErrors());
}
public ErrorResponse(CustomException exception, String message) {
this(false, 1, message, exception.getErrors());
}
public static ErrorResponse of(CustomException exception) {
return new ErrorResponse(exception);
}
public static ErrorResponse of(CustomException exception, String message) {
return new ErrorResponse(exception, message);
}
public static ErrorResponse of(Exception exception) {
return new ErrorResponse(false, HttpStatus.INTERNAL_SERVER_ERROR.value(), exception.getMessage(), null);
}
/**
* 전달 받은 Errors 커스텀에러로 파싱해주는 메서드
*
* @param errors Error 담긴 객체
* @return CustomError 리스트
*/
private List<CustomError> parseErrors(Errors errors) {
if (errors == null) return Collections.emptyList();
// 필드 에러 리스트 생성
List<CustomError> fieldErrors = errors.getFieldErrors().stream()
.map(e -> new CustomError(e.getField(), e.getCode(), e.getDefaultMessage(), e.getObjectName())).toList();
// 글로벌 에러 리스트 생성
List<CustomError> globalErrors = errors.getGlobalErrors().stream()
.map(e -> new CustomError(null, // 필드 이름이 없으므로 null
e.getCode(),
e.getDefaultMessage(),
e.getObjectName()
))
.toList();
// 리스트를 합쳐서 반환
List<CustomError> allErrors = new ArrayList<>();
allErrors.addAll(fieldErrors);
allErrors.addAll(globalErrors);
return allErrors;
}
}

View File

@ -0,0 +1,53 @@
package com.worlabel.global.response;
import org.springframework.http.HttpStatus;
/**
* 성공시 응답 객체
*
* @param <T> 응답 데이터 타입
*/
public class SuccessResponse<T> extends BaseResponse<T> {
/**
* 응답 데이터 - 객체 생성시 보내는 응답 데이터
*/
private static final SuccessResponse<Void> EMPTY = new SuccessResponse<>();
/**
* 성공 응답 객체 생성자
*/
public SuccessResponse() {
super(true, HttpStatus.OK.value(), "success");
}
/**
* 성공 응답 객체
*
* @param data 성공시 반환하는 데이터
*/
public SuccessResponse(T data) {
super(true, 200, "success");
super.data = data;
}
/**
* 응답 리턴
*
* @return 응답 객체
*/
public static SuccessResponse<Void> empty() {
return EMPTY;
}
/**
* 데이터를 성공 응답 객체에 감싸서 보내는 메서드
*
* @param data 응답 데이터
* @param <T> 응답 데이터 타입
* @return 데이터를 감싼 응답 객체
*/
public static <T> SuccessResponse<T> of(T data) {
return new SuccessResponse<T>(data);
}
}