목록

Consideration for Data Parsing System

Json파싱 구상하면서 생각한 고민들을 나열

나중에 볼 때 어떻게 돌아가는지 까먹을 까봐 일단 기록


여러 데이터 파일 형식이 있지만(json, xml등)

저번 프로젝트에서 Json을 사용했고 친숙하고 직관적이여서 만들고 있는 프레임워크에도 Json형식 기반으로 넣어보려고한다.


—엑셀데이터작성에 몇가지 약속 필요

우리는 기획자분이 만든 Excel파일을 Json형태로 파싱해야한다.

확대 cfdps-1

이렇게 규칙을 정해놔야 어떤식으로 Json파싱할지 정할 수 있다.

요런식으로 Json파싱 및 클라에서 사용하기 편하게 먼저 규칙을 잡고간다.
(프레임워크단계라 실제 프로젝트에서는 또 어떻게 바뀔지 모른다.)


—Json파싱 스크립트 작성

구글링해보니 역시, Excel을 Json으로 변환해주는 플러그인존재
하지만, 우리끼리의 Excel규칙에 맞춰 내가 원하는 구성으로 Json뽑기 원하므로 기존소스를 살짝 변경

<ExcelToJsonConverter.cs>

// 나도 전체코드 다 모르지만,
// Json으로 변환 해주는 부분만 일단 찾아서 변환전 우리 규칙에 맞게 파싱되도록 수정
// 예시로 일부 발췌
...
char[] _charAr = value.ToCharArray();
// # 또는 $ 인경우 Column값 안읽도록 셋팅.(&는 json 키 값으로 쓸 거라서) - 엑셀규칙
if (_charAr[0] != '#' && _charAr[0] != '$')
{
	table.Columns.Add(value);
	//컬럼테이블이랑 동기화 시켜줘야하므로.
	ref_tableKey.Add(ref_AlltableKey[i]);
}
...

엑셀데이터가 이런식으로 Json형태로 가공된다

cfdps-2_1

에디터 상에서 GUI적으로 작업하기 위하여

Converter과정을 UnityEditor-Tool로 확장

cfdps-2

일단 기본적인 재료들은 준비가 된거같다


—Json파일은 어떻게 관리하나?(1)

맨처음에는 서버에 올린 Json파일을 받아 사용하며 매번 앱 실행시 Json파일 통신하기가 좀 그러해서,

디바이스에 Json파일 캐싱해 통신때 버전체크만 우선적으로해 다른 버전의 Json파일만 통신해 갱신해주는 방법생각-1 -> (AssetBundle관리로직 착안)

괜춘은데, 해야할게 많고.. 암호화부분도 아직 잘 몰라 자체적으로 암호화해 디바이스에 저장하기에 부담된다


—Json파일은 어떻게 관리하나?(2)

->AssetBundle화 시켜버리면 해결될거 같다.
(AssetBundle관련로직 참고 - 아직포스팅 안함…)

다른 리소스들과 마찬가지로 Json도 AssetBundle화 시켜 서버에서 받으면
AssetBundle 내부적으로 버전관리 해주고 캐싱도 해주므로 우리가 할 일은 줄어든다


—단점으로는

1. 과정이 살짝 더 번거로워졌고(Excel -> Json -> AssetBundle -> 서버)

2. 서버에서 Json파일을 건들수가 없다.(AssetBundle화 되어 있으므로)


-> 해결방안으로는
서버단에서 cs툴이나 조작해야하는 Json파일은 AssetBundle화 하지말고 그냥 올리는식으로(게임 실행 시 매번통신)

웬만해서 변동 안되며 변동 되더라도 앱버전이나 번들버전에 따라 관리되는(다른 AssetBundle처럼) Json파일은 똑같이 AssetBundle로 관리


—Json데이터 서버로 보내기

파싱된 Json파일 까지 준비 됐으면, 이제 서버로 올려야 한다.

Json파일은 어떻게 관리하나(2)에서 구상했듯이,
Assetbundle로 묶일 Json파일들은 그냥 Assetbunble만들 때 같이 만들고 같이 서버에 올려주면 된다.
([프레임워크_제작기-참고-포스팅준비중]] 에서 만든 시스템에 묻어가면 됨)


Json파일은 어떻게 관리하나(2) CS용 Json파일은 따로 서버에 보내야 되는데,
서버개발자의 요구에 따라(전 프로젝트는 요런식으로 관리해서) Git을 통해 서버에 올리는 방식을 사용해 보자

CS용 Json파일 대응

  • Json파일 날짜별 버전관리가 용이하도록 Git이용
  • 우리는 그냥 Git과 연동해 해당 파일만 새로 올리면 된다. 나머지는 서버측에서 알아서 해줄것(webhook을 이용해서?)

—Git에 올리기용 스크립트 작성

  • 따로 스크립트로 만들은 이유는, 에디터tool로 자동화되도록 하려고(Json파일 생성 -> Git푸쉬)
  • Git명령어를 코드를 C#에 접목시키기 위해 LibGit2Sharp라이브러리 를 사용했다.
  • 전체 코드 표시하기엔 너무 길으므로 Git에 푸쉬하는 부분의 코드만 첨부
/// <summary>
/// 만들어진 JSON파일(srcCopyPath에 존재하는)을 gitPath에 복사 후 원격저장소로 올리는 프로세스
/// </summary>
/// <param name="gitPath"></param>
/// <param name="srcCopyPath"></param>
private void GitProcessByLibgit2sharp(string gitPath, string srcCopyPath)
{
    using (Repository repo = new Repository(gitPath))
    {
        #region 푸쉬 과정 콜백 함수.
        // (강제)푸쉬 과정 플로우 미리 콜백함수로.
        System.Action _onGitPush = () =>
        {
            using (Repository repo_ = new Repository(gitPath))
            {
                // 기존파일 변경되거나 추가된 부분(스테이지 푸쉬안된 사항) 있으면 지운다.
                repo_.Reset(ResetMode.Hard);
                repo_.RemoveUntrackedFiles();

                // Pull실행 후 결과가 데이터 업로드해도 되는 경우.
                try
                {
                    if (help.Funcs.Git.Pull(repo_) == MergeStatus.UpToDate)
                    {
                        // 체크아웃 시도.
                        string _checkOutBranchName = gitAvailableBranchNames.ElementAt(gitAvailableBranchIndex);
                        Branch _currentBranch = Commands.Checkout(repo_, repo_.Branches[_checkOutBranchName]);

                        // 체크아웃 성공 시.
                        if (_currentBranch != null)
                        {
                            // converter된 서버용 데이터(outPath - server)를 깃 로컬 저장소 폴더로 이동
                            help.Funcs.DirectoryCopy(srcCopyPath, gitPath, true, true);
                        }

                        // 스테이지에 올리고
                        help.Funcs.Git.AddAllIndex(repo_);
                        // 변경사항 커밋
                        help.Funcs.Git.Commit(repo_, gitCommitMsg);
                        // 원격 저장소로 푸쉬
                        help.Funcs.Git.Push(repo_, _currentBranch);

                        Debug.Log("git push success!!");
                    }
                }
                catch(Exception e)
                {
                    Debug.LogError("깃 액션 관련.. 에러..(Pull, Push, Commit) Git User Info맞나 체크 요망.\n" + e);
                }

            }
        };
        #endregion

        // 경고 팝업 띄운다.
        if (gitForcedPushPopupWindowInstance == null)
        {
            gitForcedPushPopupWindowInstance = GitForcedPushPopup.OnShow(this.position.center.x, this.position.center.y, 400, 400);
            gitForcedPushPopupWindowInstance.onIgnoreAndContinue = _onGitPush;
        }
    }
}

-> 위에 만들어 놓은 UnityEditor-Tool에 추가

cfdps-3_1

이런식으로 Json파일 뽑고 Git에 올리도록 만들어 놓는다.


—내부 클라에서는 어떤식으로 사용?

로드된 Json을 단위별로 나눠 Dictionary형태로 가공 해 놓으면 사용하기 편할거 같다.

예로 Dic<파일이름, <Dic<ID, Dic<데이터이름, 데이터>>>> 식의 트리구조로 저장해 놓으면
사용할때 변수[파일이름][ID값][찾을데이터이름] 식으로 바로 접근해 사용하면 편할듯하다.

참고 - 엑셀시트가 [파일이름]-[ID값]-[데이터키]-[데이터] 식으로 구성되어 있다.
그러므로 우리가 받은 Json의 형태는 아래와 같다는 전제하에 코드를 작성해야한다.

cfdps-3

—데이터보관 스크립트 작성(ExternalJsonData.cs)

#필드 정의

// 값데이터 <값 이름, 값>
[Serializable] public class ValueUnitDt : SerializableDictionary<string, string>
// Id데이터 <ID키, 값 데이터>
[Serializable] public class IdUnitDt    : SerializableDictionary<string, ValueUnitDt> { }
// 파일데이터 <파일이름, Id데이터>
[Serializable] public class FileUnitDt  : SerializableDictionary<string, IdUnitDt> { }

[SerializeField] FileUnitDt m_JsonFile = new FileUnitDt();
[SerializeField] ValueUnitDt m_JsonFileVersion = new ValueUnitDt();


외람이지만,

  • Dictionary말고 SerializableDictionary사용한 이유는 맨 처음 만들 때
    Json파일은 어떻게 관리하나(1) 처럼 디바이스에 직렬화해 저장하는걸 고려했기 떄문인데,
    사실 그 방법은 배제되었으므로 일반 Dictionary사용해도됨.
  • ValueUnitDt, IdUnitDt, FileUnitDt클래스로 따로 만든 이유는,
    사실 원래 SerializableDictionary사용하려면 class로 한번 감싸줘야 제대로 직렬화 기능이 작동하므로 어쩔수 없이 만들었는데,
    저런 단위별로 클래스 만드니 좀 더 보기에 깔끔해져서 그냥 놔두고 있다.


#프로퍼티

public IdUnitDt this[string fileName_]
{
    get
    {
        return m_JsonFile[fileName_];
    }
}
public ValueUnitDt JsonFileVersion { get => m_JsonFileVersion; }

#데이터 추가 메서드

/// <summary>
/// 해당 파일 이름의 Json데이터 추가
/// </summary>
/// <param name="fileName_"></param>
/// <param name="jsonData_"></param>
public void AddData(string fileName_, JSONObject jsonData_)
{
    if (m_JsonFile.ContainsKey(fileName_))
    {
        Debug.LogError("ExternalJsonData - AddData already contain" + fileName_);
        return;
    }

    IdUnitDt _idUnitDt = new IdUnitDt();
    jsonData_.keys.ForEach(_keyIsID =>
    {
        if (jsonData_[_keyIsID].IsObject)
        {
            // [ID, 값단위데이터] 형식으로 1차 가공
            _idUnitDt.Add(_keyIsID, new ValueUnitDt(jsonData_[_keyIsID]));
        }
        else if (jsonData_[_keyIsID].IsString)
        {
            // 버전 정보는 따로 저장
            if (_keyIsID == ReservedName.version)
                m_JsonFileVersion.Add(fileName_, jsonData_[_keyIsID].str);
        }
    });
    // [파일이름, [ID값, 데이터]] 형식으로 2차 가공
    m_JsonFile.Add(fileName_, _idUnitDt);
}

잠깐 여기서 추가해줘야할 사항이 있다.

new ValueUnitDt(jsonData_[_keyIsID])

이 부분을 보면jsonData_[_keyIsID]자체가 JsonObject형식이라 우리가 만들어둔 wrapping클래스인 ValueUnitDt(Dictionary)가 JsonObject형식에 대응하도록 해야한다.

사실 JsonObject자체가 toDictionary함수를 제공해서 애초에 Dictionary형식이면 그냥 변환 후 넣어도 되는데 우린이미 wrapping클래스 쓰고있고 ValueUnitDt에 또 copy작업하기에는 그러므로

JsonObject에서 제공하는 toDictionary함수코드를 복사해 ValueUnitDt생성자 부분에 넣어주자

/// Jobject데이터 복사
public ValueUnitDt(JSONObject jOb_)
{
    if (jOb_.type == JSONObject.Type.OBJECT)
    {
        for (int i = 0; i < jOb_.list.Count; i++)
        {
            JSONObject val = jOb_.list[i];
            switch (val.type)
            {
                case JSONObject.Type.STRING: this.Add(jOb_.keys[i], val.str); break;
                case JSONObject.Type.NUMBER: this.Add(jOb_.keys[i], val.n + ""); break;
                case JSONObject.Type.BOOL: this.Add(jOb_.keys[i], val.b + ""); break;
                default:
#if UNITY_2 || UNITY_3 || UNITY_4 || UNITY_5
            Debug.LogWarning
#else
                System.Diagnostics.Debug.WriteLine
#endif
            ("Omitting object: " + jOb_.keys[i] + " in dictionary conversion");
                break;
            }
        }
    }
}

그럼이제 ValueUnitDt생성과 동시에 JsonObject의 값이 채워질 것이다.


—로드된 AssetBundle에서 데이터 추출

이제 AssetBundle에서 받은 Json데이터를 위의 데이터보관 스크립트(ExternalJsonData.cs)의 변수에 넣는 과정을 작성할건데,

더 앞부분인 AssetBundle받는 로직이 궁금하면 [[프레임워크_제작기|]- 포스팅예정..] 참고 하면 된다.

DataManager 스크립트 작성

  • ExternalJsonData는 위에 작성한 클래스이다

#필드 구성

ExternalJsonData m_JsonDt;
public ExternalJsonData JsonDt { get => m_JsonDt; }

Data관련 에셋 번들만 찾아서 로드해 JsonObject형식으로 바꿔준 후
ExternalJsonData에 데이터 추가하는 코드작성

public async Task LoadJsonDataFromBundles()
{
    Debug.Log("** start LoadJsonDataFromBundles **");

    // FIXME : data번들들이 JSON오브젝트 형태라고 가정하에 가져옴.
    string[] _nameRelatedDataBundles = ResourceManager.Instance.GetNameRelatedDataBundles();
    List<Task<UnityEngine.Object[]>> _IndividualBundleContains = new List<Task<UnityEngine.Object[]>>();

    for (int i = 0; i < _nameRelatedDataBundles.Length; ++i)
    {
        string _dtBundleName = _nameRelatedDataBundles[i];

        // task들 모았다가 한번에 일괄 처리 하려고.(비동기)
        _IndividualBundleContains.Add(ResourceManager.Instance.GetBundleContents(_dtBundleName));
    }

    await Task.WhenAll(_IndividualBundleContains);

    _IndividualBundleContains.ForEach((_bundleContains) =>
    {
        UnityEngine.Object[] _allAssets = _bundleContains.Result;
        for (int j = 0; j < _allAssets.Length; ++j)
        {
            string _strJsonDt = _allAssets[j].ToString();
            JSONObject _jsonDt = new JSONObject(_strJsonDt);

            string _keyIsFileName = _allAssets[j].name;

            // **! 데이터 추가 !**
            m_JsonDt.AddData(_keyIsFileName, _jsonDt);
        }
    });

    Debug.Log("** end LoadJsonDataFromBundles **");
    return;
}

나중에 다른 시점에 서버로부터 Json파일을 받는 경우는 그 시점에 ExternalJsonData의 AddData함수 호출하여 할당해주면 된다.

주의사항

  • 모든AssetBundle이 로드된 후 호출해줘야한다

나중에 고쳐야 할 부분 ResourceManager.Instance.GetNameRelatedDataBundles()함수는 일단 로드된 데이터에셋번들들을 다 가져오므로, 데이터 추가가 일괄처리 되게 작성되어있다.
그래서 특정 데이터사용할 떄 그 때 로드해 추가하되도록 동적으로도 되도록 따로 함수 작성해줄 필요 있음


—사용해보자

이제 외부에서 이런식으로 사용하면 된다.

string _getData = DataManager.Instance.JsonDt["character"]["2"]["character_model_name"];

-> 아직, 실제 프로젝트에서 제대로 적용해보지 못했고 이론상 생각한것만 구상해놓은 수준이라 수정사항이 많이 생길수도 있다 아니 생긴다