From c0661f03d54692f50e7e969c1df93b27b4e6c03f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EC=88=98?= Date: Mon, 26 Aug 2024 22:40:44 +0900 Subject: [PATCH] =?UTF-8?q?Feat:=20=EC=98=88=EC=99=B8=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EB=B0=8F=20=EC=BB=A4=EC=8A=A4=ED=85=80=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/build.gradle | 10 +++ .../global/advice/CustomControllerAdvice.java | 44 +++++++++++ .../global/exception/CustomException.java | 34 +++++++++ .../worlabel/global/exception/ErrorCode.java | 31 ++++++++ .../global/response/BaseResponse.java | 61 +++++++++++++++ .../worlabel/global/response/CustomError.java | 31 ++++++++ .../global/response/ErrorResponse.java | 74 +++++++++++++++++++ .../global/response/SuccessResponse.java | 53 +++++++++++++ 8 files changed, 338 insertions(+) create mode 100644 backend/src/main/java/com/worlabel/global/advice/CustomControllerAdvice.java create mode 100644 backend/src/main/java/com/worlabel/global/exception/CustomException.java create mode 100644 backend/src/main/java/com/worlabel/global/exception/ErrorCode.java create mode 100644 backend/src/main/java/com/worlabel/global/response/BaseResponse.java create mode 100644 backend/src/main/java/com/worlabel/global/response/CustomError.java create mode 100644 backend/src/main/java/com/worlabel/global/response/ErrorResponse.java create mode 100644 backend/src/main/java/com/worlabel/global/response/SuccessResponse.java diff --git a/backend/build.gradle b/backend/build.gradle index af3bedb..05a9fae 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -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') { diff --git a/backend/src/main/java/com/worlabel/global/advice/CustomControllerAdvice.java b/backend/src/main/java/com/worlabel/global/advice/CustomControllerAdvice.java new file mode 100644 index 0000000..644320c --- /dev/null +++ b/backend/src/main/java/com/worlabel/global/advice/CustomControllerAdvice.java @@ -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 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)); + } +} diff --git a/backend/src/main/java/com/worlabel/global/exception/CustomException.java b/backend/src/main/java/com/worlabel/global/exception/CustomException.java new file mode 100644 index 0000000..0a99bde --- /dev/null +++ b/backend/src/main/java/com/worlabel/global/exception/CustomException.java @@ -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(); + } +} diff --git a/backend/src/main/java/com/worlabel/global/exception/ErrorCode.java b/backend/src/main/java/com/worlabel/global/exception/ErrorCode.java new file mode 100644 index 0000000..f704db3 --- /dev/null +++ b/backend/src/main/java/com/worlabel/global/exception/ErrorCode.java @@ -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; +} diff --git a/backend/src/main/java/com/worlabel/global/response/BaseResponse.java b/backend/src/main/java/com/worlabel/global/response/BaseResponse.java new file mode 100644 index 0000000..5c0f7b4 --- /dev/null +++ b/backend/src/main/java/com/worlabel/global/response/BaseResponse.java @@ -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 응답 데이터 형식 + */ +@Getter +@ToString +public abstract class BaseResponse { + /** + * 성공 여부 + */ + @Getter(onMethod_ = @JsonProperty("isSuccess")) + private boolean isSuccess; + + /** + * 응답 코드 + */ + private int code; + + /** + * 응답 메시지 + */ + private String message; + + /** + * 응답 데이터 + */ + protected T data; + + /** + * 에러 리스트 + */ + protected List 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); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/worlabel/global/response/CustomError.java b/backend/src/main/java/com/worlabel/global/response/CustomError.java new file mode 100644 index 0000000..57b96c7 --- /dev/null +++ b/backend/src/main/java/com/worlabel/global/response/CustomError.java @@ -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; +} \ No newline at end of file diff --git a/backend/src/main/java/com/worlabel/global/response/ErrorResponse.java b/backend/src/main/java/com/worlabel/global/response/ErrorResponse.java new file mode 100644 index 0000000..97c20be --- /dev/null +++ b/backend/src/main/java/com/worlabel/global/response/ErrorResponse.java @@ -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 { + + 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 parseErrors(Errors errors) { + if (errors == null) return Collections.emptyList(); + + // 필드 에러 리스트 생성 + List fieldErrors = errors.getFieldErrors().stream() + .map(e -> new CustomError(e.getField(), e.getCode(), e.getDefaultMessage(), e.getObjectName())).toList(); + + // 글로벌 에러 리스트 생성 + List globalErrors = errors.getGlobalErrors().stream() + .map(e -> new CustomError(null, // 필드 이름이 없으므로 null + e.getCode(), + e.getDefaultMessage(), + e.getObjectName() + )) + .toList(); + + // 두 리스트를 합쳐서 반환 + List allErrors = new ArrayList<>(); + allErrors.addAll(fieldErrors); + allErrors.addAll(globalErrors); + return allErrors; + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/worlabel/global/response/SuccessResponse.java b/backend/src/main/java/com/worlabel/global/response/SuccessResponse.java new file mode 100644 index 0000000..4f8d984 --- /dev/null +++ b/backend/src/main/java/com/worlabel/global/response/SuccessResponse.java @@ -0,0 +1,53 @@ +package com.worlabel.global.response; + +import org.springframework.http.HttpStatus; + +/** + * 성공시 응답 객체 + * + * @param 응답 데이터 타입 + */ +public class SuccessResponse extends BaseResponse { + /** + * 빈 응답 데이터 - 객체 생성시 보내는 빈 응답 데이터 + */ + private static final SuccessResponse 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 empty() { + return EMPTY; + } + + /** + * 데이터를 성공 응답 객체에 감싸서 보내는 메서드 + * + * @param data 응답 할 데이터 + * @param 응답 할 데이터 타입 + * @return 데이터를 감싼 응답 객체 + */ + public static SuccessResponse of(T data) { + return new SuccessResponse(data); + } + +} \ No newline at end of file