diff --git a/build.gradle.kts b/build.gradle.kts index 28287c7..554b34c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -26,6 +26,9 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-actuator") implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.boot:spring-boot-starter-validation") + + developmentOnly("org.springframework.boot:spring-boot-devtools") //flyway implementation("org.flywaydb:flyway-core") @@ -42,7 +45,10 @@ dependencies { compileOnly("org.projectlombok:lombok") annotationProcessor("org.projectlombok:lombok") - developmentOnly("org.springframework.boot:spring-boot-devtools") + //problem + implementation("org.zalando:problem-spring-web-starter:0.27.0") + + //test testImplementation("org.springframework.boot:spring-boot-starter-test") } diff --git a/src/main/java/org/idiot/yesslave/global/exception/InvalidResponse.java b/src/main/java/org/idiot/yesslave/global/exception/InvalidResponse.java new file mode 100644 index 0000000..53ee574 --- /dev/null +++ b/src/main/java/org/idiot/yesslave/global/exception/InvalidResponse.java @@ -0,0 +1,19 @@ +package org.idiot.yesslave.global.exception; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +@Schema(description = "입력된 값에 대한 유효성 검사에 실패했을 경우의 응답값") +public class InvalidResponse { + @Schema(description = "오류가 발생한 field명", example = "verificationCode") + private String field; + + @Schema(description = "오류 메세지", example = "확인번호를 입력해주세요.") + private String message; + + @Schema(description = "잘못 입력된 field 입력 값", example = "ABCDE") + private Object rejectValue; +} diff --git a/src/main/java/org/idiot/yesslave/global/exception/RootExceptionHandler.java b/src/main/java/org/idiot/yesslave/global/exception/RootExceptionHandler.java new file mode 100644 index 0000000..457afb0 --- /dev/null +++ b/src/main/java/org/idiot/yesslave/global/exception/RootExceptionHandler.java @@ -0,0 +1,93 @@ +package org.idiot.yesslave.global.exception; + +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.zalando.problem.Problem; +import org.zalando.problem.Status; + +import javax.validation.ConstraintViolationException; +import java.util.List; + +@Slf4j +@ControllerAdvice +@EnableAutoConfiguration(exclude = ErrorMvcAutoConfiguration.class) +public class RootExceptionHandler { + @ExceptionHandler(Exception.class) + @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) + @ApiResponse(responseCode = "500", description = "Internal Server Error", content = { + @Content(schema = @Schema(implementation = Problem.class)) + }) + public ResponseEntity exceptionHandler(Exception e) { + + log.error("[ 500 ERROR ] : ", e); + + Problem problem = Problem.builder() + .withStatus(Status.INTERNAL_SERVER_ERROR) + .withTitle(Status.INTERNAL_SERVER_ERROR.getReasonPhrase()) + .withDetail(e.getMessage()) + .build(); + + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(problem); + } + + @ResponseStatus(value = HttpStatus.BAD_REQUEST) + @ApiResponse(responseCode = "400", description = "Bad Request", content = { + @Content(schema = @Schema(implementation = InvalidResponse.class)) + }) + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) { + log.error("[ 400 ERROR ] : ", e); + + BindingResult bindingResult = e.getBindingResult(); + + List responses = bindingResult.getFieldErrors().stream() + .map(fieldError -> new InvalidResponse(fieldError.getField() + , fieldError.getDefaultMessage() + , fieldError.getRejectedValue()) + ).toList(); + + Problem problem = Problem.builder() + .withStatus(Status.BAD_REQUEST) + .withTitle(Status.BAD_REQUEST.getReasonPhrase()) + .with("parameters", responses) + .build(); + + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(problem); + } + + @ResponseStatus(value = HttpStatus.BAD_REQUEST) + @ApiResponse(responseCode = "400", description = "Bad Request", content = { + @Content(schema = @Schema(implementation = InvalidResponse.class)) + }) + @ExceptionHandler(ConstraintViolationException.class) + public Problem constraintViolationExceptionHandler(ConstraintViolationException e) { + log.error("[ 400 ERROR ] : ", e); + + List responses = e.getConstraintViolations().stream() + .map(fieldError -> new InvalidResponse(fieldError.getPropertyPath().toString() + , fieldError.getMessage() + , fieldError.getInvalidValue()) + ).toList(); + + return Problem.builder() + .withStatus(Status.BAD_REQUEST) + .withTitle(Status.BAD_REQUEST.getReasonPhrase()) + .with("parameters", responses) + .build(); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 8105739..49b0d07 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -15,3 +15,9 @@ spring: format_sql: true show_sql: true dialect: org.hibernate.dialect.MySQL8Dialect + web: + resources: + add-mappings: false + mvc: + throw-exception-if-no-handler-found: true + \ No newline at end of file