개요
사내에 딱 정해진 REST api 통신 모듈이 없다보니
직접 만들고 다음 프로젝트가 진행될때마다
겪었던 시행착오들을 상기하며 모듈을 보완, 발전 시키고 있다.
이제는 틀이 꽤 잡힌것 같아서
한번 기록을 해둘려고 한다.
본문
먼저 들어가기에 앞서,
이 모듈은 Json을 쉽게 입출력 하기 위해
Newtonsoft.Json 패키지를 사용하였다.
이 패키지에 대한 설명은 아래 글에 기록해뒀다.
먼저 ServerManager의 베이스가될 클래스를 아래와 같이 구현했다.
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
using System.Text;
/* ServerManager의 베이스가 될 클래스 */
public class HttpServerBase : MonoBehaviour
{
public enum SendType { GET, POST, PUT, DELETE }
/* 최종적으로 보내는 곳 */
protected virtual IEnumerator SendRequestCor(string url, SendType sendType, JObject jobj, Action<Result> onSucceed, Action<Result> onFailed, Action<Result> onNetworkFailed)
{
/* 네트워크 연결상태를 확인한다. */
yield return StartCoroutine(CheckNetwork());
using (var req = new UnityWebRequest(url, sendType.ToString()))
{
/* 보낸 데이터를 확인하기 위한 로그 */
Debug.LogFormat("url: {0} \n" +
"보낸데이터: {1}",
url,
JsonConvert.SerializeObject(jobj, Formatting.Indented));
/* body 입력 */
byte[] bodyRaw = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(jobj));
req.uploadHandler = new UploadHandlerRaw(bodyRaw);
req.downloadHandler = new DownloadHandlerBuffer();
/* header 입력 */
req.SetRequestHeader("Content-Type", "application/json");
/* 전송 */
yield return req.SendWebRequest();
/* 성공인지 실패인지 확인 */
var result = ResultCheck(req);
/* 네트워크 에러라면 */
if (result.IsNetworkError)
{
onNetworkFailed?.Invoke(result);
/* TODO: 네트워크 재시도 팝업 호출. */
yield return new WaitForSeconds(1f);
Debug.LogError("재시도");
yield return StartCoroutine(SendRequestCor(url, sendType, jobj, onSucceed, onFailed, onNetworkFailed));
}
else
{
/* 통신성공 이라면 */
if (result.IsSuccess)
{
onSucceed?.Invoke(result);
}
/* 서버측 실패라면(인풋이 잘못됐덨가 서버에서 연산오류가 났던가) */
else
{
onFailed?.Invoke(result);
}
}
}
}
protected virtual IEnumerator CheckNetwork()
{
if(Application.internetReachability == NetworkReachability.NotReachable)
{
/* TODO: 네트워크 오류 팝업 호출 */
Debug.LogError("네트워크 연결 안됨");
yield return new WaitUntil(() => Application.internetReachability != NetworkReachability.NotReachable);
Debug.Log("네트워크 재연결됨");
}
}
protected virtual Result ResultCheck(UnityWebRequest req)
{
Result res;
switch (req.result)
{
case UnityWebRequest.Result.InProgress:
res = new Result(req.downloadHandler.text, false, true, "InProgress");
return res;
case UnityWebRequest.Result.Success:
JObject jobj = JObject.Parse(req.downloadHandler.text);
/* 서버측에서 "code"데이터가 0이 아니면 전부 실패 케이스로 쓰기로 했다. */
bool isSuccess = int.Parse(jobj["code"].ToString()) == 0 ? true : false;
Debug.Log(req.downloadHandler.text);
/* 성공 */
if (isSuccess)
{
res = new Result(req.downloadHandler.text, true, false, string.Empty);
return res;
}
/* 실패 */
else
{
Debug.LogErrorFormat("요청 실패: {0}", jobj["message"].ToString());
res = new Result(req.downloadHandler.text, false, false,
string.Format("Code: {0} - {1}", jobj["code"].ToString(), jobj["message"].ToString()));
return res;
}
case UnityWebRequest.Result.ConnectionError:
case UnityWebRequest.Result.ProtocolError:
case UnityWebRequest.Result.DataProcessingError:
/* 통신에러 */
Debug.LogError(req.error);
Debug.Log(req.downloadHandler.text);
res = new Result(req.downloadHandler.text, false, true, req.error);
return res;
default:
Debug.LogError("디폴트 케이스에 걸림");
Debug.LogError(req.error);
Debug.Log(req.downloadHandler.text);
res = new Result(req.downloadHandler.text, false, true, "Unknown");
return res;
}
}
/* 통신 결과를 담기위한 클래스 */
public class Result
{
/* 서버로부터 받은 리턴값을 담을 변수 */
private string json;
/* 성공인지 여부 */
private bool isSuccess;
/* 네트워크 에러인지 서버측 에러인지 여부 */
private bool isNetworkError;
/* 에러 내용 */
private string error;
public string Json => json;
public bool IsSuccess => isSuccess;
public bool IsNetworkError => isNetworkError;
public string Error => error;
public Result(string json, bool isSuccess, bool isNetworkError, string error)
{
this.json = json;
this.isSuccess = isSuccess;
this.isNetworkError = isNetworkError;
this.error = error;
}
}
}
특히 신경쓴 부분은 아래정도이다.
1. 실패 케이스를 서버에서 실패를 리턴해준 케이스와 네트워크 에러인케이스를 구분한 것
2. 성공, 실패시 실행할 콜백에 필요한 정보를 Result클래스로 구현해서 Result클래스를 파라미터로 콜백을 보냄으로
파라미터를 깔끔하게 유지
URL을 저장할 클래스는 아래와 같이 관리하였다.
일부 예시만 가져왔다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameURL
{
/* DB서버 */
public static class DBServer
{
public static readonly string Server_URL = "http://**.***.***.***:52532";
public static readonly string getBoardPath = "/get_board";
}
/* 회원가입, 로그인 등을 담당할 서버 */
public static class AuthServer
{
public static readonly string Server_URL = "http://**.***.***.***:52531";
public static readonly string userLogInPath = "/user_login";
}
}
특히 신경쓴 부분은 아래정도이다.
1. static클래스로 접근하기 쉽게함
2. 포트번호에 따라 담당하는 기능이 다르므로 클래스도 나눔
이제 이 모듈로 AuthServerManager를 만들어 로그인 요청을 날리는 예시를 보자면 아래와 같다.
public class AuthServerManager : HttpServerBase
{
public static AuthServerManager Instance { get; private set; }
[SerializeField]
private UserInfo currentUserInfo;
#region 프로퍼티
public UserInfo CurrentUserInfo { get => currentUserInfo;
set
{
currentUserInfo = value;
}
}
#endregion
private void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
else
Destroy(gameObject);
}
public Coroutine UserLogin(string email, string nickName, Enums.LoginType type, string password,
Action<Result> onSucceed = null, Action<Result> onFailed = null, Action<Result> onNetworkFailed = null)
{
/* 로그인 URL을 조합 */
string url = GameURL.AuthServer.Server_URL + GameURL.AuthServer.userLogInPath;
/* Newtonsoft.Json 패키지를 이용한 Json생성 */
JObject jobj = new JObject();
jobj["email"] = email;
jobj["nickname"] = nickName;
jobj["type"] = (int)type;
jobj["password"] = password;
/* 성공했을때 콜백 */
/* 새로운 유저 정보를 세팅함 로그인 요청을했고 성공했다면 항상 업데이트 되도록 할려고 이쪽에 정의함 */
Action<Result> updateUserInfoAction = (result) =>
{
/* Newtonsoft.Json 패키지를 이용한 Json Parsing */
var resultData = JObject.Parse(result.Json)["data"];
string uuid = resultData["uid"].ToString();
string email = resultData["email"].ToString();
string nickName = resultData["nickname"].ToString();
int gold = int.Parse(resultData["gold"].ToString());
int cash = int.Parse(resultData["cash"].ToString());
int loginType = int.Parse(resultData["type"].ToString());
int catterySlotCount = int.Parse(resultData["home_max"].ToString());
UserInfo newUserInfo = new UserInfo(uuid, nickName, email, gold, cash, catterySlotCount, (Enums.LoginType)loginType);
UserInventory newInven = ScriptableObject.CreateInstance<UserInventory>();
newUserInfo.Inventory = newInven;
CurrentUserInfo = newUserInfo;
};
onSucceed += updateUserInfoAction;
return StartCoroutine(SendRequestCor(url, SendType.POST, jobj, onSucceed, onFailed, onNetworkFailed));
}
}
위와 같이 처음에 만든 HttpServerBase클래스를 상속받아
AuthServerManager클래스를 만든뒤
요청별로 매개변수와 콜백들을 받아서 요청을 보내게 된다.
결론
이렇게 공유하며 쓸 모듈들을 늘려가다보면 뿌듯함과 동시에
정석적인 방법이 맞는지 항상 불안하고 고민에 빠지게 된다.
많이찾아보고 다양한 방법도 써보고 직접 장단점을 느껴보는게 좋은 방법인것 같다.
다음 프로젝트엔 UniTask라이브러리를 활용해서 코루틴과 콜백 없이
Task비동기로 통신하는 모듈로 업그레이드를 해봐야겠다.
'Unity > Network' 카테고리의 다른 글
[Unity] 오픈 소스 게임 서버 Nakama를 알아보자 (0) | 2024.10.24 |
---|---|
[Unity] HTTP 통신 모듈 빌더패턴으로 재구성 (0) | 2023.03.08 |
[Unity] TCP서버 위치 동기화 데드 레커닝 구현 (0) | 2023.01.09 |