gRPC 요청/응답 커스텀 로깅 구현하기
Spring Boot 3와 Kotlin, 그리고 Armeria 환경에서 gRPC로 들어오는 모든 요청과 응답을 커스텀하게 로깅하는 방법을 단계별로 설명합니다. 실무에서 활용할 수 있는 실질적인 코드와 함께, 초보자도 따라할 수 있도록 쉽게 안내합니다.
들어가며
Spring Boot 3와 Kotlin, 그리고 Armeria를 함께 사용하는 경우, gRPC 기반 서비스에서 모든 요청과 응답을 커스텀하게 로깅하고 싶을 때가 많습니다. gRPC는 HTTP/2 기반의 고성능 통신을 제공하지만, 기본적으로 로깅이 제한적이기 때문에 별도의 인터셉터를 구현해야 합니다. 이 글에서는 gRPC 인터셉터
를 활용해 요청/응답 전체를 로깅하는 방법을 상세히 다룹니다.
1. 왜 커스텀 로깅이 필요한가?
- gRPC는 바이너리 프로토콜을 사용하기 때문에 일반적인 HTTP 로깅 방식으로는 내용을 확인하기 어렵습니다.
- 실시간 모니터링, 디버깅, 보안 감사 등 다양한 목적으로 요청/응답 전체를 기록할 필요가 있습니다.
- Spring Boot + Armeria 환경에서는 별도의 gRPC 인터셉터를 통해 손쉽게 커스텀 로깅을 구현할 수 있습니다.
2. gRPC 인터셉터란?
gRPC 인터셉터는 gRPC 서버로 들어오는 요청(request)과 나가는 응답(response)을 가로채서 별도의 로직(예: 로깅, 인증, 트래픽 제어 등)을 추가할 수 있는 미들웨어입니다. Java/Kotlin에서는 ServerInterceptor
를 구현하여 사용할 수 있습니다.
3. 프로젝트 환경 설정
- Spring Boot 3.x
- Kotlin (JVM)
- Armeria (gRPC 서버 구현)
- Gradle 또는 Maven
예시 Gradle 의존성
dependencies {
implementation("org.springframework.boot:spring-boot-starter")
implementation("com.linecorp.armeria:armeria-spring-boot3-starter:1.25.2")
implementation("com.linecorp.armeria:armeria-grpc:1.25.2")
implementation("io.grpc:grpc-kotlin-stub:1.4.1")
}
4. 커스텀 gRPC 인터셉터 구현하기
아래는 모든 gRPC 요청과 응답을 로깅하는 커스텀 인터셉터의 예시입니다.
import io.grpc.*
import org.slf4j.LoggerFactory
class LoggingGrpcInterceptor : ServerInterceptor {
private val logger = LoggerFactory.getLogger(LoggingGrpcInterceptor::class.java)
override fun <ReqT : Any?, RespT : Any?> interceptCall(
call: ServerCall<ReqT, RespT>,
headers: Metadata,
next: ServerCallHandler<ReqT, RespT>
): ServerCall.Listener<ReqT> {
logger.info("gRPC 요청 - 메서드: {}", call.methodDescriptor.fullMethodName)
logger.info("gRPC 요청 헤더: {}", headers)
val listener = next.startCall(object : ForwardingServerCall.SimpleForwardingServerCall<ReqT, RespT>(call) {
override fun sendMessage(message: RespT) {
logger.info("gRPC 응답: {}", message)
super.sendMessage(message)
}
}, headers)
return object : ForwardingServerCallListener.SimpleForwardingServerCallListener<ReqT>(listener) {
override fun onMessage(message: ReqT) {
logger.info("gRPC 요청 메시지: {}", message)
super.onMessage(message)
}
}
}
}
5. 인터셉터를 Armeria 서버에 등록하기
Armeria의 gRPC 서버에 위에서 만든 인터셉터를 등록해야 모든 gRPC 요청에 대해 로깅이 적용됩니다.
import com.linecorp.armeria.server.grpc.GrpcServiceBuilder
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
class GrpcConfig {
@Bean
fun grpcServiceBuilder(loggingGrpcInterceptor: LoggingGrpcInterceptor): GrpcServiceBuilderCustomizer {
return GrpcServiceBuilderCustomizer { builder ->
builder.intercept(loggingGrpcInterceptor)
}
}
@Bean
fun loggingGrpcInterceptor(): LoggingGrpcInterceptor = LoggingGrpcInterceptor()
}
6. 실제 로그 예시
아래는 위 인터셉터를 적용했을 때 남는 로그의 예시입니다.
INFO [main] c.e.LoggingGrpcInterceptor : gRPC 요청 - 메서드: mypackage.MyService/MyMethod
INFO [main] c.e.LoggingGrpcInterceptor : gRPC 요청 헤더: {user-agent=grpc-java-netty/1.45.1, ...}
INFO [main] c.e.LoggingGrpcInterceptor : gRPC 요청 메시지: MyRequest(id=123, ...)
INFO [main] c.e.LoggingGrpcInterceptor : gRPC 응답: MyResponse(result=OK, ...)
7. 주의사항 및 확장
- 바이너리 데이터(예: 파일 업로드)는 로그에 그대로 남기면 보안 이슈가 생길 수 있습니다. 민감 정보는 마스킹 처리하세요.
- 대량의 트래픽이 발생할 경우, 로그가 쌓이는 속도와 저장소 용량을 반드시 고려해야 합니다.
- 로그 포맷을 JSON 등으로 커스텀하여 로그 수집 시스템(예: ELK, CloudWatch)과 연동할 수 있습니다.
- 필요에 따라 요청/응답의 특정 필드만 로깅하도록 확장할 수 있습니다.
8. 참고 레퍼런스
gRPC 요청과 응답을 커스텀하게 로깅하면 서비스의 신뢰성과 디버깅 효율이 크게 향상됩니다. 위의 방법을 참고하여 실무에 적용해보세요!