카테고리 없음

asp.net core를 활용하여 모바일 게임 만들기 (7일차)

susot 2026. 8. 2. 21:36

전에는 ServerConnectionTester에서 요청을 처리하는 코드를 제작했었습니다. 하지만 다양한 기능을 추가하면 UnityWebRequest 코드가 반복되는 문제가 있습니다. 따라서 이를 해결하기 위해 공통된 네트워크 처리를 해줄 ApiClient를 만들어보겠습니다.

 

namespace Work.LCM._01.Scripts.Network
{
    public class ApiResult<T>
    {
        public bool IsSuccess { get; }
        public long StatusCode { get; }
        public T Data { get; }
        public string ErrorCode { get; }
        public string ErrorMessage { get; }

        private ApiResult(
            bool isSuccess,
            long statusCode,
            T data,
            string errorCode,
            string errorMessage)
        {
            IsSuccess = isSuccess;
            StatusCode = statusCode;
            Data = data;
            ErrorCode = errorCode;
            ErrorMessage = errorMessage;
        }

        public static ApiResult<T> Success(
            long statusCode,
            T data)
        {
            return new ApiResult<T>(
                true,
                statusCode,
                data,
                string.Empty,
                string.Empty);
        }

        public static ApiResult<T> Failure(
            long statusCode,
            string errorCode,
            string errorMessage)
        {
            return new ApiResult<T>(
                false,
                statusCode,
                default(T),
                errorCode,
                errorMessage);
        }
    }
}

 

ApiResult는 Unity가 Api를 호출하면 성공 데이터만 받는게 아니라

  • 요청 성공 여부
  • HTTP 상태 코드
  • 정상 응답 데이터
  • 서버 오류 코드
  • 서버 오류 메시지

등을 같이 받는데 이 정보를 하나로 묶기 위한 코드가 ApiResult 코드입니다. T에는 정상 응답 DTO가 들어갑니다.

 

또한 서버한테 요청을 할때 오류가 나면 asp.net core에서 오류를 보내주는데 이를 받기 위한 DTO도 만들도록 하겠습니다.

using System;

namespace Work.LCM._01.Scripts.Network
{
    [Serializable]
    public class ApiErrorResponse
    {
        public string code;
        public string message;
        public string title;
    }
}

 

이제 유니티에서 플레이어 생성 요청을 보내면 서버가 이를 처리하는걸 만들어보겠습니다.

일단 이전에 만들었던 서버는 플레이어 생성을 위해 닉네임을 데이터로 받았습니다. 이를 위해 RequestDTO도 닉네임을 가진 DTO로 만들어주겠습니다.

 

using System;

namespace Work.LCM._01.Scripts.Network
{
    [Serializable]
    public class CreatePlayerRequest
    {
        public string nickname;
    }
}

 

그리고 이제 플레이어가 생성되었다면 서버에서 클라이언트에게 생성된 플레이어의 ID,닉네임,레벨을 전달해주기 때문에 이를 받을 ResponseDTO도 만들어주겠습니다.

 

using System;

namespace Work.LCM._01.Scripts.Network
{
    [Serializable]
    public class PlayerSummaryResponse
    {
        public int playerId;
        public string nickname;
        public int level;
    }
}

 

자 이제 서버에게 플레이어 생성 요청을 보낼 DTO, 생성된 플레이어 정보를 받을 DTO, 오류 메세지를 받을 DTO까지 전부 만들어졌으니 ApiClient 코드를 작성해보겠습니다.

 

using System;
using System.Collections;
using System.Text;
using UnityEngine;
using UnityEngine.Networking;

namespace Work.LCM._01.Scripts.Network
{
    public class ApiClient : MonoBehaviour
    {
        [SerializeField] private string serverBaseUrl =
            "http://localhost:5098";

        public IEnumerator Get<TResponse>(
            string path,
            Action<ApiResult<TResponse>> onCompleted)
        {
            string url = BuildUrl(path);

            using (UnityWebRequest request = UnityWebRequest.Get(url))
            {
                yield return SendRequest(request, onCompleted);
            }
        }

        public IEnumerator Post<TRequest, TResponse>(
            string path,
            TRequest body,
            Action<ApiResult<TResponse>> onCompleted)
        {
            string url = BuildUrl(path);
            string json = JsonUtility.ToJson(body);
            byte[] bodyBytes = Encoding.UTF8.GetBytes(json);

            using (UnityWebRequest request =
                   new UnityWebRequest(
                       url,
                       UnityWebRequest.kHttpVerbPOST))
            {
                request.uploadHandler =
                    new UploadHandlerRaw(bodyBytes);

                request.downloadHandler =
                    new DownloadHandlerBuffer();

                request.SetRequestHeader(
                    "Content-Type",
                    "application/json");

                yield return SendRequest(request, onCompleted);
            }
        }

        private IEnumerator SendRequest<TResponse>(
            UnityWebRequest request,
            Action<ApiResult<TResponse>> onCompleted)
        {
            yield return request.SendWebRequest();

            if (request.result == UnityWebRequest.Result.Success)
            {
                string json = request.downloadHandler.text;

                if (string.IsNullOrWhiteSpace(json))
                {
                    onCompleted?.Invoke(
                        ApiResult<TResponse>.Success(
                            request.responseCode,
                            default(TResponse)));

                    yield break;
                }

                try
                {
                    TResponse data =
                        JsonUtility.FromJson<TResponse>(json);

                    onCompleted?.Invoke(
                        ApiResult<TResponse>.Success(
                            request.responseCode,
                            data));
                }
                catch (Exception exception)
                {
                    onCompleted?.Invoke(
                        ApiResult<TResponse>.Failure(
                            request.responseCode,
                            "RESPONSE_DESERIALIZATION_FAILED",
                            $"서버 응답 변환에 실패했습니다. {exception.Message}"));
                }

                yield break;
            }

            onCompleted?.Invoke(
                CreateFailureResult<TResponse>(request));
        }

        private static ApiResult<TResponse>
            CreateFailureResult<TResponse>(
                UnityWebRequest request)
        {
            string errorCode = request.result.ToString();
            string errorMessage = request.error;
            string responseBody =
                request.downloadHandler?.text ?? string.Empty;

            if (request.result ==
                UnityWebRequest.Result.ProtocolError)
            {
                ApplyServerError(
                    responseBody,
                    ref errorCode,
                    ref errorMessage);
            }

            return ApiResult<TResponse>.Failure(
                request.responseCode,
                errorCode,
                errorMessage);
        }

        private static void ApplyServerError(
            string responseBody,
            ref string errorCode,
            ref string errorMessage)
        {
            if (string.IsNullOrWhiteSpace(responseBody))
            {
                return;
            }

            try
            {
                ApiErrorResponse error =
                    JsonUtility.FromJson<ApiErrorResponse>(
                        responseBody);

                if (!string.IsNullOrWhiteSpace(error.code))
                {
                    errorCode = error.code;
                }

                if (!string.IsNullOrWhiteSpace(error.message))
                {
                    errorMessage = error.message;
                    return;
                }

                if (!string.IsNullOrWhiteSpace(error.title))
                {
                    errorMessage = error.title;
                    return;
                }
            }
            catch (Exception)
            {
            }

            errorMessage = responseBody;
        }

        private string BuildUrl(string path)
        {
            string baseUrl = serverBaseUrl.TrimEnd('/');
            string relativePath =
                path.StartsWith("/") ? path : $"/{path}";

            return $"{baseUrl}{relativePath}";
        }
    }
}

 

코드를 보면 중요한 5가지 파트로 이루어져있습니다.

  • BuildUrl()
  • Get()
  • Post()
  • SendRequest()
  • CreateFailureResult(), ApplyServerError()

BuildUrl()은 서버 기본 주소와 API 주소를 합쳐주는 역할입니다.

 

string baseUrl = serverBaseUrl.TrimEnd('/');

 

만약 주소 마지막에 http://localhost:5098/ 처럼 입력되면 마지막 /가 두개가 되기 때문에 이를 지워주는 코드입니다.

 

string relativePath =
    path.StartsWith("/") ? path : $"/{path}";

호출하는 쪽에서

  • /api/players
  • api/players

둘 다 제대로 경로가 입력되도록 하는 코드입니다.

 

Get()은 서버 데이터를 조회할 때 사용됩니다.

public IEnumerator Get<TResponse>(
    string path,
    Action<ApiResult<TResponse>> onCompleted)
{
    string url = BuildUrl(path);

    using (UnityWebRequest request = UnityWebRequest.Get(url))
    {
        yield return SendRequest(request, onCompleted);
    }
}

사용 예시는 아래 코드처럼 씁니다.

 

StartCoroutine(
    apiClient.Get<ServerStatusResponse>(
        "/api/server/ping",
        HandlePingResult));

매개변수는 각각 TResponse는 성공 응답을 변환할 DTO이고, path는 호출할 API 경로, onCompleted는 요청 완료 후 실행할 메서드입니다.

 

Get()에서 실제 서버에게 요청을 하는게 아닌 요청을 위한 준비만 하고 실제 요청은 SendRequest()에서 합니다.

 

 

Post()는 유니티에서 서버에게 데이터를 보낼 때 사용합니다.

public IEnumerator Post<TRequest, TResponse>(
    string path,
    TRequest body,
    Action<ApiResult<TResponse>> onCompleted)
{
    string url = BuildUrl(path);
    string json = JsonUtility.ToJson(body);
    byte[] bodyBytes = Encoding.UTF8.GetBytes(json);

    using (UnityWebRequest request =
           new UnityWebRequest(
               url,
               UnityWebRequest.kHttpVerbPOST))
    {
        request.uploadHandler =
            new UploadHandlerRaw(bodyBytes);

        request.downloadHandler =
            new DownloadHandlerBuffer();

        request.SetRequestHeader(
            "Content-Type",
            "application/json");

        yield return SendRequest(request, onCompleted);
    }
}

 

사용하는 코드는 아래처럼 사용합니다.

StartCoroutine(
    apiClient.Post<
        CreatePlayerRequest,
        PlayerSummaryResponse>(
            "/api/players",
            request,
            HandleCreateResult));

 

TRequest는 서버에 보낼 DTO, TResponse는 서버한테 받을 DTO입니다.

 

이때 클라이언트와 서버가 통신하기 위해 직렬화가 필요합니다.

string json = JsonUtility.ToJson(body);

여기서 매개변수에서 받은 DTO를 JSON으로 변환해줍니다.

 

byte[] bodyBytes = Encoding.UTF8.GetBytes(json);

또한 json을 UTF-8을 사용해 byte로 변환하면 영문뿐만 아니라 한글도 깨지지 않기 때문에 UTF-8 변환을 해줍니다.

 

using (UnityWebRequest request =
       new UnityWebRequest(
           url,
           UnityWebRequest.kHttpVerbPOST))

주소로 보낼 POST 요청을 만드는 코드입니다. 

 

request.uploadHandler =
    new UploadHandlerRaw(bodyBytes);

UploadHandler는 유니티에서 서버로 보내는 데이터를 관리합니다.

앞서 만든 json 바이트가 들어가는데 json 바이트가 Request Body로 서버에 전송됩니다.

 

request.downloadHandler =
    new DownloadHandlerBuffer();

DownloadHandler는 UploadHandler에 반대로 서버에서 유니티로 오는 응답을 받습니다.

 

request.SetRequestHeader(
    "Content-Type",
    "application/json");

 

Content-Type을 설정하는 코드로 서버에게 지금 보내는 Request Body가 JSON 타입이라는걸 알려주는 코드입니다.

 

 

 

private IEnumerator SendRequest<TResponse>(
    UnityWebRequest request,
    Action<ApiResult<TResponse>> onCompleted)

SendRequest()는 서버에게 실제 요청을 보내는 함수입니다.

요청을 보냈을 때 성공 응답을 받으면 서버가 넘겨준 JSON을 DTO로 변환하고 ApiResult로 묶어서 onCompleted로 넘겨주는 역할을 합니다.

 

코드가 길어서 이해하기 좀 어려웠지만 메소드 단위로 보면 생각보다 어렵지 않았던 것 같습니다.