[Kotlin] FeignClient Exception 다루기

0

MSA

목록 보기
10/10
  • 새로 만들고 있는 프로젝트에서 기존에 만들었던 프로젝트의 API를 사용하게 될 일이 생겼다. (MSA 구조 아님)
    (새로 만드는 웹 프로젝트와 기존 프로젝트는 앱 프로젝트는 같은 데이터를 사용하고 response의 형식만(keyValue <-> List) 조금 다르게 사용)

    • 근데 기존 프로젝트에서 Exception이 발생하는 조건이 새로운 프로젝트에서는 Exception이 발생하지 않고 정상적으로 작동해야했다.
      (List로 반환하는데, 새로운 프로젝트에서는 Exception이 발생하지 않고 EmptyList로 반환해야함)
  • FeignClient로 이어져있는 기존 프로젝트의 가칭 AAException을 컨트롤해줘야하는 일이 생겼고, 이를 찾아보게 됨.

1. 기존 프로젝트

Controller

@GetMapping("uri")
    fun get(): Response = service.getXXX()

Service

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와 동일한 클래스를 만들어서 이를 보내준다.

GlobalExceptionHandler

@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. 새로 만드는 프로젝트

  • 기존 프로젝트를 FeignClient를 이용해서 연동하므로, FeignClient Config에서 설정이 필요하다.
  • FeignClientErrorDecoder를 만들어 주면, Service에서 기존 프로젝트를 호출했을 때 에러가 생기면 먼저 여기를 거친다.
@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()
    }
}
  • 내가 처리하고 싶은 AAException은 error code가 400이다.
    error code = 500인 경우에만 위에서 처리가 되고, 500이 아니면 service 단에서 처리해주면 된다.

Service

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를 응답하게하는 메소드이다.


핵심은

  • FeignClientErrorDecoder를 해주지 않으면 Error가 되게 어렵게 돼있다.
    기존 프로젝트에서 내보내주는 Exception Message를 사용하기 위해서는 꼭 decode해서 사용해야한다.
    decode를 하고 디버그를 해보면 사실 어려운 건 없을 것이다.
  • 참고에서 2번째 블로그를 보면 자세히 적혀있으니 참고하면 좋을 것 같다.

참고

profile
백엔드를 공부하고 있습니다.

0개의 댓글