안녕하세요!
회사에서 프로젝트가 산더미 처럼 떨어져서, 아주 오랜만에 글을 이어가려 합니다.
오늘은 Bridge-Api 라이브러리의 에러핸들러 구현에 대해 이야기해보려고 하는데요!
에러핸들러는 라이브러리의 중요한 부분으로, 일관성 있고 효율적인 에러 처리를 가능하게 합니다.
1. 에러핸들러의 필요성
에러핸들러를 만들게 된 주요 이유는 다음과 같습니다:
- 일관성: 모든 에러를 일관된 방식으로 처리할 수 있습니다.
- 중앙화: 최상위 레벨에서 에러를 처리함으로써 코드 중복을 줄이고 관리를 용이하게 합니다.
- 유연성: 다양한 타입의 에러에 대해 각각 다른 처리 방식을 적용할 수 있습니다.
- 디버깅 용이성: 모든 에러가 한 곳에서 처리되므로 디버깅이 쉬워집니다.
2. 에러핸들러 인터페이스 설계
먼저, 에러핸들러의 인터페이스를 설계했습니다. 간단하면서도 유연한 구조를 위해 함수형 인터페이스를 사용했습니다.
fun interface ErrorHandler {
fun handle(cause: Throwable): Any?
}
이 인터페이스는 handle
메소드 하나만을 가지고 있어, 람다 표현식으로 쉽게 구현할 수 있습니다.
3. BridgeRouter에 에러핸들러 통합
다음으로, BridgeRouter
클래스에 에러핸들러를 통합했습니다.
class BridgeRouter private constructor(
private val objectMapper: ObjectMapper = jacksonObjectMapper(),
private val logger: Logger = getLogger(BridgeRouter::class.java),
private val routeTree: RouteNode = RouteNode(),
private val decoratedService: BridgeService,
private val errorHandlers: List<ErrorHandler> = emptyList(),
) {
// ... 기존 코드 ...
suspend fun routingRequest(
pathAndQueryString: String,
method: MethodType,
headers: Map<String, String> = emptyMap(),
body: Any? = null,
): String = try {
// ... 기존 라우팅 로직 ...
} catch (throwable: Throwable) {
val actualException = when (throwable) {
is InvocationTargetException -> throwable.targetException
else -> throwable
}
logger.error("Routing error: ${actualException.message}", actualException)
val results = errorHandlers.mapNotNull { it.handle(actualException) }
if (results.isEmpty()) "500" else results.first()
.serializeToJson(objectMapper)
}
}
이 구현에서 주목할 점은 다음과 같습니다:
- 여러 개의 에러핸들러를 등록할 수 있습니다.
- 등록된 순서대로 에러핸들러를 실행합니다.
- 첫 번째로 null이 아닌 결과를 반환하는 핸들러의 결과를 사용합니다.
- 모든 핸들러가 null을 반환하면 기본적으로 "500" 에러를 반환합니다.
여기서 InvocationTargetException
은 리플렉션을 사용하여 메서드를 호출할 때 발생할 수 있는 예외입니다. 이 예외는 실제 발생한 예외를 감싸고 있어, 우리는 targetException
을 통해 원본 예외에 접근합니다. InvocationTargetException에 대한 자세한 설명은 이 링크를 참고하시기 바랍니다.
4. 에러핸들러 등록 방법
에러핸들러를 등록하는 방법은 BridgeRouter.Builder
에 추가했습니다:
class Builder {
// ... 기존 코드 ...
fun registerErrorHandler(errorHandler: ErrorHandler): Builder {
errorHandlers.add(errorHandler)
logger.debug("ErrorHandler registered: $errorHandler")
return this
}
fun registerAllErrorHandlers(errorHandlers: List<ErrorHandler>): Builder {
this.errorHandlers.addAll(errorHandlers)
logger.debug("ErrorHandlers registered: {}", errorHandlers)
return this
}
// ... 기존 코드 ...
}
이렇게 하면 사용자가 쉽게 커스텀 에러핸들러를 등록할 수 있습니다.
5. 사용 예시
에러핸들러의 사용 예시는 다음과 같습니다:
val serviceExceptionHandler = ErrorHandler { throwable ->
when (throwable) {
is TestServiceException -> {
ApiCommonResponse(
status = -1,
message = "TestServiceException occurred",
data = ErrorData(throwable),
)
}
is TestServiceException2 -> {
ApiCommonResponse(
status = -2,
message = "TestServiceException2 occurred",
data = ErrorData(throwable),
)
}
else -> null
}
}
val universalExceptionHandler = ErrorHandler { throwable ->
ApiCommonResponse(
status = -99,
message = throwable.message ?: "Unknown error",
data = ErrorData(throwable),
)
}
val router = BridgeRouter.builder().apply {
registerErrorHandler(serviceExceptionHandler)
registerErrorHandler(universalExceptionHandler)
}.build()
이 예시에서는 두 개의 에러핸들러를 등록했습니다. 첫 번째 핸들러는 특정 예외를 처리하고, 두 번째 핸들러는 모든 예외를 처리합니다.
6. 마치며
에러핸들러를 구현하면서 느낀 점은 다음과 같습니다:
- 유연성의 중요성: 다양한 상황에 대응할 수 있는 유연한 설계가 중요합니다.
- 추상화의 중요성: 간단한 인터페이스로 복잡한 에러 처리 로직을 추상화할 수 있었습니다.
이번 에러핸들러 구현을 통해 Bridge-Api 라이브러리의 안정성과 사용성이 꽤나 향상되었다고 생각합니다.
다음 글에서는 Bridge-Api의 또 다른 중요한 기능인 데코레이터 패턴을 활용한 서비스 인터셉터 구현에 대해 다루어보도록 하겠습니다.
+ 라이브러리 레포지토리 : https://github.com/shiniseong/bridge-api