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 요청과 응답을 커스텀하게 로깅하면 서비스의 신뢰성과 디버깅 효율이 크게 향상됩니다. 위의 방법을 참고하여 실무에 적용해보세요!