카테고리 없음

Unity - Unity Service Game Cloud Code (Google Natural API 키 보안강화)

rimugiri 2025. 10. 18. 18:47
728x90
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System.Text;
using System.Text.Json;
using Unity.Services.CloudCode.Apis;
using Unity.Services.CloudCode.Core;


namespace CloudCode.Modules;

// 구글 응답기
internal class GoogleApiRequest
{
    public Document document { get; set; }
    public string encodingType { get; set; }
}

internal class Document
{
    public string type { get; set; }
    public string content { get; set; }
    public string languageCode { get; set; }
}

public class SentimentResponse
{
    public DocumentSentiment documentSentiment { get; set; }
}

public class DocumentSentiment
{
    public float score { get; set; }
    public float magnitude { get; set; }
}

public class ModuleConfig : ICloudCodeSetup
{
    public void Setup(ICloudCodeConfig config)
    {
        config.Dependencies.AddSingleton(GameApiClient.Create());
    }
}

public class SentimentModule
{
    private readonly ILogger<SentimentModule> _logger;

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

    [CloudCodeFunction("AnalyzeSentiment")]
    public async Task<float> AnalyzeSentiment(
            IExecutionContext context,
            IGameApiClient secrets,          
            string text,                    
            string language)                
    {
        var apiKey = await secrets.SecretManager.GetSecret(context, "GOOGLE_API_KEY");

        if(apiKey == null || String.IsNullOrEmpty(apiKey.Value))
        {
            _logger.LogError("Google API key not found in secrets.");
            throw new Exception("Google API key not found in secrets.");
        }

        var apiUrl = $"https://language.googleapis.com/v2/documents:analyzeSentiment?key={apiKey.Value}";

        var requestData = new GoogleApiRequest
        {
            encodingType = "UTF8",
            document = new Document
            {
                type = "PLAIN_TEXT",
                content = text,
                languageCode = language
            }
        };

        var jsonData = JsonSerializer.Serialize(requestData);
        var content = new StringContent(jsonData, Encoding.UTF8, "application/json");

        using var httpClient = new HttpClient();
        var response = await httpClient.PostAsync(apiUrl, content);

        var responseJson = await response.Content.ReadAsStringAsync();

        if (!response.IsSuccessStatusCode)
        {
            _logger.LogError($"Google API request failed {responseJson}");
            throw new Exception($"Google API request failed: {responseJson}");
        }

        _logger.LogInformation("Google API request succeeded.");

        var sentimentResponse = JsonSerializer.Deserialize<SentimentResponse>(responseJson);
        if(sentimentResponse == null)
        {
            _logger.LogError("Failed to deserialize Google API response.");
            throw new Exception("Failed to deserialize Google API response.");
        }

        float sentimentScore = sentimentResponse.documentSentiment.score;

        return sentimentScore;
    }
}

API키는 항상 외부에 드러나면 안되며 클라이언트 로직에서는 이 코드를 알면 안된다 이에따라 API키를 숨기면서 이를 클라에서 절대로 알 수 없게 하는 방법은 서버에 코드를 실행하는 것인데 이를 간단하게 만들어 주는 방법은 Unity Service Game 의 Cloud Code service를 사용하는 것이다

 

이 서비스는 2가지 방식으로 만들 수 있는데

1. c# 방식 : CLI를 사용하거나 유니티 툴 상의 Deploy패키지를 다운 받는다

2. javascript 방식 : Dashboard 내에서 직접 생성이 가능하다

 

이 중에 c#방식의 CLI 방식으로 Cloud Code를 서버에 올려 놓는 방식을 선택하였다 -> 그냥 궁금했음

 

1. UGS 프로젝트 연결

- 유니티 PackageManager에서 Cloud Code패키지를 다운 받는다 -> 이때 주의점은 버전에 따라서 에러가 발생하는 경우가 있을 수 있으니 자신의 유니티 버전에 맞는 것으로 다운그레이드 해야 될 수도 있다

- Edit -> ProjectSetting -> Service 쪽에서 현재 프로젝트를 연동한다

 

자 이제 프로젝트에 UGS 서비스를 사용할 준비는 되어있다

 

2. SecretKey  설정

- Add project secert을 통해 자신의 API를 올려둔다

 

3. Cloud Code 설정

이제 클로드 코드의 C# module쪽으로 보면 아래가 아닌 CLI ~~ 라고 나와 있는데 이 Dashboard에서는 직접 추가가 불가능 하다 (왜 일까??)

 

- cmd 창을 연다

- npm install -g ugs 입력

-  ugs config set project-id <your-project-id> -> project의 setting에 존재한다
 - ugs config set environment-name <your-environment-name> -> project의 environment 의 id가 아닌 name이 맞다

-  ugs login 을 한다 이때 추가적으로 아래 설정을 해 줘야 된다

이 부분을 생성해 줘야된다

이 역할들을 설정해 줘야 되는데 이를 설정해 주지 않으면 오류가 난다

자 이제 login 까지 했으면 작업을 실행할 c# 코드를 작성해 보자

 

4. c# Code

- 클래스 라이브러리를 만들어 주자

- Unity.Services.CloudCode.Apis;
- Unity.Services.CloudCode.Core;

- 위 두 서비스가 필요하다 NGet에서 라이브러리를 다운 받자

- 아래와 같이 코드를 작성해 줬는데 하나하나 주석을 달아서 설명해 두었다

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System.Text;
using System.Text.Json;
using Unity.Services.CloudCode.Apis;
using Unity.Services.CloudCode.Core;


namespace CloudCode.Modules;

/// <summary>
/// 감성 분석을 위한 클래스 구조
/// JsonSerializer를 사용하기위하여 property로 만들어 줘야된다
/// </summary>
internal class GoogleApiRequest
{
    public Document document { get; set; }
    public string encodingType { get; set; }
}

internal class Document
{
    public string type { get; set; }
    public string content { get; set; }
    public string languageCode { get; set; }
}

public class SentimentResponse
{
    public DocumentSentiment documentSentiment { get; set; }
}

public class DocumentSentiment
{
    public float score { get; set; }
    public float magnitude { get; set; }
}

/// <summary>
/// DI를 위한 클래스 리플랙션을 이용해 자동으로 GameApiClient를 주입해준다고 한다
/// </summary>
public class ModuleConfig : ICloudCodeSetup
{
    public void Setup(ICloudCodeConfig config)
    {
        config.Dependencies.AddSingleton(GameApiClient.Create());
    }
}

// 감성분석 모듈
public class SentimentModule
{
    // logs 창에서 로그를 확인하기 위한 로거이다 -> DI주입
    private readonly ILogger<SentimentModule> _logger;

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

    // 아래 모듈이 실제로 실행 시킬수 있는 함수이다
    // IExcutionContext과 IGameApiClient는 DI주입
    // text와 language는 클라이언트에서 전달해주는 파라미터
    [CloudCodeFunction("AnalyzeSentiment")]
    public async Task<float> AnalyzeSentiment(
            IExecutionContext context,
            IGameApiClient secrets,          
            string text,                    
            string language)                
    {
        var apiKey = await secrets.SecretManager.GetSecret(context, "GOOGLE_API_KEY");

        if(apiKey == null || String.IsNullOrEmpty(apiKey.Value))
        {
            _logger.LogError("Google API key not found in secrets.");
            throw new Exception("Google API key not found in secrets.");
        }

        var apiUrl = $"https://language.googleapis.com/v2/documents:analyzeSentiment?key={apiKey.Value}";

        var requestData = new GoogleApiRequest
        {
            encodingType = "UTF8",
            document = new Document
            {
                type = "PLAIN_TEXT",
                content = text,
                languageCode = language
            }
        };

        var jsonData = JsonSerializer.Serialize(requestData);
        var content = new StringContent(jsonData, Encoding.UTF8, "application/json");

        using var httpClient = new HttpClient();
        var response = await httpClient.PostAsync(apiUrl, content);

        var responseJson = await response.Content.ReadAsStringAsync();

        if (!response.IsSuccessStatusCode)
        {
            _logger.LogError($"Google API request failed {responseJson}");
            throw new Exception($"Google API request failed: {responseJson}");
        }

        _logger.LogInformation("Google API request succeeded.");

        var sentimentResponse = JsonSerializer.Deserialize<SentimentResponse>(responseJson);
        if(sentimentResponse == null)
        {
            _logger.LogError("Failed to deserialize Google API response.");
            throw new Exception("Failed to deserialize Google API response.");
        }

        float sentimentScore = sentimentResponse.documentSentiment.score;

        return sentimentScore;
    }
}

- 이렇게 만들었으면 이를 게시해 줘야된다

이런식으로 로컬에 작성해 두고 게시해 준다

- 게시한 폴더에 들어가 보면 아래 코드들이 있는데 ccm은 없을 것이다

- 폴더에 있는 모든 코드를 zip으로 압축한뒤 Language.dll의 이름에 맞춰서 이름을 설정하고 ccm확장자로 변경해 준다

- cmd 창을 다시 들어간 다음 ugs deploy <ccm파일경로> 를 입력한다

- 성공적으로 되었으면 Dashboard에 이가 나타났을거고 오류가 났으면 세팅이 잘못되었거나 role을 설정해주지 않았을 거다

 

5. unity 에서 호출

아래 코드를 분석하면 간단하게 알 수 있을 것이다 return 값이 다른건 내 실제 게임은 CloudCode의 Task<string>으로 return이 되어있기 때문이다

public class SentimentAnalysisManager : MonoBehaviour
{
    public string TestText = "";

    // 예제 테스트용
    async void Start()
    {
        // 씬이 시작될 때 "I love this game!" 텍스트 분석 시도
        await AnalyzeText("I love this game!", "en");

        // 씬이 시작될 때 "이 게임 정말 싫어." 텍스트 분석 시도
        await AnalyzeText(TestText, "ko");
    }


    // 테스트용 UI에서 호출할 수 있도록 public으로 선언
    public async Task AnalyzeText(string textToAnalyze, string languageCode)
    {
        try
        {
            if (UnityServices.State != ServicesInitializationState.Initialized)
            {
                await InitializeAndSignInAsync();
            }

            Debug.Log($"분석 시작 '{textToAnalyze}'");

            var args = new Dictionary<string, object>
            {
                { "text", textToAnalyze },
                { "language", languageCode }
            };

            string responseJson = await CloudCodeService.Instance.CallModuleEndpointAsync<string>(
                "Language",
                "AnalyzeSentiment",
                args
            );

            if (string.IsNullOrEmpty(responseJson))
            {
                Debug.LogWarning("어딘가 이상하니 서버를 확인해 보자");
            }
            else
            {
                Debug.Log($"데이터 얻기 성공");

                SentimentResponse responseData = JsonUtility.FromJson<SentimentResponse>(responseJson);
                Debug.Log($"Sentiment Score: {responseData.documentSentiment.score}, Magnitude: {responseData.documentSentiment.magnitude}");
            }
        }
        catch (Exception e)
        {
            Debug.LogError($"분석 실패: {e.Message}");
        }
    }

    private async Task InitializeAndSignInAsync()
    {
        await UnityServices.InitializeAsync();
        if (!AuthenticationService.Instance.IsSignedIn)
        {
            await AuthenticationService.Instance.SignInAnonymouslyAsync();
            Debug.Log("익명 로그인");
        }
    }
}

 

6. 추가정보

- 비용감소는 문서를 뒤져보면서 파악하자 https://docs.unity.com/ugs/en-us/manual/cloud-code/manual/modules/reference/cost

- 위를 자동화 하고싶다면 CI/CD를 사용하자 https://docs.unity.com/ugs/ko-kr/manual/cloud-code/manual/modules/how-to-guides/automation

- 귀찮으면 AI한테 JavaScript로 짜달라고 하거나 유니티 deploy툴을 활용하자 -> 설명이 잘 되어있다

728x90