Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

YW2-237 feat: 공통 Response 및 ControllerAdvice 정의 #5

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.dangle.api.common.error

import com.dangle.api.common.phase.ActiveProfilesResolver
import com.dangle.api.common.response.ApiResponse
import com.dangle.common.DangleErrorCode
import com.dangle.common.DangleException
import org.slf4j.LoggerFactory
import org.springframework.http.HttpStatus
import org.springframework.http.HttpStatusCode
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.RestControllerAdvice

@RestControllerAdvice
class DangleApiExceptionControllerAdvice(
private val activeProfilesResolver: ActiveProfilesResolver
) {

private val logger = LoggerFactory.getLogger(this::class.java)

@ExceptionHandler(DangleException::class)
fun handleDangleException(e: DangleException): ResponseEntity<ApiResponse<Any?>>{
return ResponseEntity(
ApiResponse.error(
errorCode = e.errorCode,
error = e.toError(),
debug = e.toDebug()
),
e.errorCode.httpStatusCode()
)
}

// 별도로 Exception 을 처리할 것들은 이앞에서 다시 처리할 것
@ExceptionHandler(Throwable::class)
fun handleUnKnownException(e: Throwable): ResponseEntity<ApiResponse<Any?>>{
logger.error(e.stackTraceToString())
return handleDangleException(
DangleException(
errorCode = DangleErrorCode.INTERNAL_SERVER_ERROR,
debug = e.message,
throwable = e
)
)
}

private fun DangleErrorCode.httpStatusCode(): HttpStatusCode{
return when (value) {
in 4000 until 4100 -> HttpStatus.BAD_REQUEST
in 4100 until 4200 -> HttpStatus.NOT_FOUND
in 5000 until 5100 -> HttpStatus.INTERNAL_SERVER_ERROR
else -> HttpStatus.INTERNAL_SERVER_ERROR
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

에러코드 범위 확인했습니다~!

}
}


private fun DangleException.toError(): String {
return this.errorCode.name
}
private fun DangleException.toDebug(): String?{
if(activeProfilesResolver.isPrd()){
return null
}
return this.stackTraceToString()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오호 Prod 환경에서는 스택트레이스를 노출하지 않는군요,,

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.dangle.api.common.error

import com.dangle.common.DangleErrorCode

data class ErrorResponse(
val code: Int,
val message: String,
val data: String? = null
Copy link
Member

@rilac1 rilac1 Oct 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ErrorResponse의 data 부는 어떤 값들이 들어가게 될까요?

특별한 값이 아니라면, DangleExeption만으로도 표현이 가능할 것 같아서요!
(message는 사용자에게 직접 노출되는 에러메시지)

data class Error<T>(
    val code: String,
    val message: String,
    val debug: String? = null,
) : ApiResponse<T> {
    override val result = ResponseType.ERROR
}

ApiResponse.Error(
    error = dangleException.errorCode.value,
    message = dangleException.errorCode.message,
    debug = dangleException.debug,
)

){
constructor(errorCode: DangleErrorCode,data:String?):this(
code = errorCode.value,
message = errorCode.message,
data = data

)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.dangle.api.common.phase

import org.springframework.core.env.Environment
import org.springframework.stereotype.Component

@Component
class ActiveProfilesResolver(
env: Environment
){

private val activeProfilePhases = ActiveProfilePhase.values().map { it.phase }.toSet()

private val currentProfiles = env.activeProfiles
.filter { it in activeProfilePhases }
.associateBy{ it }
.ifEmpty { mapOf(ActiveProfilePhase.LOCAL.phase to ActiveProfilePhase.LOCAL) }
fun isPrd() : Boolean{
return currentProfiles[ActiveProfilePhase.PRD.phase] != null
}

fun isDev() : Boolean{
return currentProfiles[ActiveProfilePhase.DEV.phase] != null
}

fun isLocal(): Boolean{
return currentProfiles[ActiveProfilePhase.LOCAL.phase] != null
}
private enum class ActiveProfilePhase(val phase: String) {
LOCAL("local"),
DEV("dev"),
PRD("prd"),
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.dangle.api.common.response

import com.dangle.api.common.error.ErrorResponse
import com.dangle.common.DangleErrorCode

class ApiResponse<T> private constructor(
val result: ResponseType,
val data: T? = null,
val error: ErrorResponse? = null,
val debug: String? = null
){
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분 SealedInterface 적용해보는건 어떨까요?

sealed interface ApiResponse<T> {
    val result: ResponseType

    data class Success<T>(
        val data: T?
    ) : ApiResponse<T> {
        override val result = ResponseType.SUCCESS
    }

    data class Error<T>(
        val error: ErrorResponse? = null,
        val debug: String? = null
    ) : ApiResponse<T> {
        override val result = ResponseType.ERROR
    }
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sealed interface를 잘 사용해볼 버릇을 안해봐서 정확한 사용처? 에 대해서 아직 와닿지않는 것 같습니다 ㅎㅎ..
사실 사용해본 것도 enum과 같이 when절 -> else 미정의시 컴파일 타임에 에러 잡기위한 용도 정도밖에 없었어서요

혹시 sealed 키워드 (클래스, 인터페이스 포함) 를 사용하시는 기준이 있을까요?
그와 더불어서, 제안해주신 코드 대로 진행한다면 성공때와 실패때의 응답이 달라질 것 같은데 혹시맞을까요?
클라이언트 측에서는 어차피 하나의 모델로 대응가능할 것 같긴합니다만


companion object{
fun<T> success(result: T): ApiResponse<T>{
return ApiResponse(
result = ResponseType.SUCCESS,
data = result,
)
}

fun<T> success(result: List<T>) : ApiResponse<List<T>>{
return ApiResponse(
result = ResponseType.SUCCESS,
data = result
)
}

fun<T> error(errorCode: DangleErrorCode,error: String?, debug:String? ):ApiResponse<T>{
return ApiResponse(
result = ResponseType.ERROR,
data = null,
error = ErrorResponse(
errorCode = errorCode,
data = error,
),
debug = debug
)
}
}
enum class ResponseType{
SUCCESS,ERROR
}

override fun toString(): String {
return "ApiResponse(result=$result, data=$data, error=$error, debug=$debug)"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ package com.dangle.api.v1.shelter

import com.dangle.api.common.resolver.VolunteerAuthentication
import com.dangle.api.common.resolver.VolunteerAuthenticationInfo
import com.dangle.api.common.response.ApiResponse
import com.dangle.usecase.shelter.port.`in`.command.ToggleBookmarkCommandUseCase
import com.dangle.usecase.shelter.port.`in`.query.GetShelterQueryUseCase
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("/v1/shelter")
class ShelterController(
private val getShelterQueryUseCase: GetShelterQueryUseCase,
private val toggleBookmarkCommandUseCase: ToggleBookmarkCommandUseCase,
Expand All @@ -18,8 +21,8 @@ class ShelterController(
fun getShelterInfo(
@PathVariable shelterId: Long,
@VolunteerAuthentication volunteerInfo: VolunteerAuthenticationInfo?,
): GetShelterResponse {
return getShelterQueryUseCase.invoke(shelterId).let {
): ApiResponse<GetShelterResponse> {
val response = getShelterQueryUseCase.invoke(shelterId).let {
GetShelterResponse(
id = it.id,
name = it.name,
Expand All @@ -37,14 +40,16 @@ class ShelterController(
bookMarked = false, // TODO(kang) 북마크 처리 필요
)
}

return ApiResponse.success(response)
}

@PostMapping("/{shelterId}/bookmark")
fun bookmarkShelter(
@PathVariable shelterId: Long,
@VolunteerAuthentication volunteerInfo: VolunteerAuthenticationInfo
): BookMarkShelterResponse {
return toggleBookmarkCommandUseCase.invoke(
): ApiResponse<BookMarkShelterResponse> {
val response = toggleBookmarkCommandUseCase.invoke(
ToggleBookmarkCommandUseCase.Command(
shelterId = shelterId,
volunteerId = volunteerInfo.volunteerId,
Expand All @@ -56,6 +61,8 @@ class ShelterController(
bookMarked = it.bookMarked,
)
}

return ApiResponse.success(response)
}

data class GetShelterResponse(
Expand Down
1 change: 1 addition & 0 deletions dangle-api/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ spring:
mvc:
pathmatch:
matching-strategy: ant_path_matcher
throw-exception-if-no-handler-found: true
jackson:
property-naming-strategy: SNAKE_CASE
transaction:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ enum class DangleErrorCode(
val value: Int,
val message: String,
) {
// 4xxx. Not Found Error
NOT_FOUND(4000, "잘못된 요청이에요."),
NOT_FOUND_SHELTER(4001, "보호소를 찾을 수 없어요."),
// 40xx. Bad Request
BAD_REQUEST(4000, "잘못된 요청이에요."),
// 41xx. Not Found Error
NOT_FOUND(4100, "정보를 찾을 수 없어요."),
NOT_FOUND_SHELTER(4101, "보호소를 찾을 수 없어요."),

// 5xxx. Internal Server Error
INTERNAL_SERVER_ERROR(5000,"시스템 내부 에러가 발생했어요.")
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.dangle.common

class DangleException(
errorCode: DangleErrorCode,
debug: String? = null,
throwable: Throwable? = null,
val errorCode: DangleErrorCode,
val debug: String? = null,
val throwable: Throwable? = null,
) : RuntimeException("$errorCode${debug?.let { " - $it" }}", throwable)