[#26] feat: application 전체 공통 response 규격화 작업

- ApplicationResponse 통한 공통 response 규격화
- BusinessException 적용(로직 상 예측 가능한 예외)
- Exception handler 적용(ControllerAdvice)
This commit is contained in:
beaniejoy
2022-12-03 20:43:15 +09:00
parent 30386a2f28
commit cab20de7b1
16 changed files with 157 additions and 47 deletions

View File

@@ -1,6 +1,6 @@
package io.beaniejoy.dongnecafe.error.handler
import io.beaniejoy.dongnecafe.error.ErrorResponse
import io.beaniejoy.dongnecafe.common.response.ApplicationResponse
import mu.KLogging
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
@@ -15,10 +15,10 @@ class CommonControllerAdvice {
companion object : KLogging()
@ExceptionHandler(AuthenticationException::class)
fun handleAuthenticationException(e: AuthenticationException): ResponseEntity<ErrorResponse> {
fun handleAuthenticationException(e: AuthenticationException): ResponseEntity<ApplicationResponse<Any?>> {
logger.error { "AuthenticationException: ${e.message}" }
return ResponseEntity.ok().body(
ErrorResponse(
ApplicationResponse(
code = HttpStatus.BAD_REQUEST.value(),
message = "계정 혹은 비밀번호가 일치하지 않습니다."
)

View File

@@ -0,0 +1,23 @@
package io.beaniejoy.dongnecafe.common.error.advice
import io.beaniejoy.dongnecafe.common.error.exception.BusinessException
import io.beaniejoy.dongnecafe.common.response.ApplicationResponse
import mu.KLogging
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestControllerAdvice
@RestControllerAdvice
class BasicControllerAdvice {
companion object: KLogging()
@ResponseStatus(HttpStatus.OK)
@ExceptionHandler(BusinessException::class)
fun handleBusinessException(e: BusinessException): ApplicationResponse {
logger.error { "[BusinessException] ${e.errorCode.name}" }
return ApplicationResponse.fail(e.errorCode, "error")
}
}

View File

@@ -0,0 +1,7 @@
package io.beaniejoy.dongnecafe.common.error.constant
enum class Domain {
AUTH,
CAFE,
CAFE_MENU
}

View File

@@ -0,0 +1,13 @@
package io.beaniejoy.dongnecafe.common.error.constant
import io.beaniejoy.dongnecafe.common.error.constant.Domain.CAFE
import io.beaniejoy.dongnecafe.common.error.constant.SubCategory.*
enum class ErrorCode(
val domain: Domain,
val subCategory: SubCategory
) {
// CAFE
CAFE_NOT_FOUND(CAFE, NOT_FOUND),
CAFE_EXISTED(CAFE, EXISTED);
}

View File

@@ -0,0 +1,6 @@
package io.beaniejoy.dongnecafe.common.error.constant
enum class SubCategory {
NOT_FOUND,
EXISTED
}

View File

@@ -0,0 +1,30 @@
package io.beaniejoy.dongnecafe.common.error.exception
import io.beaniejoy.dongnecafe.common.error.constant.ErrorCode
/*
* Business Logic 상 발생 가능한 Exception
* - 로직상 개발자가 예측 가능한 예외
* - front 측면에서 해당 에러에 대해서 error code(4xx, 5xx)보다 success code(2xx)를 응답받게 설계
* - front에서 해당 예외 응답에 대해서 ErrorResponse의 Result field로 따로 구분해서 처리가능
*/
class BusinessException : RuntimeException {
var errorCode: ErrorCode
private set
constructor(errorCode: ErrorCode) : super(errorCode.name){
this.errorCode = errorCode
}
constructor(errorCode: ErrorCode, message: String): super(message) {
this.errorCode = errorCode
}
constructor(errorCode: ErrorCode, cause: Throwable) : super(cause) {
this.errorCode = errorCode
}
constructor(errorCode: ErrorCode, message: String, cause: Throwable) : super(message, cause) {
this.errorCode = errorCode
}
}

View File

@@ -0,0 +1,46 @@
package io.beaniejoy.dongnecafe.common.response
import com.fasterxml.jackson.annotation.JsonInclude
import io.beaniejoy.dongnecafe.common.error.constant.ErrorCode
@JsonInclude(JsonInclude.Include.NON_NULL)
class ApplicationResponse {
var result: ResultCode
private set
var data: Any? = null
private set
var message: String?
private set
var errorCode: String? = null
private set
constructor(resultCode: ResultCode, message: String?) {
this.result = resultCode
this.message = message
}
constructor(resultCode: ResultCode, errorCode: ErrorCode, message: String?) {
this.result = resultCode
this.errorCode = errorCode.name
this.message = message
}
companion object {
fun success(message: String? = null): ApplicationResponse {
return ApplicationResponse(ResultCode.SUCCESS, message)
}
fun fail(errorCode: ErrorCode, message: String?): ApplicationResponse {
return ApplicationResponse(ResultCode.FAIL, errorCode, message)
}
}
fun data(data: Any): ApplicationResponse {
this.data = data
return this
}
}

View File

@@ -0,0 +1,6 @@
package io.beaniejoy.dongnecafe.common.response
enum class ResultCode {
SUCCESS,
FAIL;
}

View File

@@ -1,6 +0,0 @@
package io.beaniejoy.dongnecafe.error
data class ErrorResponse(
val code: Int,
val message: String?
)

View File

@@ -22,9 +22,10 @@ class SecurityConfig {
fun filterChain(http: HttpSecurity): SecurityFilterChain {
return http
.csrf().disable()
.formLogin().disable()
.authorizeRequests()
.anyRequest().authenticated()
.anyRequest().permitAll()
.and()
.also { jwtAuthenticationConfigurer(it) }

View File

@@ -1,5 +1,6 @@
package io.beaniejoy.dongnecafe.domain.cafe.controller
import io.beaniejoy.dongnecafe.common.response.ApplicationResponse
import io.beaniejoy.dongnecafe.domain.cafe.model.request.CafeRegisterRequest
import io.beaniejoy.dongnecafe.domain.cafe.model.request.CafeUpdateRequest
import io.beaniejoy.dongnecafe.domain.cafe.model.response.CafeDetailedInfo
@@ -20,14 +21,16 @@ class CafeController(
* 신규 카페 생성
*/
@PostMapping
fun createNewCafe(@RequestBody resource: CafeRegisterRequest): Long {
return cafeService.createNew(
fun createNewCafe(@RequestBody resource: CafeRegisterRequest): ApplicationResponse {
val newCafeId = cafeService.createNew(
name = resource.name!!,
address = resource.address!!,
phoneNumber = resource.phoneNumber!!,
description = resource.description!!,
cafeMenuRequestList = resource.cafeMenuList
)
return ApplicationResponse.success("OK").data(newCafeId)
}
/**
@@ -36,8 +39,8 @@ class CafeController(
@GetMapping
fun searchCafeList(
@PageableDefault(sort = ["name"], direction = Sort.Direction.ASC, page = 0, size = 10) pageable: Pageable
): Page<CafeSearchInfo> {
return cafeService.searchCafeList(pageable)
): ApplicationResponse {
return ApplicationResponse.success().data(cafeService.searchCafeList(pageable))
}
/**

View File

@@ -1,12 +1,12 @@
package io.beaniejoy.dongnecafe.domain.cafe.service
import io.beaniejoy.dongnecafe.domain.cafe.entity.Cafe
import io.beaniejoy.dongnecafe.domain.cafe.model.request.CafeMenuRegisterRequest
import io.beaniejoy.dongnecafe.domain.cafe.model.response.CafeDetailedInfo
import io.beaniejoy.dongnecafe.domain.cafe.model.response.CafeSearchInfo
import io.beaniejoy.dongnecafe.domain.cafe.model.request.CafeMenuRegisterRequest
import io.beaniejoy.dongnecafe.domain.cafe.entity.Cafe
import io.beaniejoy.dongnecafe.error.exception.CafeExistedException
import io.beaniejoy.dongnecafe.error.exception.CafeNotFoundException
import io.beaniejoy.dongnecafe.domain.cafe.repository.CafeRepository
import io.beaniejoy.dongnecafe.common.error.constant.ErrorCode
import io.beaniejoy.dongnecafe.common.error.exception.BusinessException
import mu.KLogging
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
@@ -55,7 +55,7 @@ class CafeService(
private fun checkCafeExistedByName(name: String) {
val findCafe = cafeRepository.findByName(name)
if (findCafe != null) {
throw CafeExistedException(name)
throw BusinessException(ErrorCode.CAFE_EXISTED)
}
}
@@ -67,7 +67,7 @@ class CafeService(
fun getDetailedInfoByCafeId(id: Long): CafeDetailedInfo {
val cafe = cafeRepository.findByIdOrNull(id)
?: throw CafeNotFoundException(id)
?: throw BusinessException(ErrorCode.CAFE_NOT_FOUND)
return CafeDetailedInfo.of(cafe)
}
@@ -85,7 +85,7 @@ class CafeService(
description: String,
) {
val cafe = cafeRepository.findByIdOrNull(id)
?: throw CafeNotFoundException(id)
?: throw BusinessException(ErrorCode.CAFE_NOT_FOUND)
cafe.updateInfo(
name = name,

View File

@@ -1,15 +0,0 @@
package io.beaniejoy.dongnecafe.error
import io.beaniejoy.dongnecafe.error.exception.CafeNotFoundException
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.RestControllerAdvice
@RestControllerAdvice
class CafeExceptionHandler {
// TODO: error 규격화
@ExceptionHandler(CafeNotFoundException::class)
fun handleNotFound(exception: CafeNotFoundException): ResponseEntity<String> {
return ResponseEntity.badRequest().body(exception.message)
}
}

View File

@@ -1,3 +0,0 @@
package io.beaniejoy.dongnecafe.error.exception
class CafeExistedException(name: String) : RuntimeException("Cafe[$name] is already existed")

View File

@@ -1,3 +0,0 @@
package io.beaniejoy.dongnecafe.error.exception
class CafeNotFoundException(cafeId: Long) : RuntimeException("Cafe[$cafeId] is not found")

View File

@@ -1,10 +1,10 @@
package io.beaniejoy.dongnecafe.domain.cafe.service
import io.beaniejoy.dongnecafe.domain.cafe.entity.Cafe
import io.beaniejoy.dongnecafe.error.exception.CafeExistedException
import io.beaniejoy.dongnecafe.error.exception.CafeNotFoundException
import io.beaniejoy.dongnecafe.domain.cafe.repository.CafeRepository
import io.beaniejoy.dongnecafe.domain.cafe.utils.CafeTestUtils
import io.beaniejoy.dongnecafe.common.error.constant.ErrorCode
import io.beaniejoy.dongnecafe.common.error.exception.BusinessException
import org.junit.jupiter.api.*
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.extension.ExtendWith
@@ -68,7 +68,7 @@ internal class CafeServiceTest {
`when`(mockCafeRepository.findByName(name)).thenReturn(cafe)
// then
assertThrows<CafeExistedException> {
val exception = assertThrows<BusinessException> {
// when
mockCafeService.createNew(
name = name,
@@ -78,8 +78,8 @@ internal class CafeServiceTest {
cafeMenuRequestList = cafeMenuList
)
}
verify(mockCafeRepository).findByName(name)
assertEquals(ErrorCode.CAFE_EXISTED, exception.errorCode)
}
@Test
@@ -134,7 +134,7 @@ internal class CafeServiceTest {
`when`(mockCafeRepository.findById(cafeId)).thenReturn(Optional.empty())
assertThrows<CafeNotFoundException> {
val exception = assertThrows<BusinessException> {
mockCafeService.updateInfo(
id = cafeId,
name = "",
@@ -143,5 +143,7 @@ internal class CafeServiceTest {
description = "",
)
}
assertEquals(ErrorCode.CAFE_NOT_FOUND, exception.errorCode)
}
}