
지난번에는 클라이언트의 패킷을 해석하여 서버의 심장부인 PlayerAgent까지 전달하는 완전한 파이프라인을 구축했습니다. 이제 여러분의 서버는 하나의 완결된 유기체처럼 동작할 수 있습니다.
하지만 거대한 MMORPG 월드는 서버 한 대로 감당할 수 없습니다. 수많은 서버가 각자의 역할을 수행하며 서로 '대화'해야 하죠. 이번 주에는 바로 이 서버 간 통신(Inter-Server Communication) 이라는 새로운 세계로 나아갑니다.
안녕하세요! 스튜디오 비를긋다입니다. 이번 챌린지의 주제는 'gRPC를 이용한 분산 마이크로서비스 아키텍처 구축' 입니다. 기존의 단일(Monolithic) 서버 구조에서 벗어나, 기능별로 분리된 여러 서버가 어떻게 효율적으로 데이터를 주고받는지, 현대적인 RPC(Remote Procedure Call) 프레임워크인 gRPC를 통해 직접 구현해 보겠습니다.
C# 개발자라면 꼭 알아야 할 gRPC 마이크로서비스 구축 실전 예제입니다. 단일 서버를 게임 서버와 채팅 서버로 분리하는 방법, .proto 정의, 양방향 스트리밍 구현, 서비스 디스커버리 개념까지 C# 코드로 직접 확인하세요.
'아르카디아의 그림자'의 인기가 폭발적으로 증가하면서, 모든 기능을 한 서버에서 처리하는 현재 구조가 한계에 부딪혔습니다. 특히 채팅 메시지 트래픽이 게임 로직 처리에 영향을 주기 시작했습니다. 이에 따라, 채팅 기능만을 전담하는 별도의 '채팅 서버'를 분리하기로 결정했습니다.
당신의 임무는 기존 '게임 서버'와 새로운 '채팅 서버'를 gRPC로 연결하는 것입니다. 플레이어가 게임 서버에 접속해 채팅을 하면, 해당 메시지는 게임 서버를 거쳐 채팅 서버로 전달되어야 합니다. 채팅 서버는 이 메시지를 현재 접속 중인 모든 게임 서버들에게 다시 전파(Broadcast)하고, 각 게임 서버는 메시지를 소속 플레이어들에게 최종적으로 전달해야 합니다.
ChatMessage와 ChatService의 API를 정의해야 합니다.ChatServer 프로젝트에 Kestrel을 사용하여 gRPC 서비스를 호스팅하고, 여러 게임 서버로부터의 스트리밍 연결을 관리하며 메시지를 중계하는 로직을 구현해야 합니다.GameServer 프로젝트에 gRPC 클라이언트를 구현해야 합니다. 서버 시작 시 채팅 서버에 접속하여 양방향 스트림을 열고, 채팅 메시지를 보내고 받는 기능을 구현해야 합니다.이 솔루션은 두 개의 프로젝트로 구성됩니다. gRPC를 사용하기 위해서는 Grpc.AspNetCore, Google.Protobuf, Grpc.Tools 등의 NuGet 패키지가 필요합니다.
.proto 파일 (프로젝트 공용)Protos/chat.proto 파일을 만들고 두 프로젝트에 모두 링크합니다.
syntax = "proto3";
package chat;
// 채팅 서비스 정의
service ChatService {
  // 양방향 스트리밍 RPC. 게임 서버와 채팅 서버는 이 스트림을 통해 계속 메시지를 주고받는다.
  rpc StartChat (stream ChatMessage) returns (stream ChatMessage);
}
// 채팅 메시지 구조
message ChatMessage {
  uint64 player_id = 1;
  string player_name = 2;
  string content = 3;
}
using Grpc.Core;
using System.Collections.Concurrent;
// ChatService의 실제 구현
public class ChatServiceImpl : ChatService.ChatServiceBase
{
    // 연결된 모든 게임 서버의 응답 스트림을 저장. Key: 고유 ID, Value: 스트림 객체
    private static readonly ConcurrentDictionary<string, IServerStreamWriter<ChatMessage>> _gameServers = new();
    // 양방향 스트리밍 RPC 메서드 구현
    public override async Task StartChat(
        IAsyncStreamReader<ChatMessage> requestStream, 
        IServerStreamWriter<ChatMessage> responseStream, 
        ServerCallContext context)
    {
        var connectionId = context.Peer; // 연결된 피어(게임 서버)의 고유 주소
        Console.WriteLine($"GameServer connected: {connectionId}");
        _gameServers[connectionId] = responseStream;
        try
        {
            // 클라이언트(게임 서버)로부터 메시지가 올 때까지 비동기적으로 대기
            await foreach (var message in requestStream.ReadAllAsync())
            {
                Console.WriteLine($"Received from {connectionId}: {message.Content}");
                // 받은 메시지를 '모든' 게임 서버에게 다시 전파 (Broadcast)
                foreach (var server in _gameServers.Values)
                {
                    await server.WriteAsync(message);
                }
            }
        }
        catch (IOException) { /* 클라이언트 연결 끊김 */ }
        finally
        {
            // 연결이 끊기면 목록에서 제거
            _gameServers.TryRemove(connectionId, out _);
            Console.WriteLine($"GameServer disconnected: {connectionId}");
        }
    }
}
// ChatServer의 Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddGrpc(); // gRPC 서비스 추가
var app = builder.Build();
app.MapGrpcService<ChatServiceImpl>(); // ChatService 구현체 매핑
app.Run(); // Kestrel 서버 실행
using Grpc.Net.Client;
using Grpc.Core;
// GameServer의 채팅 관련 로직을 담당하는 클래스
public class ChatClientService
{
    private readonly GrpcChannel _channel;
    private readonly ChatService.ChatServiceClient _client;
    private AsyncDuplexStreamingCall<ChatMessage, ChatMessage>? _chatStream;
    public ChatClientService(string chatServerAddress)
    {
        _channel = GrpcChannel.ForAddress(chatServerAddress);
        _client = new ChatService.ChatServiceClient(_channel);
    }
    public async Task ConnectAsync()
    {
        _chatStream = _client.StartChat();
        Console.WriteLine("Connected to ChatServer.");
        // 채팅 서버로부터 메시지를 수신하는 별도의 Task 실행
        _ = Task.Run(async () =>
        {
            await foreach (var message in _chatStream.ResponseStream.ReadAllAsync())
            {
                // TODO: 이 게임 서버에 접속한 플레이어들에게 메시지를 전달하는 로직
                // 예: var agent = PlayerManager.GetAgent(message.PlayerId);
                //     agent?.SendToClient(message);
                Console.WriteLine($"[Broadcast] {message.PlayerName}: {message.Content}");
            }
        });
    }
    // 플레이어가 보낸 채팅 메시지를 채팅 서버로 전송
    public async Task SendMessageAsync(ChatMessage message)
    {
        if (_chatStream != null)
        {
            await _chatStream.RequestStream.WriteAsync(message);
        }
    }
}
// GameServer의 Program.cs (지난주 코드와 통합)
// ... PacketHandler 등록 로직 ...
PacketHandler.Register<ChatRequestPacket>(PacketId.C_Chat, async (session, req) => 
{
    // chatClientService는 싱글턴으로 관리되어야 함
    await chatClientService.SendMessageAsync(new ChatMessage
    {
        PlayerId = session.PlayerId,
        PlayerName = "PlayerNameHere", // DB 등에서 조회
        Content = req.Message
    });
});
// ... 서버 시작 로직 ...
var chatClientService = new ChatClientService("http://localhost:5000"); // ChatServer 주소
await chatClientService.ConnectAsync();
// ... GameServer 시작 ...
이 분산 아키텍처는 MMORPG 서버를 수평적으로 확장(Scale-out)하기 위한 핵심 패턴입니다.
gRPC를 사용하면 각 서버가 서로 다른 프로그래밍 언어로 작성되어도(예: 게임 로직은 C#, AI는 Python) 프로토콜 버퍼를 통해 원활하게 통신할 수 있다는 강력한 장점도 있습니다.
.proto 파일을 통한 API 우선 설계 방식을 학습합니다..proto 파일이 서비스의 의도를 잘 표현하고 있는가?현재 샘플 코드는 게임 서버가 채팅 서버의 주소(http://localhost:5000)를 하드코딩하여 알고 있습니다. 이는 유연성이 떨어지는 방식입니다. 이 문제를 해결해 보세요.
과제: 서비스 디스커버리(Service Discovery) 기본 패턴 구현 🗺️
ChatServer가 시작될 때, 자신의 IP 주소와 포트 번호를 'chat-service-address'와 같은 키로 서비스 레지스트리에 등록(쓰기)하도록 수정합니다.GameServer가 시작될 때, 하드코딩된 주소 대신 서비스 레지스트리에 질의하여 'chat-service-address' 키의 값을 읽어와 채팅 서버에 접속하도록 수정합니다.이 패턴을 적용하면, 채팅 서버를 다른 머신에서 실행하거나 포트를 변경해도 게임 서버의 코드를 전혀 수정할 필요가 없어져, 실제 분산 환경 운영에 필수적인 유연성을 확보하게 됩니다.
이번 챌린지를 통해 여러분은 비로소 '대규모' 서버 시스템을 설계하는 개발자의 시야를 갖게 될 것입니다. 두 개의 서버를 직접 실행하고 서로 통신하는 모습을 지켜보는 것은 매우 흥미로운 경험이 될 겁니다!
gRPC
gRPC는 Google이 개발하고 현재 Cloud Native Computing Foundation(CNCF)에서 관리하는 고성능 오픈소스 원격 프로시저 호출(Remote Procedure Call, RPC) 프레임워크이다.주요 개념gRPC는 클라이언트가 다른 머신의
blog.rainshelter.net
프로토콜 버퍼(Protocol Buffers, Protobuf)
프로토콜 버퍼(Protocol Buffers, 줄여서 Protobuf)는 구글이 개발한 데이터 직렬화(serialization) 프레임워크로, 서로 다른 시스템 간 효율적이고 안정적인 데이터 교환을 위해 사용된다.핵심 개념프로
blog.rainshelter.net
| 8.주간 C# 서버 챌린지: 유닛 테스트 및 통합 테스트 (1) | 2025.11.01 | 
|---|---|
| 6.주간 C# 서버 챌린지: MemoryPack으로 패킷 처리 시스템 구축하기 (0) | 2025.10.11 | 
| 5.주간 C# 서버 챌린지: MMORPG 서버 개발의 핵심, C# 비동기 소켓 프로그래밍 (0) | 2025.10.02 | 
| 4.주간 C# 서버 챌린지: 비동기 I/O와 리포지토리 패턴으로 플레이어 데이터 저장하기 (0) | 2025.09.24 | 
| 3.주간 C# 서버 챌린지: '락(Lock)' 없는 플레이어 상태 관리 (0) | 2025.09.17 |