gRpc Context 안전하게 유지하기
Spring Boot 3.4.5, Kotlin, Armeria 1.32.5, gRPC 환경에서 사용자 정보를 context에 담아 service, repository 등 모든 계층에서 접근 가능하게 만드는 방법을 정리합니다. 특히 비동기 처리(Coroutine) 환경에서도 안전하게 유지되도록 구현합니다.
배경
Spring Boot와 gRPC를 함께 사용하는 프로젝트에서, 로그인 유저 정보
, 트래킹 ID
, 요청 헤더 정보
등을 모든 계층에서 사용하고 싶을 때가 많습니다. 이런 정보를 어디에 어떻게 저장하고 꺼내 써야 할까요?
Armeria는 RequestContext
라는 구조를 통해 요청 스레드 안에서 context를 공유할 수 있게 도와주지만, Coroutine처럼 스레드가 바뀌는 환경에서는 별도 처리가 필요합니다. 이 글에서는 그 해결 방법을 다룹니다.
gRPC 요청자의 정보를 Context에 저장하기
먼저 gRPC에서 요청자의 메타데이터(metadata) 정보를 읽어서 Armeria의 RequestContext
에 저장합니다.
import com.linecorp.armeria.common.RequestContext
import com.linecorp.armeria.server.ServiceRequestContext
import io.grpc.*
class GrpcServerInterceptor : ServerInterceptor {
override fun <ReqT : Any?, RespT : Any?> interceptCall(
call: ServerCall<ReqT, RespT>,
headers: Metadata,
next: ServerCallHandler<ReqT, RespT>
): ServerCall.Listener<ReqT> {
val userIdKey = Metadata.Key.of("user-id", Metadata.ASCII_STRING_MARSHALLER)
val userId = headers.get(userIdKey) ?: "anonymous"
val listener = next.startCall(call, headers)
val ctx = ServiceRequestContext.current()
ctx.setAttr(UserContext.USER_ID_KEY, userId)
return listener
}
}
user-id
라는 메타데이터 키로 유저 ID를 추출하여 context에 저장합니다.
Context에 저장된 유저 정보를 조회하는 헬퍼 클래스
import com.linecorp.armeria.common.RequestContext
import com.linecorp.armeria.common.util.AttributeKey
object UserContext {
val USER_ID_KEY: AttributeKey<String> = AttributeKey.valueOf("userId")
fun currentUserId(): String? {
return RequestContext.mapCurrent { it.attr(USER_ID_KEY) }.orElse(null)
}
}
어디서든 UserContext.currentUserId()
를 호출하면 현재 요청자의 ID를 확인할 수 있습니다.
Service / Repository에서 Context 값 사용하기
@Service
class MyService {
fun handleBusinessLogic() {
val userId = UserContext.currentUserId()
println("현재 요청자: $userId")
}
}
비동기 Coroutine 환경에서 Context 유지하기
Armeria의 RequestContext
는 ThreadLocal 기반이므로, 코루틴 전환 시 자동 전파되지 않습니다. 아래와 같이 CoroutineContext
에 붙여주는 별도 클래스가 필요합니다.
import com.linecorp.armeria.common.RequestContext
import kotlinx.coroutines.ThreadContextElement
import kotlin.coroutines.AbstractCoroutineContextElement
import kotlin.coroutines.CoroutineContext
class ArmeriaRequestContextElement(
private val ctx: RequestContext
) : ThreadContextElement<RequestContext>,
AbstractCoroutineContextElement(Key) {
companion object Key : CoroutineContext.Key<ArmeriaRequestContextElement>
private var previous: RequestContext? = null
override fun updateThreadContext(context: CoroutineContext): RequestContext {
previous = RequestContext.mapCurrent { it }.orElse(null)
ctx.makeCurrent()
return ctx
}
override fun restoreThreadContext(context: CoroutineContext, oldState: RequestContext) {
previous?.makeCurrent()
}
}
코루틴 내에서 Context 유지하며 실행하기
import com.linecorp.armeria.server.ServiceRequestContext
import kotlinx.coroutines.withContext
suspend fun <T> withArmeriaContext(block: suspend () -> T): T {
val ctx = ServiceRequestContext.current()
return withContext(ArmeriaRequestContextElement(ctx)) {
block()
}
}
사용 예시:
@Service
class MyAsyncService {
suspend fun asyncBusinessLogic() = withArmeriaContext {
val userId = UserContext.currentUserId()
println("코루틴 내 유저 ID: $userId")
}
}
마무리 정리
Armeria 기반의 Spring Boot + gRPC 프로젝트에서 사용자 정보를 context로 관리하면, 계층 구조와 상관없이 어디서든 필요한 정보를 손쉽게 사용할 수 있습니다. 특히 Coroutine을 사용할 때는
RequestContext
전파를 위한 코루틴 Context 연동 코드가 반드시 필요합니다.