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 연동 코드가 반드시 필요합니다.


📌 참고자료