Skip to content

Commit

Permalink
Merge pull request #9 from Team-Going/feature/3
Browse files Browse the repository at this point in the history
ISSUE-3 API 응답 Spec & 예외 Setting
  • Loading branch information
SunwoongH authored Dec 2, 2024
2 parents 68f5cd2 + 837eb83 commit d0c0801
Show file tree
Hide file tree
Showing 8 changed files with 237 additions and 11 deletions.
12 changes: 12 additions & 0 deletions core/src/main/kotlin/org/doorip/core/TestService.kt
Original file line number Diff line number Diff line change
@@ -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
}
}
47 changes: 47 additions & 0 deletions domain/src/main/kotlin/org/doorip/domain/DooripException.kt
Original file line number Diff line number Diff line change
@@ -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", "정의되지 않은 예외입니다. (로그 확인이 필요합니다.)", cause)
11 changes: 0 additions & 11 deletions presentation/api/src/main/kotlin/org/doorip/TestController.kt

This file was deleted.

29 changes: 29 additions & 0 deletions presentation/api/src/main/kotlin/org/doorip/api/TestController.kt
Original file line number Diff line number Diff line change
@@ -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<ApiResponse<Unit>> {
return ApiResponse.ok()
}

@ResponseBody
@GetMapping("/api/test/ex")
fun exception() {
testService.throwDooripException()
}
}
38 changes: 38 additions & 0 deletions presentation/api/src/main/kotlin/org/doorip/api/dto/ApiResponse.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package org.doorip.api.dto

import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity

data class ApiResponse<T>(
val status: Int,
val code: String,
val message: String,
val data: T?,
) {

companion object {
fun <T> ok(
data: T? = null,
): ResponseEntity<ApiResponse<T>> = ResponseEntity.ok(
ApiResponse(
status = 200,
code = "s2000",
message = "요청이 성공했습니다.",
data = data,
),
)

fun <T> created(
data: T? = null,
): ResponseEntity<ApiResponse<T>> = ResponseEntity.status(
HttpStatus.CREATED,
).body(
ApiResponse(
status = 201,
code = "s2010",
message = "요청이 성공했습니다.",
data = data,
),
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.doorip.api.dto

data class ExceptionResponse(
val status: Int,
val code: String,
val message: String?,
)
Original file line number Diff line number Diff line change
@@ -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<ExceptionResponse>

@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))
}
}
Original file line number Diff line number Diff line change
@@ -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<ExceptionResponse> {
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
}

0 comments on commit d0c0801

Please sign in to comment.