.NET의 gRPC 인터셉터

작성자: Ernest Nguyen

인터셉터는 앱이 들어오는 또는 나가는 gRPC 호출과 상호 작용할 수 있도록 하는 gRPC 개념으로, 요청 처리 파이프라인을 보강하는 방법을 제공합니다.

인터셉터는 채널 또는 서비스에 대해 구성되고 각 gRPC 호출에서 자동으로 실행됩니다. 인터셉터는 사용자의 애플리케이션 논리에 투명하므로 로깅, 모니터링, 인증, 유효성 검사와 같은 공통 사례에 적합한 솔루션입니다.

Interceptor 형식

Interceptor 형식에서 상속되는 클래스를 만들어 gRPC 서버 및 클라이언트 모두에 대해 인터셉터를 구현할 수 있습니다.

public class ExampleInterceptor : Interceptor
{
}

기본적으로, Interceptor 기본 클래스는 아무 동작도 하지 않습니다. 인터셉터 구현에서 적절한 기본 클래스 메서드를 재정의하여 인터셉터에 동작을 추가합니다.

클라이언트 인터셉터

gRPC 클라이언트 인터셉터는 나가는 RPC 호출을 가로챌 수 있습니다. 전송된 요청, 들어오는 응답 및 클라이언트 쪽 호출의 컨텍스트에 대한 액세스를 제공합니다.

클라이언트에 대해 재정의할 Interceptor 메서드:

  • BlockingUnaryCall: 단항 RPC의 차단 호출을 가로챕니다.
  • AsyncUnaryCall: 단항 RPC의 비동기 호출을 가로챕니다.
  • AsyncClientStreamingCall: 클라이언트 스트리밍 RPC의 비동기 호출을 가로챕니다.
  • AsyncServerStreamingCall: 서버 스트리밍 RPC의 비동기 호출을 가로챕니다.
  • AsyncDuplexStreamingCall: 양방향 스트리밍 RPC의 비동기 호출을 가로챕니다.

Warning

BlockingUnaryCallAsyncUnaryCall 모두 단항 RPC를 참조하지만 서로 교환하여 사용할 수는 없습니다. 차단 호출은 AsyncUnaryCall에서 가로채지 않고, 비동기 호출은 BlockingUnaryCall에서 가로채지 않습니다.

클라이언트 gRPC 인터셉터 만들기

다음 코드는 단항 호출의 비동기 호출을 가로채는 기본 예제입니다.

public class ClientLoggingInterceptor : Interceptor
{
    private readonly ILogger _logger;

    public ClientLoggingInterceptor(ILoggerFactory loggerFactory)
    {
        _logger = loggerFactory.CreateLogger<ClientLoggingInterceptor>();
    }

    public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
        TRequest request,
        ClientInterceptorContext<TRequest, TResponse> context,
        AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
    {
        _logger.LogInformation("Starting call. Type/Method: {Type} / {Method}",
            context.Method.Type, context.Method.Name);
        return continuation(request, context);
    }
}

AsyncUnaryCall 재정의:

  • 비동기 단항 호출을 가로챕니다.
  • 호출 세부 정보를 로깅합니다.
  • 메서드에 전달된 continuation 매개 변수를 호출합니다. 마지막 인터셉터인 경우 체인의 다음 인터셉터 또는 기본 호출 호출자를 호출합니다.

각 서비스 메서드 종류의 Interceptor에 대한 메서드에는 서로 다른 서명이 있습니다. 그러나 continuationcontext의 개념과 매개 변수는 동일하게 유지됩니다.

  • continuation는 체인의 다음 인터셉터 또는 기본 호출 호출자(체인에 남아 있는 인터셉터가 없는 경우)를 호출하는 대리자입니다. 한 번도 호출하지 않거나 여러 번 호출하는 것은 오류가 아닙니다. 인터셉터는 continuation 대리자에서 반환된 호출 표현(단항 RPC의 경우 AsyncUnaryCall)을 반환할 필요가 없습니다. 대리자 호출을 생략하고 고유한 호출 표현 인스턴스를 반환하면 인터셉터 체인이 끊어지고 연결된 응답이 즉시 반환됩니다.
  • context는 클라이언트 쪽 호출과 연결된 범위가 지정된 값을 전달합니다. context는 보안 주체, 자격 증명 또는 추적 데이터와 같은 메타데이터를 전달하는 데 사용합니다. 또한 context는 최종 기한 및 취소에 대한 정보를 전달합니다. 자세한 내용은 최종 기한 및 취소를 사용하는 안정적인 gRPC 서비스를 참조하세요.

클라이언트 인터셉터에서 응답 대기

인터셉터는 AsyncUnaryCall<TResponse>.ResponseAsync 또는 AsyncClientStreamingCall<TRequest, TResponse>.ResponseAsync 값을 업데이트하여 단항 및 클라이언트 스트리밍 호출에서 응답을 대기할 수 있습니다.

public class ErrorHandlerInterceptor : Interceptor
{
    public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
        TRequest request,
        ClientInterceptorContext<TRequest, TResponse> context,
        AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
    {
        var call = continuation(request, context);

        return new AsyncUnaryCall<TResponse>(
            HandleResponse(call.ResponseAsync),
            call.ResponseHeadersAsync,
            call.GetStatus,
            call.GetTrailers,
            call.Dispose);
    }

    private async Task<TResponse> HandleResponse<TResponse>(Task<TResponse> inner)
    {
        try
        {
            return await inner;
        }
        catch (Exception ex)
        {
            throw new InvalidOperationException("Custom error", ex);
        }
    }
}

앞의 코드가 하는 역할은 다음과 같습니다.

  • AsyncUnaryCall을 재정의하는 새 인터셉터를 만듭니다.
  • 재정의 AsyncUnaryCall:
    • continuation 매개 변수를 호출하여 인터셉터 체인에서 다음 항목을 호출합니다.
    • 연속 작업의 결과에 따라 새 AsyncUnaryCall<TResponse> 인스턴스를 만듭니다.
    • HandleResponse 메서드를 사용하여 ResponseAsync 작업을 래핑합니다.
    • HandleResponse를 사용하여 응답을 기다립니다. 응답을 대기하면 클라이언트가 응답을 받은 후 논리를 추가할 수 있습니다. try-catch 블록에서 응답을 기다리면 호출 오류를 기록할 수 있습니다.

클라이언트 인터셉터를 만드는 방법에 대한 자세한 내용은 grpc/grpc-dotnet GitHub 리포지토리의 ClientLoggerInterceptor.cs 예제를 참조하세요.

클라이언트 인터셉터 구성

gRPC 클라이언트 인터셉터는 채널에서 구성됩니다.

코드는 다음과 같습니다.

  • GrpcChannel.ForAddress를 사용하여 채널을 만듭니다.
  • Intercept 확장 메서드를 사용하여 인터셉터를 사용하도록 채널을 구성합니다. 이 메서드는 CallInvoker를 반환합니다. 강력한 형식의 gRPC 클라이언트는 채널과 마찬가지로 호출자에서 만들 수 있습니다.
  • 호출자에서 클라이언트를 만듭니다. 클라이언트에서 수행한 gRPC 호출은 인터셉터를 자동으로 실행합니다.
using var channel = GrpcChannel.ForAddress("https://localhost:5001");
var invoker = channel.Intercept(new ClientLoggerInterceptor());

var client = new Greeter.GreeterClient(invoker);

Intercept 확장 메서드를 연결하여 하나의 채널에 대해 여러 인터셉터를 구성할 수 있습니다. 또는 여러 인터셉터를 허용하는 Intercept 오버로드가 있습니다. 다음 예제와 같이 단일 gRPC 호출에서 임의 개수의 인터셉터를 실행할 수 있습니다.

var invoker = channel
    .Intercept(new ClientTokenInterceptor())
    .Intercept(new ClientMonitoringInterceptor())
    .Intercept(new ClientLoggerInterceptor());

인터셉터는 연결된 Intercept 확장 메서드의 역순으로 호출됩니다. 위의 코드에서 인터셉터는 다음 순서로 호출됩니다.

  1. ClientLoggerInterceptor
  2. ClientMonitoringInterceptor
  3. ClientTokenInterceptor

gRPC 클라이언트 팩터리에서 인터셉터를 구성하는 방법에 대한 자세한 내용은 .NET의 gRPC 클라이언트 팩터리 통합을 참조하세요.

서버 인터셉터

gRPC 서버 인터셉터는 들어오는 RPC 요청을 가로챕니다. 들어오는 요청, 나가는 응답 및 클라이언트 쪽 호출의 컨텍스트에 대한 액세스를 제공합니다.

서버에 대해 재정의할 Interceptor 메서드:

  • UnaryServerHandler: 단항 RPC를 가로챕니다.
  • ClientStreamingServerHandler: 클라이언트 스트리밍 RPC를 가로챕니다.
  • ServerStreamingServerHandler: 서버 스트리밍 RPC를 가로챕니다.
  • DuplexStreamingServerHandler: 양방향 스트리밍 RPC를 가로챕니다.

서버 gRPC 인터셉터 만들기

다음 코드는 들어오는 단항 RPC를 가로채는 예제입니다.

public class ServerLoggerInterceptor : Interceptor
{
    private readonly ILogger _logger;

    public ServerLoggerInterceptor(ILogger<ServerLoggerInterceptor> logger)
    {
        _logger = logger;
    }

    public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
        TRequest request,
        ServerCallContext context,
        UnaryServerMethod<TRequest, TResponse> continuation)
    {
        _logger.LogInformation("Starting receiving call. Type/Method: {Type} / {Method}",
            MethodType.Unary, context.Method);
        try
        {
            return await continuation(request, context);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, $"Error thrown by {context.Method}.");
            throw;
        }
    }
}

UnaryServerHandler 재정의:

  • 들어오는 단항 호출을 가로챕니다.
  • 호출 세부 정보를 로깅합니다.
  • 메서드에 전달된 continuation 매개 변수를 호출합니다. 마지막 인터셉터인 경우 체인의 다음 인터셉터 또는 서비스 처리기를 호출합니다.
  • 예외를 로깅합니다. 연속을 대기하면 서비스 메서드가 실행된 후 논리를 추가할 수 있습니다. try-catch 블록에서 연속을 기다리면 메서드의 오류를 기록할 수 있습니다.

클라이언트 및 서버 인터셉터 메서드 모두 서명은 다음과 비슷합니다.

  • continuation는 체인의 다음 인터셉터 또는 서비스 처리기(체인에 남아 있는 인터셉터가 남아 없는 경우)를 호출하는 들어오는 RPC의 대리자를 나타냅니다. 클라이언트 인터셉터와 마찬가지로 언제든지 호출할 수 있으며 연속 대리자에서 직접 응답을 반환할 필요가 없습니다. 연속을 대기하여 서비스 처리기가 실행된 후에 아웃바운드 논리를 추가할 수 있습니다.
  • context는 요청 메타데이터, 최종 기한 및 취소 또는 RPC 결과와 같은 서버 쪽 호출과 연결된 메타데이터를 전달합니다.

서버 인터셉터를 만드는 방법에 대한 자세한 내용은 grpc/grpc-dotnet GitHub 리포지토리의 ServerLoggerInterceptor.cs 예제를 참조하세요.

서버 인터셉터 구성

gRPC 서버 인터셉터는 시작할 때 구성됩니다. 코드는 다음과 같습니다.

  • AddGrpc를 사용하여 앱에 gRPC를 추가합니다.
  • 서비스 옵션의 Interceptors 컬렉션에 추가하여 모든 서비스에 대해 ServerLoggerInterceptor를 구성합니다.
public void ConfigureServices(IServiceCollection services)
{
    services.AddGrpc(options =>
    {
        options.Interceptors.Add<ServerLoggerInterceptor>();
    });
}

AddServiceOptions를 사용하고 서비스 유형을 지정하여 특정 서비스에 대해 인터셉터를 구성할 수도 있습니다.

public void ConfigureServices(IServiceCollection services)
{
    services
        .AddGrpc()
        .AddServiceOptions<GreeterService>(options =>
        {
            options.Interceptors.Add<ServerLoggerInterceptor>();
        });
}

인터셉터는 InterceptorCollection에 추가된 순서대로 실행됩니다. 전역 및 단일 서비스 인터셉터가 모두 구성된 경우 전역적으로 구성된 인터셉터가 단일 서비스에 대해 구성된 인터셉터보다 먼저 실행됩니다.

기본적으로 gRPC 서버 인터셉터에는 요청당 수명이 있습니다. 인터셉터 형식을 종속성 주입에 등록하면 이 동작을 재정의할 수 있습니다. 다음 예제에서는 ServerLoggerInterceptor를 싱글톤 수명으로 등록합니다.

public void ConfigureServices(IServiceCollection services)
{
    services.AddGrpc(options =>
    {
        options.Interceptors.Add<ServerLoggerInterceptor>();
    });

    services.AddSingleton<ServerLoggerInterceptor>();
}

gRPC 인터셉터와 미들웨어

ASP.NET Core 미들웨어는 C Core 기반 gRPC 앱의 인터셉터와 유사한 기능을 제공합니다. ASP.NET Core 미들웨어와 인터셉터는 개념적으로 유사합니다. 모두:

  • gRPC 요청을 처리하는 파이프라인을 생성하는 데 사용됩니다.
  • 파이프라인의 다음 구성 요소 이전이나 이후에 작업을 수행할 수 있습니다.
  • 다음 액세스 권한 HttpContext제공:
    • 미들웨어에서 HttpContext는 매개 변수입니다.
    • 인터셉터에서는 ServerCallContext.GetHttpContext 확장 메서드와 함께 ServerCallContext 매개 변수를 사용하여 HttpContext에 액세스할 수 있습니다. 이 기능은 ASP.NET Core에서 실행되는 인터셉터와 관련이 있습니다.

ASP.NET Core 미들웨어와 gRPC 인터셉터의 차이점:

  • 인터셉터:
    • 를 사용하여 추상화의 gRPC 계층에서 작동합니다 ServerCallContext.
    • 다음 액세스 권한을 제공합니다.
      • 호출에 전송되는 역직렬화된 메시지
      • 호출에서 반환된 직렬화되기 전의 메시지
    • gRPC 서비스에서 throw된 예외를 catch하고 처리할 수 있습니다.
  • 미들웨어:
    • 모든 HTTP 요청에 대해 실행됩니다.
    • gRPC 인터셉터 이전에 실행됩니다.
    • 기본 HTTP/2 메시지에서 작동합니다.
    • 요청 및 응답 스트림의 바이트에만 액세스할 수 있습니다.

추가 리소스