diff --git a/build.gradle b/build.gradle index 2958cf19..73a95298 100644 --- a/build.gradle +++ b/build.gradle @@ -26,6 +26,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'org.springframework.boot:spring-boot-starter-validation' compileOnly 'org.projectlombok:lombok' runtimeOnly 'org.postgresql:postgresql' annotationProcessor 'org.projectlombok:lombok' diff --git a/src/main/java/com/server/capple/global/common/BaseResponse.java b/src/main/java/com/server/capple/global/common/BaseResponse.java new file mode 100644 index 00000000..d0f4c427 --- /dev/null +++ b/src/main/java/com/server/capple/global/common/BaseResponse.java @@ -0,0 +1,29 @@ +package com.server.capple.global.common; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +@JsonPropertyOrder({"timeStamp", "code", "message", "result"}) +public class BaseResponse { + + private final LocalDateTime timeStamp = LocalDateTime.now(); + private final String code; + private final String message; + @JsonInclude(Include.NON_NULL) + private T result; + + // 성공 시 응답 + public static BaseResponse onSuccess(T result) { + return new BaseResponse<>("COMMON200", "요청에 성공하였습니다.", result); + } + + public static BaseResponse onFailure(String code, String message, T data) { + return new BaseResponse<>(code, message, data); + } +} diff --git a/src/main/java/com/server/capple/global/exception/ControllerAdvice.java b/src/main/java/com/server/capple/global/exception/ControllerAdvice.java new file mode 100644 index 00000000..936f77dc --- /dev/null +++ b/src/main/java/com/server/capple/global/exception/ControllerAdvice.java @@ -0,0 +1,116 @@ +package com.server.capple.global.exception; + +import com.server.capple.global.common.BaseResponse; +import com.server.capple.global.exception.errorCode.GlobalErrorCode; +import jakarta.validation.ConstraintViolationException; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.servlet.NoHandlerFoundException; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +@Slf4j +@RestControllerAdvice(annotations = {RestController.class}) +public class ControllerAdvice extends ResponseEntityExceptionHandler { + + /** + * 정의한 RestApiException 예외 처리 + */ + @ExceptionHandler(value = RestApiException.class) + public ResponseEntity> handleRestApiException( + RestApiException e) { + ErrorCode errorCode = e.getErrorCode(); + return handlerExceptionInternal(errorCode); + } + + /** + * 일반적인 서버 에러 예외 처리 + */ + @ExceptionHandler + public ResponseEntity> handleException( + Exception e) { + e.printStackTrace(); + return handleExceptionInternalFalse(GlobalErrorCode.SERVER_ERROR.getErrorCode(), e.getMessage()); + } + + /** + * @Validated 검증 실패 예외 처리 + */ + @ExceptionHandler(ConstraintViolationException.class) + public ResponseEntity> handleConstraintViolationException( + ConstraintViolationException e) { + return handleExceptionInternal(GlobalErrorCode.VALIDATION_ERROR.getErrorCode()); + } + + /** + * 메서드의 인자 타입이 예상과 다른 경우 예외 처리 + */ + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public ResponseEntity> handleMethodArgumentTypeMismatch( + MethodArgumentTypeMismatchException e) { + // 예외 처리 로직 + return handleExceptionInternal(GlobalErrorCode.NOT_VALID_ARGUMENT_ERROR.getErrorCode()); + } + + /** + * @RequestBody 내부 처리 실패, + * @Valid 검증 실패한 경우 예외 처리 + */ + @Override + public ResponseEntity handleMethodArgumentNotValid( + MethodArgumentNotValidException e, + HttpHeaders headers, + HttpStatusCode statusCode, + WebRequest request) { + Map errors = new LinkedHashMap<>(); + + e.getBindingResult().getFieldErrors().stream() + .forEach(fieldError -> { + String fieldName = fieldError.getField(); + String errorMessage = Optional.ofNullable(fieldError.getDefaultMessage()).orElse(""); + errors.merge(fieldName, errorMessage, (existingErrorMessage, newErrorMessage) -> existingErrorMessage + ", " + newErrorMessage); + }); + + return handleExceptionInternalArgs(GlobalErrorCode.VALIDATION_ERROR.getErrorCode(), errors); + + } + + private ResponseEntity> handlerExceptionInternal( + ErrorCode errorCode) { + return ResponseEntity + .status(errorCode.getHttpStatus().value()) + .body(BaseResponse.onFailure(errorCode.getCode(), errorCode.getMessage(), null)); + } + + private ResponseEntity> handleExceptionInternal(ErrorCode errorCode) { + return ResponseEntity + .status(errorCode.getHttpStatus().value()) + .body(BaseResponse.onFailure(errorCode.getCode(), errorCode.getMessage(), null)); + } + + private ResponseEntity handleExceptionInternalArgs( + ErrorCode errorCode, + Map errorArgs) { + return ResponseEntity + .status(errorCode.getHttpStatus().value()) + .body(BaseResponse.onFailure(errorCode.getCode(), errorCode.getMessage(), errorArgs)); + } + private ResponseEntity> handleExceptionInternalFalse( + ErrorCode errorCode, + String errorPoint) { + return ResponseEntity + .status(errorCode.getHttpStatus().value()) + .body(BaseResponse.onFailure(errorCode.getCode(), errorCode.getMessage(), errorPoint)); + } +} diff --git a/src/main/java/com/server/capple/global/exception/ErrorCode.java b/src/main/java/com/server/capple/global/exception/ErrorCode.java new file mode 100644 index 00000000..822c47a3 --- /dev/null +++ b/src/main/java/com/server/capple/global/exception/ErrorCode.java @@ -0,0 +1,13 @@ +package com.server.capple.global.exception; + +import lombok.Builder; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@Builder +public class ErrorCode { + private final String code; + private final String message; + private final HttpStatus httpStatus; +} diff --git a/src/main/java/com/server/capple/global/exception/ErrorCodeInterface.java b/src/main/java/com/server/capple/global/exception/ErrorCodeInterface.java new file mode 100644 index 00000000..b3b47e87 --- /dev/null +++ b/src/main/java/com/server/capple/global/exception/ErrorCodeInterface.java @@ -0,0 +1,6 @@ +package com.server.capple.global.exception; + + +public interface ErrorCodeInterface { + ErrorCode getErrorCode(); +} diff --git a/src/main/java/com/server/capple/global/exception/RestApiException.java b/src/main/java/com/server/capple/global/exception/RestApiException.java new file mode 100644 index 00000000..31c4e35f --- /dev/null +++ b/src/main/java/com/server/capple/global/exception/RestApiException.java @@ -0,0 +1,12 @@ +package com.server.capple.global.exception; + +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public class RestApiException extends RuntimeException { + private final ErrorCodeInterface errorCode; + + public ErrorCode getErrorCode() { + return this.errorCode.getErrorCode(); + } +} diff --git a/src/main/java/com/server/capple/global/exception/errorCode/GlobalErrorCode.java b/src/main/java/com/server/capple/global/exception/errorCode/GlobalErrorCode.java new file mode 100644 index 00000000..3686e7a8 --- /dev/null +++ b/src/main/java/com/server/capple/global/exception/errorCode/GlobalErrorCode.java @@ -0,0 +1,34 @@ +package com.server.capple.global.exception.errorCode; + +import com.server.capple.global.exception.ErrorCode; +import com.server.capple.global.exception.ErrorCodeInterface; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum GlobalErrorCode implements ErrorCodeInterface { + BAD_REQUEST("GLOBAL001", "잘못된 요청입니다.", HttpStatus.BAD_REQUEST), + NOT_SUPPORTED_URI_ERROR("GLOBAL002", "올바르지 않은 URI입니다.", HttpStatus.NOT_FOUND), + NOT_SUPPORTED_METHOD_ERROR("GLOBAL003", "지원하지 않는 Method입니다.", HttpStatus.METHOD_NOT_ALLOWED), + NOT_SUPPORTED_MEDIA_TYPE_ERROR("GLOBAL004", "지원하지 않는 Media type입니다.", HttpStatus.UNSUPPORTED_MEDIA_TYPE), + SERVER_ERROR("GLOBAL005", "서버 에러, 관리자에게 문의해주세요.", HttpStatus.INTERNAL_SERVER_ERROR), + ACCESS_DENIED("GLOBAL006", "올바르지 않은 권한입니다.", HttpStatus.FORBIDDEN), + NOT_VALID_ARGUMENT_ERROR("GLOBAL007", "올바르지 않은 Argument Type입니다.", HttpStatus.BAD_REQUEST), + VALIDATION_ERROR("GLOBAL007", "Validation Error입니다.", HttpStatus.BAD_REQUEST), + ; + + private final String code; + private final String message; + private final HttpStatus httpStatus; + + @Override + public ErrorCode getErrorCode() { + return ErrorCode.builder() + .code(code) + .message(message) + .httpStatus(httpStatus) + .build(); + } +}