에러 핸들링을 중앙에서 관리하는 방법: Spring Boot 3 + Kotlin + Armeria
Spring Boot 3와 Kotlin, 그리고 Armeria를 함께 사용할 때, 에러 핸들링을 중앙에서 일관성 있게 관리하는 방법을 소개합니다.
실무에서 자주 발생하는 문제와 해결법, 그리고 실전 적용 팁까지 초보자도 이해할 수 있도록 설명합니다.
Spring Boot 3, Kotlin, Armeria란?
Spring Boot 3는 최신 스프링 생태계의 핵심 프레임워크로, 빠른 개발과 최신 JVM 기능을 지원합니다. Kotlin은 간결하고 안전한 문법을 제공하는 JVM 언어입니다. Armeria는 비동기/논블로킹 HTTP, gRPC, Thrift 서버 구현을 지원하는 오픈소스 네트워크 프레임워크입니다. 이 세 가지를 조합하면 현대적인 마이크로서비스와 API 서버를 효율적으로 구축할 수 있습니다.
왜 에러 핸들링의 중앙 관리가 중요한가?
- 모든 API에서 일관된 에러 포맷을 제공할 수 있습니다.
- 예외 발생 시 공통 로깅, 모니터링, 알림 등을 쉽게 연동할 수 있습니다.
- 비즈니스 로직과 에러 처리 코드의 분리로 코드 가독성과 유지보수성이 높아집니다.
Spring Boot 3 + Kotlin + Armeria에서의 에러 핸들링 기본 구조
Spring Boot만 사용하는 경우에는 @ControllerAdvice
와 @ExceptionHandler
를 활용하여 예외를 중앙에서 처리합니다. 하지만 Armeria를 함께 사용할 때는 Armeria의 ExceptionHandlerFunction
또는 DecoratingHttpServiceFunction
을 활용해야 합니다.
1. Spring의 @ControllerAdvice 활용 (RestController)
@RestControllerAdvice
class GlobalExceptionHandler {
@ExceptionHandler(value = [CustomException::class])
fun handleCustomException(e: CustomException): ResponseEntity<ErrorResponse> {
return ResponseEntity.status(e.status).body(ErrorResponse(e.message ?: "알 수 없는 에러"))
}
@ExceptionHandler(Exception::class)
fun handleException(e: Exception): ResponseEntity<ErrorResponse> {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ErrorResponse("서버 내부 오류가 발생했습니다."))
}
}
data class ErrorResponse(val message: String)
class CustomException(val status: HttpStatus, message: String): RuntimeException(message)
2. Armeria의 ExceptionHandlerFunction 활용
Armeria의 엔드포인트(예: annotated service, route)에 직접 적용할 수 있습니다.
import com.linecorp.armeria.server.annotation.ExceptionHandlerFunction
import com.linecorp.armeria.server.annotation.ExceptionHandler
import com.linecorp.armeria.common.HttpResponse
import com.linecorp.armeria.common.HttpStatus
class ArmeriaGlobalExceptionHandler : ExceptionHandlerFunction {
override fun handleException(ctx: ServiceRequestContext, cause: Throwable): HttpResponse? {
return when (cause) {
is CustomException -> HttpResponse.of(HttpStatus.valueOf(cause.status.value()), cause.message)
else -> HttpResponse.of(HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 오류가 발생했습니다.")
}
}
}
적용 예시:
@ExceptionHandler(ArmeriaGlobalExceptionHandler::class)
@Get("/api/hello")
fun hello(): String {
throw CustomException(HttpStatus.BAD_REQUEST, "잘못된 요청입니다.")
}
3. DecoratingHttpServiceFunction으로 모든 요청에 글로벌 적용
Armeria 서버 전체에 글로벌하게 에러 핸들러를 적용하려면 DecoratingHttpServiceFunction
을 사용합니다.
import com.linecorp.armeria.server.DecoratingHttpServiceFunction
import com.linecorp.armeria.server.ServiceRequestContext
import com.linecorp.armeria.common.HttpRequest
import com.linecorp.armeria.common.HttpResponse
class GlobalErrorHandlingDecorator : DecoratingHttpServiceFunction {
override fun serve(ctx: ServiceRequestContext, req: HttpRequest, delegate: HttpService): HttpResponse {
return try {
delegate.serve(ctx, req)
} catch (e: CustomException) {
HttpResponse.of(HttpStatus.valueOf(e.status.value()), e.message)
} catch (e: Exception) {
HttpResponse.of(HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 오류가 발생했습니다.")
}
}
}
적용 예시 (Spring + Armeria 통합 환경):
@Configuration
class ArmeriaServerConfig {
@Bean
fun armeriaServerCustomizer(): ArmeriaServerConfigurator = ArmeriaServerConfigurator { builder ->
builder.decorator(GlobalErrorHandlingDecorator())
}
}
실전 팁 및 권장 패턴
- 에러 응답 포맷을 통일하세요. (예: code, message, detail)
- 커스텀 예외를 정의하여 비즈니스 로직과 에러 처리를 명확히 분리하세요.
- 공통 에러 로깅, 모니터링(예: Sentry, Slack 연동) 코드를 데코레이터에 추가하면 운영에 매우 유용합니다.
- Swagger(OpenAPI) 문서에서도 에러 응답 예시를 명확히 기술하세요.
- Spring과 Armeria를 함께 쓸 때는, 각 프레임워크의 예외 처리 체계가 다르므로 반드시 글로벌 핸들러를 각각 구현해야 합니다.
참고할 만한 레퍼런스
에러 핸들링을 중앙에서 일관성 있게 관리하면 서비스의 신뢰성과 유지보수성이 크게 향상됩니다.
실무에서는 반드시 예외 상황을 꼼꼼히 정의하고, 글로벌 핸들러를 통해 일관된 정책을 적용해보세요!