새로 만들고 있는 프로젝트에서 기존에 만들었던 프로젝트의 API를 사용하게 될 일이 생겼다. (MSA 구조 아님)
(새로 만드는 웹 프로젝트와 기존 프로젝트는 앱 프로젝트는 같은 데이터를 사용하고 response의 형식만(keyValue <-> List) 조금 다르게 사용)
FeignClient로 이어져있는 기존 프로젝트의 가칭 AAException
을 컨트롤해줘야하는 일이 생겼고, 이를 찾아보게 됨.
1. 기존 프로젝트
@GetMapping("uri")
fun get(): Response = service.getXXX()
fun getXXX(): Response {
내부Service.getXXXXX()
.fold(
onSuccess = {
return Response(어쩌구)
},
onFailure = {
if (it is AAException) {
throw InternalAAException(it.message) // ErrorCode = 400
} else {
throw InternalCustomException(it.message) // ErrorCode = 500
}
},
)
}
기존 프로젝트에서는 AAException 이 errorCode=500 으로 발생하는게 자연스럽지만,
새로 만드는 프로젝트에서 AAException
이 발생하지 않고 EmptyList를 반환 해야하므로
기존 프로젝트에서 원래 특정 조건에서 발생하는 AAException
을 잡아서 커스텀한 InternalAAException
(errorCode = 400) 을 발생하도록 한다.
-> error code 400은 다른걸로 대체할 수 있지만, 그냥 일단 실제 이 AAException의 에러코드가 400이기 때문에 400으로 함.
-> 400으로 하려고 했는데, 코드리뷰에서 기존 프로젝트에서 넘겨줄 때 Exception으로 넘겨주지 않는게 좋다고 해서 Response에 기본값을 넣어서 보내기로 하고 새로 만드는 프로젝트에서 이 응답값으로 분기처리를 나눠서 하기로 했다.
InternalAAException
이 발생했을 경우에 GlobalExceptionHandler
에서 잡아오고, 이를 새로 만드는 프로젝트에서 사용하는(기존 프로젝트의 Exception Response와 다름) Exception Response와 동일한 클래스를 만들어서 이를 보내준다.
@ExceptionHandler(InternalAAElementException::class)
@ResponseStatus(HttpStatus.BAD_REQUEST) -> 400
fun handleInternalNoSuchElementException(ex: Exception, request: HttpServletRequest): InternalExceptionResponse {
return InternalExceptionResponse.of(request, ex)
// 오직 새로운 프로젝트에 Exception이 발생했을 때를 위해 클래스를 만들어줌
}
@ExceptionHandler(InternalCustomException::class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) -> 500
fun handleInternalCustomException(ex: Exception, request: HttpServletRequest): InternalExceptionResponse {
return InternalExceptionResponse.of(request, ex)
}
InternalExceptionResponse는 대략적으로 아래와 같이 구성돼있다. (기존 프로젝트에서는 이거 사용 안한다.)
class InternalExceptionResponse private constructor(
val path: String,
val message: String?,
) {
val timestamp: OffsetDateTime = OffsetDateTime.now()
}
2. 새로 만드는 프로젝트
@Configuration
@EnableFeignClients(clients = [어쩌구FeignClient::class])
class FeignClientConfig {
@Bean
fun errorDecoder(): ErrorDecoder {
return FeignClientErrorDecoder()
}
class FeignClientErrorDecoder : ErrorDecoder {
override fun decode(methodKey: String, response: Response): Exception {
if (response.status() == HttpStatus.INTERNAL_SERVER_ERROR.value()) {
// error code = 500인 경우에만 아래와 같이 처리함
parse(response)?.let {
throw InternalCustomException(message = it.message.toString())
// 기존 프로젝트처럼 Exceptiond을 하나 커스텀하게 만들어준다.
}
}
return ErrorDecoder.Default().decode(methodKey, response)
}
private fun parse(response: Response): ExceptionResponse? {
return runCatching {
objectMapper.readValue(response.body().asInputStream(), ExceptionResponse::class.java)
}.getOrNull()
}
private val objectMapper = jacksonObjectMapper().findAndRegisterModules()
}
}
fun getXXX(): Response {
val model = runCatching {
어쩌구FeignClient.getXXXXX()
}.fold(
onSuccess = { XXXModel.from(it) },
onFailure = { return Response.notXXXXXX() },
)
return Response.from(model)
}
어쩌구FeignClient.getXXXXX()
를 호출했을 때, errorCode = 500이 발생하면 위에 말했듯이 FeignClientErrorDecoder
에서 Exception을 던져서 새로운 프로젝트의 GlobalExceptionHandler로 잡히고, 그게 아니면 runCatching의 onFailure로 온다.
Response.notXXXXXX()
가 내가 원하던 EmptyList를 응답하게하는 메소드이다.
참고