2022년에 있었던 팀프로젝트 The Worker 중 내가 맡았던 백엔드에 대한 코드와 설명이다. 원래 주석이 적지 않았지만 인코딩 이슈로 인해 삭제했다. 다음엔 주석을 영어로 다는 것도 고려해야겠다. 이외의 코드도 좀 있지만 내가 주로 작업한 코드는 아래 코드가 전체 코드라고 봐도 무방하다. 백엔드만 담당하였기에 시연 영상에 보이는 클라이언트와 관련된 부분들은 팀원들이 작업한 내용이다. 비록 클라이언트에 비하면 했던 것은 적었지만 개인적으로 의미 있었던 팀프로젝트라고 생각한다.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using BackEnd;
using LitJson;
using UnityEngine;
using Random = UnityEngine.Random;
// DB Function Naming Rule
// DB -> User = Get...
// User -> DB = Update...
public class BackendManager : MonoBehaviour
{
private static BackendManager instance = null;
private void Awake()
{
if (instance is null)
{
instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}
public static BackendManager Instance
{
get
{
if (instance is null)
return null;
return instance;
}
}
public Dictionary<string, RankItem> GetRank(int stageNum)
{
var rankItemList = new Dictionary<string, RankItem>();
var uuid = GetRankId(stageNum);
Debug.Log("Before GetDetailedRank");
foreach (var id in uuid)
{
rankItemList.Add(id.Key, GetDetailedRank(id.Value));
}
Debug.Log("After GetDetailedRank");
return rankItemList;
}
private Dictionary<string, string> GetRankId(int stageNum)
{
// BlockTypeCount, BlockLength, AveargeStep
var rankTableItemList = new List<RankTableItem>();
string[] rankTypeArray = { $"S{stageNum}_BL", $"S{stageNum}_BTC", $"S{stageNum}_AS" };
var rankId = new Dictionary<string, string>();
var bro = Backend.URank.User.GetRankTableList();
if (bro.IsSuccess())
{
JsonData rankTableListJson = bro.FlattenRows();
for (int i = 0; i < rankTableListJson.Count; i++)
{
RankTableItem rankTableItem = new RankTableItem();
rankTableItem.rankType = rankTableListJson[i]["rankType"].ToString();
rankTableItem.date = rankTableListJson[i]["date"].ToString();
rankTableItem.uuid = rankTableListJson[i]["uuid"].ToString();
rankTableItem.order = rankTableListJson[i]["order"].ToString();
rankTableItem.isReset = rankTableListJson[i]["isReset"].ToString() == "true" ? true : false;
rankTableItem.title = rankTableListJson[i]["title"].ToString();
rankTableItem.table = rankTableListJson[i]["table"].ToString();
rankTableItem.column = rankTableListJson[i]["column"].ToString();
if (rankTableListJson[i].ContainsKey("rankStartDateAndTime"))
{
rankTableItem.rankStartDateAndTime =
DateTime.Parse(rankTableListJson[i]["rankStartDateAndTime"].ToString());
rankTableItem.rankEndDateAndTime =
DateTime.Parse(rankTableListJson[i]["rankEndDateAndTime"].ToString());
}
if (rankTableListJson[i].ContainsKey("extraDataColumn"))
{
rankTableItem.extraDataColumn = rankTableListJson[i]["extraDataColumn"].ToString();
rankTableItem.extraDataType = rankTableListJson[i]["extraDataType"].ToString();
}
rankTableItemList.Add(rankTableItem);
Debug.Log(rankTableItem.ToString());
}
foreach (var type in rankTypeArray)
{
var result = rankTableItemList.Find(a => a.title.Equals(type));
if (result is null)
{
Debug.Log("No Rank Table Item Error");
break;
}
Debug.Log($"GetRankId Success. {type} : {result.uuid}");
rankId.Add(type, result.uuid);
}
}
return rankId;
}
private RankItem GetDetailedRank(string uuid)
{
RankItem rankItem = new RankItem();
var bro = Backend.URank.User.GetMyRank(uuid);
if (bro.IsSuccess())
{
Debug.Log("@GetDetailedRank Start");
JsonData rankListJson = bro.GetFlattenJSON();
string extraName = string.Empty;
rankItem.gamerInDate = rankListJson["rows"][0]["gamerInDate"].ToString();
if (rankListJson["rows"][0]["nickname"] != null)
rankItem.nickname = rankListJson["rows"][0]["nickname"].ToString();
else
rankItem.nickname = "Unknown";
rankItem.score = rankListJson["rows"][0]["score"].ToString();
rankItem.index = rankListJson["rows"][0]["index"].ToString();
rankItem.rank = rankListJson["rows"][0]["rank"].ToString();
rankItem.totalCount = rankListJson["totalCount"].ToString();
if (rankListJson["rows"][0].ContainsKey(rankItem.extraName))
{
rankItem.extraData = rankListJson["rows"][0][rankItem.extraName].ToString();
}
Debug.Log(rankItem.ToString());
}
return rankItem;
}
public Dictionary<string, Dictionary<string, List<string>>> GetOtherUserCode(int stageNum)
{
var topThreeCodes = new Dictionary<string, Dictionary<string, List<string>>>(); // <rankType, Dict<nickname, code>>
var rankId = GetRankId(stageNum);
foreach (var id in rankId)
{
var bro = Backend.URank.User.GetRankList(id.Value, 3, 0);
if (bro.IsSuccess() == false)
{
Debug.Log("GetOtherUserCode Error : " + bro);
return null;
}
var rankListJson = bro.GetFlattenJSON();
var userCodeDict = new Dictionary<string, List<string>>();
for (int i = 0; i < rankListJson["rows"].Count; i++)
{
var gamerRank = rankListJson["rows"][i]["rank"].ToString(); // key
var gamerNickname = rankListJson["rows"][i]["nickname"].ToString(); // value
var gamerCode = rankListJson["rows"][i]["UserCode"].ToString(); // value
var list = new List<string>();
list.Add(gamerNickname);
list.Add(gamerCode);
userCodeDict.Add(gamerRank, list);
// Debug.Log($"{id.Key}, {i + 1} : {rankListJson["rows"][i]["UserCode"]}");
}
topThreeCodes.Add(id.Key, userCodeDict);
}
return topThreeCodes;
}
public void UpdateScore(int stageNum, int blockLength, int blockTypeCount, float averageStep, string userCode)
{
var rankId = GetRankId(stageNum);
var tableName = $"STAGE_{stageNum}";
var rowInDate = string.Empty;
Param param = new Param();
param.Add("BlockLength", blockLength);
param.Add("BlockTypeCount", blockTypeCount);
param.Add("AverageStep", averageStep);
param.Add("UserCode", userCode);
SendQueue.Enqueue(Backend.GameData.GetMyData, tableName, new Where(), bro =>
{
if (bro.IsSuccess() == false)
{
Debug.Log(bro);
return;
}
if (bro.GetReturnValuetoJSON()["rows"].Count <= 0)
{
SendQueue.Enqueue(Backend.GameData.Insert, tableName, param, callback =>
{
if (callback.IsSuccess() == false)
{
Debug.Log(callback);
Debug.Log("UpdateScore-Insert Error");
return;
}
if (callback.FlattenRows().Count > 0)
rowInDate = callback.GetInDate();
foreach (var item in rankId) // Dic <rankType , uuid>
{
InnerUpdate(item.Value, tableName, rowInDate, param);
}
});
}
else
{
SendQueue.Enqueue(Backend.GameData.Update, tableName, new Where(), param, callback =>
{
if (callback.IsSuccess() == false)
{
Debug.Log(callback);
Debug.Log("UpdateScore-Update Error");
return;
}
rowInDate = bro.GetInDate();
foreach (var item in rankId) // Dic <rankType , uuid>
{
InnerUpdate(item.Value, tableName, rowInDate, param);
}
});
}
});
}
private void InnerUpdate(string uuid, string tableName, string rowIndate, Param param)
{
SendQueue.Enqueue(Backend.URank.User.UpdateUserScore, uuid, tableName, rowIndate, param, bro =>
{
if (bro.IsSuccess() == false)
{
Debug.Log(bro);
Debug.Log("UpdateScore-UpdateUserScore Error");
}
});
}
public (bool isValid, string message) CreateNickname(string nickname)
{
var bro = Backend.BMember.CreateNickname (nickname);
return (bro.IsSuccess(), bro.GetMessage());
// Error cases -> https://developer.thebackend.io/unity3d/guide/bmember/nickname/
}
public void UpdateMyInfo(UserInfo userInfo)
{
Param param = Param.Parse(userInfo);
var tableName = "USER_INFO";
SendQueue.Enqueue(Backend.GameData.GetMyData, tableName, new Where(), bro =>
{
if (bro.IsSuccess() == false)
{
Debug.Log(bro);
return;
}
if (bro.GetReturnValuetoJSON()["rows"].Count <= 0)
{
SendQueue.Enqueue(Backend.GameData.Insert, tableName, param, callback =>
{
if (callback.IsSuccess() == false)
{
Debug.Log(callback);
Debug.Log("UpdateMyInfo-Insert Error");
return;
}
Debug.Log("UpdateMyInfo-Insert Success");
});
}
else
{
SendQueue.Enqueue(Backend.GameData.Update, tableName, new Where(), param, callback =>
{
if (callback.IsSuccess() == false)
{
Debug.Log(callback);
Debug.Log("UpdateMyInfo-Update Error");
return;
}
Debug.Log("UpdateMyInfo-Update Success");
});
}
});
}
public UserInfo GetMyInfo()
{
var bro = Backend.GameData.GetMyData("USER_INFO", new Where(), 1);
if (bro.IsSuccess() == false)
{
Debug.Log("IsSuccess Error");
Debug.Log(bro);
return null;
}
if (bro.FlattenRows()["rows"].Count <= 0)
{
Debug.Log("No data");
Debug.Log(bro);
return null;
}
var myInfoJson = bro.FlattenRows();
var userInfo = JsonMapper.ToObject<UserInfo>(myInfoJson.ToJson());
Debug.Log(userInfo.ToString());
return userInfo;
}
public string CreateRandomNickname()
{
var frontWord = new[] {"신나는", "멋진", "행복한","슬픈","우울한","재수없는","고생하는","귀여운", "오른손이 커다란", "밥을 좋아하는"};
var backWord = new[] {"바지", "옷걸이", "얼음", "코끼리", "개미핥기", "빨대", "가방", "눈꺼풀", "의자", "팔꿈치"};
var randFront = Random.Range(0, frontWord.Length);
var randBack = Random.Range(0, backWord.Length);
return frontWord[randFront] + backWord[randBack];
}
}
1.시작
목표
- GPGS 로그인
활동
- 안드로이드 빌드 이슈 해결
- GPGS 이슈 해결
- 로그인 구현 시도
2.중간 발표
목표
- GPGS 로그인
- 뒤끝 SDK 활용 → 유저 정보 저장
- 랭킹 생성 / 갱신
→ 팀원 피드백 : 랭킹 랜덤 조회 → 상위 3명 조회로 변경 요청
활동
- GPGS 로그인 활성화
- 뒤끝 서버 유저 정보 저장 완료
3.팀 발표
목표
- 랭킹 생성 / 갱신
- 랭킹 조회
- 유저 정보 생성 / 갱신
- 유저 정보 조회
활동
- 랭킹 조회에 대한 피드백 반영
- 각종 이슈 해결
4.최종
목표
- 기존 목표 기능 보완
활동
- 기존 목표 기능 추가 발생 이슈 해결
- 다른 팀원의 이슈 해결 도움
5.느낀점
백엔드 개발은 처음이었는데, 처음부터 끝까지 디버깅과의 전쟁이었다고 생각이 듦. GPGS 특성상 구글 플레이 콘솔에 Android App Build 파일(이하 aab)을 업로드 하지 않으면 해당 버전을 실행 조차 할 수 없어서 디버깅하는데 상당한 시간을 소요함. 특히나 안드로이드 빌드는 컴퓨터의 성능에 의해 시간이 많이 좌지우지 되어 불편함이 많았음. 백엔드의 특성상 클라이언트가 요구하는 객체만 넘겨주면 되기 때문에 적절한 객체를 약속하고 그 객체가 올바르게 반환 될 때까지 스스로 디버깅하면서 작업하는 일의 반복이기 때문에 상대적으로 팀원들과의 소통이 적었고 이로 인해 야기되는 불편한 점이 종종 있었음. 예를 들면 스테이지의 인덱스 번호를 0부터 시작할지, 1부터 시작할지 정도나 반환되는 객체가 복잡해져서 이를 다시 설명해야되는 경우 등 몇몇 불편한 경우를 겪고 소통의 중요성을 체감함.
6.개선점
디버깅 및 기능 변경 요구를 거치면서 코드에 수정을 거듭하게 되었는데, 이로 인해 시각적으로 보기에 안좋아진 코드들이 생기게 되었음. 다음부턴 디자인 패턴을 고려하며 수정하더라도 기존의 코드를 재사용할 수 있게끔 만들고 싶음.
팀 프로젝트 초반에는 느끼지 못했지만 후반에 데드라인이 가까워짐에 따라 소통의 부재가 아쉬웠음. 다음에는 좀 더 적극적으로 소통에 참여하기로 다짐함.
이번 프로젝트에서는 주로 API 를 다루었기 때문에 짜여진 코드를 잘 조립해서 원하는 모양을 만들어내는 것에 그쳤지만 다음에 다시 백엔드를 맡게 된다면 서버만 얻은 채로 직접 밑바닥부터 개발해보고 싶음.
서버의 주요 기능은 Assets/Scripts/Backends/BackendManager.cs
에 구현 됨.
싱글톤 패턴으로 BackendManger.Instance.함수명
으로 사용 가능.
각 함수가 리턴하는 객체의 사용 여부에 따라 객체를 반드시 함수 실행 이후에 사용해야 한다면 동기 방식으로, 실행 순서에 관계없다면 SendQueue(비동기와 유사) 방식으로 사용함.
동기 방식 적용 함수
GetRankId
, GetDetailedRank
, GetOtherUserCode
, CreateNickname
, GetMyInfo
주로 서버로부터 클라이언트가 객체를 전달받을 때 적용
SendQueue 방식 적용 함수
UpdateScore
, InnerUpdate
, UpdateMyInfo
주로 클라이언트가 서버로 객체를 전달할 때 적용
Assets/Scripts/Backends/GoogleLogin.cs
참조유저가 사용중인 플레이스토어 아이디로 자동으로 회원 가입 시도
회원 가입 요청이 정상적으로 이루어 졌다면 Social.localUser.Authenticate((bool success))
BackendManager.Instance.GetMyInfo()
로 서버에서 로드신규 회원의 가입 요청의 경우
랜덤한 닉네임을 생성한 후 기본 정보를 서버에 업데이트 및 로드
UpdateMyInfo(NewuserInfo)
: 새로 생성된 정보를 USER_INFO 테이블에 저장함
CreateNickname(nickname)
: 새로 생성된 닉네임을 뒤끝 서버에 전달함
생성된 유저 테이블은 위의 DB - 유저 테이블
이미지와 같은 형식
Assets/Scripts/Backends/BackendManager.cs
참조 GetMyInfo
로 사용 가능하며 USER_INFO
테이블에서 유저 고유 값(uuid)에 맞는 정보를 가져와 클라이언트에 로드함.Assets/Scripts/Backends/BackendManager.cs
참조 UpdateScore
로 사용 가능하며 생성과 갱신의 기능을 동시에 담당할 수 있게끔 SDK 를 변형하여 적용함.클리어한 스테이지 정보를 서버로부터 가져옴(GetRankId
)
서버에 자신의 데이터가 있는지 검사(Backend.GameData.GetMyData
)
Backend.GameData.Insert
→ InnerUpdate
를 통해 정보를 삽입함Backend.GameData.Update
→ InnerUpdate
를 통해 정보를 갱신함생성(삽입)과 갱신을 하나의 함수에서 처리한 이유
두 기능의 공통점은 정보를 바꾼다는 것인데, 뒤끝 서버에서 제공하는 함수는 같은 역할을 할 수 있는 하나의 함수가 없기 때문에 개발의 편의를 위해 두 기능을 하나의 함수로 묶음
InnerUpdate
의 역할
내부적으로 Backend.URank.User.UpdateUserScore
를 SendQueue 방식으로 수행함
Assets/Scripts/Backends/BackendManager.cs
참조자신의 랭킹 조회
GetRank
로 사용 가능
Dictionary<string, RankItem>
형태로 반환하며 key
값(분야)에 따른 자신의 순위를 RankItem.rank
로 조회 가능
상위 3명의 코드 조회
GetOtherUserCode
로 사용 가능
Backend.URank.User.GetRankList
의 파라미터를 적절하게 입력하여 분야별로 1등부터 상위 3명의 랭킹 정보를 가져옴
가져온 Json
타입 정보 중 필요한 정보만 분리하여 클라이언트에게 전달함
전달하는 객체 : Dictionary<string, Dictionary<string, List<string>>>
→ < 분야 , < 랭킹(인덱스) , 코드리스트 > >