Feat: 예외처리 및 커스텀 에러 처리
This commit is contained in:
parent
2e6776fd10
commit
c0661f03d5
@ -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') {
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user