[#26] feat: application 전체 공통 response 규격화 작업
- ApplicationResponse 통한 공통 response 규격화 - BusinessException 적용(로직 상 예측 가능한 예외) - Exception handler 적용(ControllerAdvice)
This commit is contained in:
@@ -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 = "계정 혹은 비밀번호가 일치하지 않습니다."
|
||||
)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package io.beaniejoy.dongnecafe.common.error.constant
|
||||
|
||||
enum class Domain {
|
||||
AUTH,
|
||||
CAFE,
|
||||
CAFE_MENU
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package io.beaniejoy.dongnecafe.common.error.constant
|
||||
|
||||
enum class SubCategory {
|
||||
NOT_FOUND,
|
||||
EXISTED
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package io.beaniejoy.dongnecafe.common.response
|
||||
|
||||
enum class ResultCode {
|
||||
SUCCESS,
|
||||
FAIL;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package io.beaniejoy.dongnecafe.error
|
||||
|
||||
data class ErrorResponse(
|
||||
val code: Int,
|
||||
val message: String?
|
||||
)
|
||||
@@ -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) }
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
package io.beaniejoy.dongnecafe.error.exception
|
||||
|
||||
class CafeExistedException(name: String) : RuntimeException("Cafe[$name] is already existed")
|
||||
@@ -1,3 +0,0 @@
|
||||
package io.beaniejoy.dongnecafe.error.exception
|
||||
|
||||
class CafeNotFoundException(cafeId: Long) : RuntimeException("Cafe[$cafeId] is not found")
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user