From 7337a5bb64f7c43dd99ad78b17c01a9a95fa60e9 Mon Sep 17 00:00:00 2001 From: sunwoong Date: Mon, 2 Dec 2024 22:21:54 +0900 Subject: [PATCH 1/5] #3 feat: design doorip exception --- .../org/doorip/domain/DooripException.kt | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 domain/src/main/kotlin/org/doorip/domain/DooripException.kt diff --git a/domain/src/main/kotlin/org/doorip/domain/DooripException.kt b/domain/src/main/kotlin/org/doorip/domain/DooripException.kt new file mode 100644 index 0000000..38f8d64 --- /dev/null +++ b/domain/src/main/kotlin/org/doorip/domain/DooripException.kt @@ -0,0 +1,47 @@ +package org.doorip.domain + +sealed class DooripException( + val code: String, + message: String, + cause: Throwable? = null, +) : RuntimeException(message, cause) + +// Client Exception +sealed class ClientException( + code: String, + message: String, +) : DooripException(code, message) + +class UnauthorizedException( + code: String, + message: String, +) : ClientException(code, message) + +class UnauthenticatedException( + code: String, + message: String, +) : ClientException(code, message) + +data object InvalidRequestValueException : ClientException("e4000", "잘못된 요청입니다.") { private fun readResolve(): Any = InvalidRequestValueException } +data object MethodNotAllowedException : ClientException("e4050", "잘못된 HTTP method 요청입니다.") { private fun readResolve(): Any = MethodNotAllowedException } +data object ConflictException : ClientException("e4090", "이미 존재하는 리소스입니다.") { private fun readResolve(): Any = ConflictException } + +// Server Exception +sealed class ServerException( + code: String, + message: String, +) : DooripException(code, message) + +data object NotFoundException : ServerException("e4040", "대상을 찾을 수 없습니다.") { private fun readResolve(): Any = NotFoundException } +data object InternalServerException : ServerException("e5000", "서버 내부 오류입니다.") { private fun readResolve(): Any = InternalServerException } + +// Critical Exception +sealed class CriticalException( + code: String, + message: String, + cause: Throwable? = null, +) : DooripException(code, message, cause) + +class UnknownException( + cause: Throwable? = null, +) : CriticalException("e6000", "정의되지 않은 예외입니다. (로그 확인이 필요합니다.)") From 1f82bcdcd28d6b09b084e49366e3a3d149819604 Mon Sep 17 00:00:00 2001 From: sunwoong Date: Mon, 2 Dec 2024 22:22:10 +0900 Subject: [PATCH 2/5] #3 feat: design api response --- .../kotlin/org/doorip/api/dto/ApiResponse.kt | 38 +++++++++++++++++++ .../org/doorip/api/dto/ExceptionResponse.kt | 7 ++++ 2 files changed, 45 insertions(+) create mode 100644 presentation/api/src/main/kotlin/org/doorip/api/dto/ApiResponse.kt create mode 100644 presentation/api/src/main/kotlin/org/doorip/api/dto/ExceptionResponse.kt diff --git a/presentation/api/src/main/kotlin/org/doorip/api/dto/ApiResponse.kt b/presentation/api/src/main/kotlin/org/doorip/api/dto/ApiResponse.kt new file mode 100644 index 0000000..3a5ac31 --- /dev/null +++ b/presentation/api/src/main/kotlin/org/doorip/api/dto/ApiResponse.kt @@ -0,0 +1,38 @@ +package org.doorip.api.dto + +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity + +data class ApiResponse( + val status: Int, + val code: String, + val message: String, + val data: T?, +) { + + companion object { + fun ok( + data: T? = null, + ): ResponseEntity> = ResponseEntity.ok( + ApiResponse( + status = 200, + code = "s2000", + message = "요청이 성공했습니다.", + data = data, + ), + ) + + fun created( + data: T? = null, + ): ResponseEntity> = ResponseEntity.status( + HttpStatus.CREATED, + ).body( + ApiResponse( + status = 201, + code = "s2010", + message = "요청이 성공했습니다.", + data = data, + ), + ) + } +} diff --git a/presentation/api/src/main/kotlin/org/doorip/api/dto/ExceptionResponse.kt b/presentation/api/src/main/kotlin/org/doorip/api/dto/ExceptionResponse.kt new file mode 100644 index 0000000..4898315 --- /dev/null +++ b/presentation/api/src/main/kotlin/org/doorip/api/dto/ExceptionResponse.kt @@ -0,0 +1,7 @@ +package org.doorip.api.dto + +data class ExceptionResponse( + val status: Int, + val code: String, + val message: String?, +) From fec9060bd16d52856eed64b6f30da5e92707c6e2 Mon Sep 17 00:00:00 2001 From: sunwoong Date: Mon, 2 Dec 2024 22:22:44 +0900 Subject: [PATCH 3/5] =?UTF-8?q?#3=20feat:=20api=20exception=20handler=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/exception/ApiExceptionHandler.kt | 58 +++++++++++++++++++ .../api/exception/ExceptionResponseFactory.kt | 46 +++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 presentation/api/src/main/kotlin/org/doorip/api/exception/ApiExceptionHandler.kt create mode 100644 presentation/api/src/main/kotlin/org/doorip/api/exception/ExceptionResponseFactory.kt diff --git a/presentation/api/src/main/kotlin/org/doorip/api/exception/ApiExceptionHandler.kt b/presentation/api/src/main/kotlin/org/doorip/api/exception/ApiExceptionHandler.kt new file mode 100644 index 0000000..fb83814 --- /dev/null +++ b/presentation/api/src/main/kotlin/org/doorip/api/exception/ApiExceptionHandler.kt @@ -0,0 +1,58 @@ +package org.doorip.api.exception + +import org.doorip.api.dto.ExceptionResponse +import org.doorip.domain.DooripException +import org.doorip.domain.InvalidRequestValueException +import org.doorip.domain.MethodNotAllowedException +import org.doorip.domain.UnknownException +import org.springframework.http.ResponseEntity +import org.springframework.validation.BindException +import org.springframework.web.HttpRequestMethodNotSupportedException +import org.springframework.web.bind.MethodArgumentNotValidException +import org.springframework.web.bind.annotation.ControllerAdvice +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException +import org.springframework.web.servlet.resource.NoResourceFoundException + +typealias ExceptionResponseEntity = ResponseEntity + +@ControllerAdvice +internal class ApiExceptionHandler( + private val exceptionResponseFactory: ExceptionResponseFactory, +) { + + @ExceptionHandler(MethodArgumentTypeMismatchException::class) + protected fun handleException(ex: MethodArgumentTypeMismatchException): ExceptionResponseEntity { + return exceptionResponseFactory.create(InvalidRequestValueException) + } + + @ExceptionHandler(MethodArgumentNotValidException::class) + protected fun handleException(ex: MethodArgumentNotValidException): ExceptionResponseEntity { + return exceptionResponseFactory.create(InvalidRequestValueException) + } + + @ExceptionHandler(BindException::class) + protected fun handleException(ex: BindException): ExceptionResponseEntity { + return exceptionResponseFactory.create(InvalidRequestValueException) + } + + @ExceptionHandler(NoResourceFoundException::class) + protected fun handleException(ex: NoResourceFoundException): ExceptionResponseEntity { + return exceptionResponseFactory.create(InvalidRequestValueException) + } + + @ExceptionHandler(HttpRequestMethodNotSupportedException::class) + protected fun handleException(ex: HttpRequestMethodNotSupportedException): ExceptionResponseEntity { + return exceptionResponseFactory.create(MethodNotAllowedException) + } + + @ExceptionHandler(DooripException::class) + protected fun handleException(ex: DooripException): ExceptionResponseEntity { + return exceptionResponseFactory.create(ex) + } + + @ExceptionHandler(Exception::class) + protected fun handleException(ex: Exception): ExceptionResponseEntity { + return exceptionResponseFactory.create(UnknownException(ex)) + } +} diff --git a/presentation/api/src/main/kotlin/org/doorip/api/exception/ExceptionResponseFactory.kt b/presentation/api/src/main/kotlin/org/doorip/api/exception/ExceptionResponseFactory.kt new file mode 100644 index 0000000..ba96c8d --- /dev/null +++ b/presentation/api/src/main/kotlin/org/doorip/api/exception/ExceptionResponseFactory.kt @@ -0,0 +1,46 @@ +package org.doorip.api.exception + +import org.doorip.api.dto.ExceptionResponse +import org.doorip.domain.ClientException +import org.doorip.domain.ConflictException +import org.doorip.domain.CriticalException +import org.doorip.domain.DooripException +import org.doorip.domain.MethodNotAllowedException +import org.doorip.domain.NotFoundException +import org.doorip.domain.ServerException +import org.doorip.domain.UnauthenticatedException +import org.doorip.domain.UnauthorizedException +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.stereotype.Component + +@Component +internal class ExceptionResponseFactory { + + fun create(exception: DooripException): ResponseEntity { + val httpStatus = exception.getHttpStatus() + + val exceptionResponse = ExceptionResponse( + status = httpStatus.value(), + code = exception.code, + message = exception.message, + ) + + return ResponseEntity.status(httpStatus) + .body(exceptionResponse) + } +} + +internal fun DooripException.getHttpStatus(): HttpStatus = + when (this) { + is UnauthorizedException -> HttpStatus.FORBIDDEN + is UnauthenticatedException -> HttpStatus.UNAUTHORIZED + + MethodNotAllowedException -> HttpStatus.METHOD_NOT_ALLOWED + ConflictException -> HttpStatus.CONFLICT + + NotFoundException -> HttpStatus.NOT_FOUND + + is ClientException -> HttpStatus.BAD_REQUEST + is ServerException, is CriticalException -> HttpStatus.INTERNAL_SERVER_ERROR + } From d5b1ef27298f457c483f8bb4bab89471e24f27a5 Mon Sep 17 00:00:00 2001 From: sunwoong Date: Mon, 2 Dec 2024 22:23:02 +0900 Subject: [PATCH 4/5] =?UTF-8?q?#3=20feat:=20test=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/org/doorip/core/TestService.kt | 12 ++++++++ .../main/kotlin/org/doorip/TestController.kt | 11 ------- .../kotlin/org/doorip/api/TestController.kt | 29 +++++++++++++++++++ 3 files changed, 41 insertions(+), 11 deletions(-) create mode 100644 core/src/main/kotlin/org/doorip/core/TestService.kt delete mode 100644 presentation/api/src/main/kotlin/org/doorip/TestController.kt create mode 100644 presentation/api/src/main/kotlin/org/doorip/api/TestController.kt diff --git a/core/src/main/kotlin/org/doorip/core/TestService.kt b/core/src/main/kotlin/org/doorip/core/TestService.kt new file mode 100644 index 0000000..cb9ec0b --- /dev/null +++ b/core/src/main/kotlin/org/doorip/core/TestService.kt @@ -0,0 +1,12 @@ +package org.doorip.core + +import org.doorip.domain.InvalidRequestValueException +import org.springframework.stereotype.Service + +@Service +class TestService { + + fun throwDooripException() { + throw InvalidRequestValueException + } +} diff --git a/presentation/api/src/main/kotlin/org/doorip/TestController.kt b/presentation/api/src/main/kotlin/org/doorip/TestController.kt deleted file mode 100644 index 3fe7590..0000000 --- a/presentation/api/src/main/kotlin/org/doorip/TestController.kt +++ /dev/null @@ -1,11 +0,0 @@ -package org.doorip - -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.RestController - -@RestController -class TestController { - - @GetMapping("/api/test") - fun test() = "doorip ok" -} diff --git a/presentation/api/src/main/kotlin/org/doorip/api/TestController.kt b/presentation/api/src/main/kotlin/org/doorip/api/TestController.kt new file mode 100644 index 0000000..0c0be10 --- /dev/null +++ b/presentation/api/src/main/kotlin/org/doorip/api/TestController.kt @@ -0,0 +1,29 @@ +package org.doorip.api + +import org.doorip.api.dto.ApiResponse +import org.doorip.core.TestService +import org.springframework.http.ResponseEntity +import org.springframework.stereotype.Controller +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.ResponseBody + +@Controller +class TestController( + private val testService: TestService, +) { + + @ResponseBody + @GetMapping("/api/test") + fun test() = "doorip ok" + + @GetMapping("/api/test/ok") + fun ok(): ResponseEntity> { + return ApiResponse.ok() + } + + @ResponseBody + @GetMapping("/api/test/ex") + fun exception() { + testService.throwDooripException() + } +} From 837eb83b56776fb13f1954a44f0443a7441960e7 Mon Sep 17 00:00:00 2001 From: sunwoong Date: Mon, 2 Dec 2024 22:37:37 +0900 Subject: [PATCH 5/5] =?UTF-8?q?#3=20refactor:=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- domain/src/main/kotlin/org/doorip/domain/DooripException.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain/src/main/kotlin/org/doorip/domain/DooripException.kt b/domain/src/main/kotlin/org/doorip/domain/DooripException.kt index 38f8d64..d403aaa 100644 --- a/domain/src/main/kotlin/org/doorip/domain/DooripException.kt +++ b/domain/src/main/kotlin/org/doorip/domain/DooripException.kt @@ -44,4 +44,4 @@ sealed class CriticalException( class UnknownException( cause: Throwable? = null, -) : CriticalException("e6000", "정의되지 않은 예외입니다. (로그 확인이 필요합니다.)") +) : CriticalException("e6000", "정의되지 않은 예외입니다. (로그 확인이 필요합니다.)", cause)