━━━━ ◇ ━━━━
유니티(Unity)/툴 개발

[Unity gPRC 서버 개발(4/4)] gRPC C# 클라이언트 개발

📌 개요

 

참고할 점

  • UI 제작 및 콘텐츠 관련 구현 코드는 다루지 않습니다.
  • 학습용 프로젝트이기 때문에 잘못된 부분이 있을 수 있습니다.

만약 잘못된 부분이 있다면 댓글로 알려주시면 감사하겠습니다.

 

 

 

 

 


 

다시 한번 이번 학습의 목표를 상기하도록 하겠습니다.

이번 학습은 위 아키텍처를 기반으로, 총 4개의 포스팅으로 나눠서 진행되고 있습니다.

궁극적인 목표는 클라우드 환경에서 Unity 클라이언트가 C# gRPC 서버와 통신하여

MySQL과 연동되는 시스템을 구축하는 것입니다.

 

 

 

 

 


이전 포스터에서는 오라클 클라우드 내에서 C# gRPC 서버가 동작하는 부분을 알아보았습니다.

또한, MySQLConnector를 활용하여 DB와 통신하며 테이블 정보를 조회 및 추가하는 방법을 살펴봤어요.

 

 

 

 

 

 

드디어 대망의 마지막 단계로 접어들었습니다.

C# gRPC 클라이언트와 서버가 서로 통신하는 부분을 구현할 차례로,

gRPC 플러그인을 설치하여 Unity 내부에서 gRPC 통신 방법을 살펴볼 것입니다.

이를 통해 게임 클라이언트와 서버가 gRPC를 통해 통신하는 흐름이 완성됩니다.

 

 

 

 

 


📌 C# gRPC 클라이언트 생성 및 구현

 

 

Visual Studio에서 콘솔 앱 프로젝트를 생성합니다.

 

 

 

 

 

 

gRPC Server의 경우 gRPC 템플릿으로 시작하여 초기 세팅이 되어 있는 반면,

gRPC Client의 경우 NuGet 패키지 관리자를 통해 직접 세팅해보도록 하겠습니다.

 

 

 

 

 

 

NuGet 패키지 관리자를 열어 아래 패키지를 설치합니다.

  • Google.Protobuf
  • Grpc.Net.Client
  • Grpc.Tools

 

 

그리고 gRPC 서버에서 구현했던 .proto 파일을 복사하여 Client에도 붙여 넣어 줍니다.

 

※ .proto 파일은 클라이언트와 서버가 통신하기 위한 규약 파일입니다.

 

 

 

proto 파일 코드

더보기
syntax = "proto3";

package mygame.auth;
option csharp_namespace = "MyGame.Grpc.Auth";

service Auth {
	rpc Register (RegisterRequest) returns (RegisterResponse);
	rpc Login (LoginRequest) returns (LoginResponse);
}

message RegisterRequest {
	string user_id	= 1;
	string user_pw	= 2;
	string nickname	= 3;
}

message RegisterResponse {
	bool succese	= 1;
	string message	= 2;
}

message LoginRequest {
	string user_id	= 1;
	string user_pw	= 2;
}

message LoginResponse {
	bool success	= 1;
	string message	= 2;
	string token	= 3;
}

 

 

 

  <TargetFramework>netstandard2.0</TargetFramework>
  
  ...
  
  <ItemGroup>
    <Protobuf Include="Protos\user.proto" GrpcServices="Client" />
  </ItemGroup>

 

그리고 csproj 파일을 열어 GrpcService 속성을 Client로 변경해줍니다.

TargetFramework는 netstandard2.0으로 설정합니다. (Unity 호환성 고려)

이때 아래와 같은 문구가 있다면 제거해주세요.

  • <Nullable>enable</Nullable>
  • <ImplicitUsings>enable</ImplicitUsings>

 

 

 

 

 

 

.proto 파일 속성도 Client only로 설정해줍니다. 이제 이 상태에서 Build 합니다.

 

 

 

 

 

 

그럼 위 사진처럼 파일들을 얻을 수 있습니다.

gRPC 통신은 Unity 엔진에서 C# 스크립트로 구현할 것이기 때문에 프로젝트 dll 파일만 복사합니다.

(프로젝트 dll 파일 = {프로젝트 이름.dll})

 

그리고 Unity Assets Plugins 폴더에 넣어줍니다. (없다면 생성)

 

 

 

 

 

⚠️ 잠깐! gRPC 관련 dll은 왜 Unity에 추가하지 않는건가요?

 

Visual Studio Nuget 관리자로 설치한 최신 버전의 gRPC 패키지는 Unity와 호환되지 않기 때문입니다.

 

문제는 Google.Protobuf.dll 최신 버전이 System.Runtime.Intrinsics 라이브러리에 의존하였으며, 이 라이브러리는 Net 6.0 이상에서만 지원한다는 점입니다. Unity 엔진은 .NET Standard 2.1 또는 .NET Framework을 지원하기에 System.Runtime.Intrinsics 라이브러리를 사용할 수 없었습니다.

 

※ System.Runtime.Intrinsics는 주로 고성능 연산 최적화를 위해 프로세서의 특정 SIMD 명령어를 활용할 때 사용됩니다.

 

주로 Unity는 .NET Standard 2.1을 백엔드 스크립팅 런타임으로 지원합니다. 그래서 gRPC 플러그인은 공식 문서에서 Unity와 호환되는 구버전 패키지를 받아 설치합니다. gRPC의 최신 기능의 이점을 일부 포기하더라도, Unity 환경에서 안정적인 gRPC 통신을 구현하기 위해 이 방법을 선택했습니다.

 

 

 

 

 


📌 Unity gRPC 설정

 

 

https://packages.grpc.io/archive/2022/04/67538122780f8a081c774b66884289335c290cbe-f15a2c1c-582b-4c51-acf2-ab6d711d2c59/index.xml

 

위 링크에 접속하여 grpc_unity_package.2.47.0-dev202204190851.zip 파일을 다운로드 합니다.

 

 

 

 

 

 

그리고 압축을 풀고 Plugins 폴더 안에 존재하는 모든 파일들을 Unity에 추가합니다.

 

 

 

 

 

 

그 후 아까 빌드했던 프로젝트 dll 파일도 추가해줍니다.

이제 Unity에서 gRPC를 사용할 준비가 끝났습니다.

 

 



 


📌 Unity에서 gRPC 응답 및 요청 구현하기

 

using Grpc.Core;
using MyGame.Grpc.Auth;

 

gRPC 클라이언트를 구현하기 위해선 위 2개의 using 문을 필요로 합니다.

  • Grpc.Core : gRPC 프레임워크 (서버간 연결을 설정하고, RPC 호출을 수행)
  • MyGame.Grpc.Auth : .proto 파일을 기반으로 자동 생성된 코드 (.proto 파일에서 선언된 패키지 네임을 따름)

 

 

 

 

 

    public class GrpcAuthService
    {
        private Channel channel;
        private Auth.AuthClient client;
        
        public GrpcAuthService(string ip, int port)
        {
            ConnectToServer($"{ip}:{port}");
        }

        private void ConnectToServer(string address)
        {
            // ChannelCredentials.Insecure는 암호화되지 않은 연결을 의미하며,
            // 테스트 또는 로컬 환경에서 사용됩니다. 실제 서비스에서는 TLS/SSL을 통한
            // 보안 연결을 사용해야 합니다.
            channel = new Channel(address, ChannelCredentials.Insecure);
            client = new Auth.AuthClient(channel);
        }
    }

 

GrpcAuthService.cs 스크립트를 생성합니다. gRPC을 통해 로그인 및 회원가입 기능을 처리합니다.

 

위 코드에서 2개의 클래스를 확인할 수 있습니다.

  • Channel : 서버와의 통신 채널 (이 채널을 통해 모든 RPC 호출이 이루어짐)
  • Auth.AuthClient : .proto 파일을 기반으로 자동 생성된 스텁 클래스
    • .proto 파일에 정의된 service의 모든 RPC 메소드를 보유

 

 

 

 

        public async Task<RegisterResponse> Register(string userId, string userPw, string nickname)
        {
            RegisterRequest request = new RegisterRequest
            {
                UserId = userId,
                UserPw = userPw,
                Nickname = nickname
            };

            RegisterResponse response = await client.RegisterAsync(request);
            return response;
        }

        public async Task<LoginResponse> Login(string userId, string userPw)
        {
            LoginRequest request = new LoginRequest
            {
                UserId = userId,
                UserPw = userPw
            };

            LoginResponse response = await client.LoginAsync(request);
            return response;
        }

 

Unity에서 활용할 수 있도록 gRPC 메소드를 Wrapping한 메소드를 생성했습니다.

client.RegisterAsunc 함수와 client.LoginAsync 함수는 .proto 파일 내부에 정의한 RPC 메소드입니다.

위 코드는 로그인 및 회원가입을 gPRC 요청 및 응답을 간단한 구조로 나타낸 코드입니다.

 

※  보통 백엔드 스크립팅에서는 try-catch 문을 사용하는 것이 좋습니다.

하단에 전체 코드를 정리했으니, try-catch 문을 사용한 방어적 코드도 살펴보시면 좋습니다.

 

 

 

[ gRPC 클라이언트 전체 코드 보기 ]

더보기
using System;
using System.Threading.Tasks;
using Grpc.Core;
using MyGame.Grpc.Auth;
using UnityEngine;

namespace DevelopKit.Network.Grpc
{
    public class GrpcAuthService
    {
        private Channel channel;
        private Auth.AuthClient client;

        public GrpcAuthService(string ip, int port)
        {
            ConnectToServer($"{ip}:{port}");
        }

        private void ConnectToServer(string address)
        {
            channel = new Channel(address, ChannelCredentials.Insecure);
            client = new Auth.AuthClient(channel);
        }

        public async Task<RegisterResponse> Register(string userId, string userPw, string nickname)
        {
            RegisterRequest request = new RegisterRequest
            {
                UserId = userId,
                UserPw = userPw,
                Nickname = nickname
            };

            try
            {
                RegisterResponse response = await client.RegisterAsync(request);
                if (response.Succese)
                {
                    Debug.Log($"[System] Registration successful: {response.Message}");
                }
                else
                {
                    Debug.LogError($"[System] Registration failed: {response.Message}");
                }
                
                return response;
            }
            catch (RpcException e)
            {
                Debug.LogError($"[System] RPC failed: {e.Status.Detail}");
                return null;
            }
            catch (Exception e)
            {
                Debug.LogError($"[System] An error occurred: {e.Message}");
                return null;
            }
        }

        public async Task<LoginResponse> Login(string userId, string userPw)
        {
            LoginRequest request = new LoginRequest
            {
                UserId = userId,
                UserPw = userPw
            };

            try
            {
                LoginResponse response = await client.LoginAsync(request);
                if (response.Success)
                {
                    Debug.Log($"[System] Login successful: {response.Message}");
                }
                else
                {
                    Debug.LogError($"[System] Login failed: {response.Message}");
                }
                
                return response;
            }
            catch (RpcException e)
            {
                Debug.LogError($"[System] RPC failed: {e.Status.Detail}");
                return null;
            }
            catch (Exception e)
            {
                Debug.LogError($"[System] An error occurred: {e.Message}");
                return null;
            }
        }
    }
}

 



 

 


📌 개발 결과 확인

 

 

유저 ID와 비밀번호, 닉네임을 입력하여 회원가입 기능 확인 완료

동일한 ID가 있을 경우 예외 처리 확인 완료

 

 

 

 

 

회원가입 및 로그인 기능 확인 완료

회원가입 성공시 DB에 데이터 정상 추가 확인

 

 

 

 

 

여러 클라이언트가 클라우드 서버에 접속하여 실시간 채팅이 되는 것까지 확인했습니다.

(실시간 채팅은 TCP Socket 방식을 통해 구현)

 

Unity 빌드를 서로 다른 기기에서 테스트를 진행했습니다.

 

 

 

 

 

🎉 프로젝트 개발 후기

 

그동안 진행했던 Unity와 gRPC 기반의 프로젝트가 성공적으로 마무리되었습니다!

 

클라이언트 입장에서 서버를 직접 구현해보고 통신해보는 과정에서 단순히 게임 엔진에서 콘텐츠와 툴을 개발하는 것을 넘어, 순수 개발적으로 시야가 많이 넓어짐을 느꼈습니다. (물론 매우 간단한 DB와 서버를 만든거지만)

 

만약 여러분도 클라이언트 직군에 종사하신다면, 서버 쪽도 직접 구현해보는 것을 추천드립니다.

 

이상으로 Unity + gRPC 프로젝트 블로그 포스팅 시리즈를 마치겠습니다.

 

 

 

 

 

COMMENT