MagicOnion 은 .proto 대신 C# 인터페이스를 스키마로 쓴다
Esc

    MagicOnion 은 .proto 대신 C# 인터페이스를 스키마로 쓴다

    –C#라이브러리네트워크
    public interface IMathService : IService<IMathService>
    {
        UnaryResult<int> SumAsync(int x, int y);
    }

    C# 서버와 C# 클라이언트가 gRPC 로 이야기하려면 보통 .proto 파일을 쓰고 protoc 로 양쪽 코드를 만듭니다. 같은 언어끼리인데 스키마 언어와 생성 단계를 하나 더 거치는 셈입니다.

    MagicOnion 은 서버와 클라이언트가 같이 참조하는 C# 인터페이스를 스키마로 쓰고, 전송만 gRPC 에 맡깁니다. 위 인터페이스가 .proto 의 자리입니다. 기준 판본은 7.11.0 입니다.

    IDL 이 하던 일을 인터페이스에서 읽는다

    .proto 는 서비스 이름, 메서드 이름, 요청과 응답 메시지를 정합니다. MagicOnion 은 셋을 리플렉션으로 인터페이스에서 뽑습니다. 서비스 이름은 인터페이스의 짧은 이름, 메서드 이름은 메서드 이름, 요청은 인자 목록, 응답은 UnaryResult<T> 의 T 입니다.

    그래서 SumAsync 의 gRPC 경로는 /IMathService/SumAsync 입니다. 인터페이스 이름과 메서드 이름이 그대로 와이어에 실립니다.

    계약은 양쪽에 똑같이 컴파일된 소스가 지킨다

    .proto 가 없으면 “양쪽이 같은 계약을 본다” 는 보장을 누가 할까요. gRPC 메서드 객체와 마샬러를 만드는 GrpcMethodHelper 가 든 MagicOnion.Internal 은 패키지로 나가지 않고, 서버 DLL 과 클라이언트 DLL 에 소스째 들어갑니다. 서버의 바인더도 클라이언트의 호출기도 같은 함수로 메서드와 마샬러를 만듭니다.

    두 DLL 의 GrpcMethodHelper 는 이름은 같아도 서로 다른 internal 타입이라 부딪히지 않습니다. 공개 API 를 늘리지 않고 와이어 규칙을 한 소스에서 나오게 한 것입니다. 대신 C# 전용입니다. 다른 언어가 붙으려면 MessagePack 바이트와 이름 규칙을 손으로 맞춰야 합니다.

    전송은 grpc-dotnet 에 통째로 맡긴다

    서버는 ASP.NET Core 의 한 기능입니다. AddMagicOnion() 이 안에서 AddGrpc() 를 부르고, grpc-dotnet 이 서비스의 메서드 목록을 묻는 확장점에 MagicOnion 의 공급자를 끼웁니다. Kestrel, 인증, 미들웨어, HTTP/2 멀티플렉싱이 그대로 따라옵니다.

    치른 값은 HTTP/2 가 필수라는 것입니다. 7.0 에서 Unity 용 C-core gRPC 지원을 걷어냈고, Unity 에서는 YetAnotherHttpHandler 를 GrpcChannelOptions.HttpHandler 로 꽂아 HTTP/2 를 씁니다.

    호출 방식은 둘이고, 인프라에는 다르게 보인다

    Unary StreamingHub
    gRPC 에서 무엇인가 메서드마다 Unary 하나 허브마다 DuplexStreaming 하나 (Connect)
    인터페이스 IService<T>, 반환 UnaryResult<T> IStreamingHub<THub, TReceiver>
    서버가 먼저 보내기 없음 TReceiver 호출
    인프라에서 보이는 것 메서드마다 HTTP POST 한 번 긴 요청 하나

    허브에 메서드가 몇 개든 gRPC 라우트는 하나입니다. 엔드포인트를 열거하면 이렇게 나옵니다.

    /IMathService/SumAsync
    /IChatHub/Connect          ← JoinAsync · SendAsync · EchoAsync 가 있지만 라우트는 하나

    허브 메서드는 그 스트림 안에서 메서드 이름의 해시(FNV-1a 32비트)로 구분합니다. 로드 밸런서와 액세스 로그가 보는 것은 /IChatHub/Connect 하나뿐입니다. (Windows 11, .NET SDK 10.0.401, net10.0, 한 프로세스의 Kestrel HTTP/2 서버와 클라이언트.)

    주의할 점

    1. 배포한 인터페이스와 메서드의 이름을 바꾸지 않는다

    안 지키면 생기는 문제. Unary 라우트는 /{인터페이스 짧은 이름}/{메서드 이름} 이고 허브 메서드 ID 는 메서드 이름의 해시입니다. 리팩터링 도구로 이름을 바꾸면 양쪽 코드가 같이 바뀌어 컴파일은 되지만, 이미 배포된 옛 클라이언트는 Unimplemented 를 받거나 허브에서 없는 메서드 ID 오류를 받습니다.

    이렇게 쓴다. 배포한 이름은 계약으로 봅니다. 바꿔야 하면 공유 인터페이스에 [ServiceName("옛이름")], 허브 메서드면 [MethodId(옛값)] 을 붙여 와이어의 이름을 고정합니다. 한쪽에만 붙이면 경로가 어긋납니다.

    2. 같은 짧은 이름의 인터페이스 둘을 한 서버에 올리지 않는다

    안 지키면 생기는 문제. 네임스페이스는 경로에 들어가지 않습니다. A.IMyService 와 B.IMyService 는 둘 다 /IMyService/... 가 되어 경로가 겹칩니다.

    이렇게 쓴다. 인터페이스 이름을 다르게 짓거나 한쪽에 [ServiceName("B.IMyService")] 처럼 겹치지 않는 이름을 줍니다.

    3. 허브 메서드 단위의 부하 분산과 로그를 기대하지 않는다

    안 지키면 생기는 문제. StreamingHub 의 호출은 전부 Connect 스트림 안의 메시지입니다. 로드 밸런서로 메서드별 라우팅을 하거나 액세스 로그로 메서드별 호출 수를 보려 하면 아무것도 나오지 않습니다.

    이렇게 쓴다. 서버가 먼저 보내야 하거나 잦은 양방향 교환이 필요할 때만 허브를 쓰고, 나머지는 Unary 로 둡니다. 공식 문서도 Unary 를 기본으로 권합니다.

    정리

    MagicOnion 은 .proto 자리에 공유 C# 인터페이스를 두고 전송은 gRPC 에 맡긴 RPC 프레임워크입니다.

    • 서비스 이름, 메서드 이름, 요청과 응답 타입을 인터페이스에서 리플렉션으로 읽습니다.
    • 와이어 규칙은 서버와 클라이언트에 소스째 컴파일되는 MagicOnion.Internal 한 벌이 지킵니다. 그래서 C# 전용이고 이름이 곧 계약입니다.
    • Unary 는 메서드마다 gRPC Unary 하나, StreamingHub 는 허브마다 Connect 스트림 하나입니다.