구글 스프레드 시트 API를 이용해 유니티 데이터 테이블 관리 매니저 만들기 2: 구현편

김보겸·2020년 6월 4일
6
post-thumbnail

안녕하세요. 준비편에 이어 이번 포스팅에서는 데이터 테이블 관리 매니저를 본격적으로 구현해보도록 하겠습니다.

public class DataManager : MonoBehaviour
{
    private static DataManager _instance;
    public static DataManager I
    {
        get
        {
            if(_instance == null)
            {
                GameObject dataManager = new GameObject();
                dataManager.name = "DataManager";
                _instance = dataManager.AddComponent<DataManager>();
            }
            return _instance;
        }
    }

    // 테이블 묶음을 관리할 DataSet 변수
    private DataSet _database

    private void Awake()
    {
        DontDestroyOnLoad(this);
    }
        public void InitDataManager()
    {
        _database = new DataSet("Database");
#if UNITY_EDITOR
	//에디터에서 실행시 스프레드시트 API 호출
        MakeSheetDatset(_database);
#else
	//Android, Ios 환경에서 실행 시 로컬 json 파일에서 데이터 받아옴
        LoadJsonData(_database);
#endif
    }
}

우선 히어아키에 존재하면서 API를 호출해 데이터를 수신할 매니저 클래스를 생성해주세요
이 매니저는 인스턴스 최초 호출 시에 GameObject로 추가되어 씬변환 시에도 계속 존재합니다.

시트에서 파싱한 DataTable 들을 관리할 DataSet 변수도 필요하구요

에디터와 다른 플랫폼과의 분기를 나누는 이유는 뒤 내용에 설명하겠습니다.

public static class DataUtil
{
    public static DataTable GetDataTable(string fileName, string tableName)
    {
        var obj = Resources.Load(fileName);
        string value = (obj as TextAsset).ToString();
        DataTable data = JsonConvert.DeserializeObject<DataTable>(value);
        data.TableName = tableName;

        return data;
    }
    
    public static DataTable GetDataTable(FileInfo info)
    {
        string fileName = Path.GetFileNameWithoutExtension(info.Name);
        string path = string.Concat("JsonData/", fileName);
        string value = string.Empty;
        try {
            value = (Resources.Load(path) as TextAsset).ToString();
        }
        catch(Exception ex)
        {

        }
        
        DataTable data = JsonConvert.DeserializeObject<DataTable>(value);
        data.TableName = fileName;

        return data;
    }
    public static string GetDataValue(DataSet dataSet, string tableName, string primary, string value, string column)
    {
        DataRow[] rows = dataSet.Tables[tableName].Select(string.Concat(primary, " = '", value, "'"));

        return rows[0][column].ToString();
    }
     public static void SetObjectFile<T>(string key, T data)
    {
        string value = JsonConvert.SerializeObject(data);
        File.WriteAllText(Application.dataPath + "/Resources/JsonData/" + key + ".json", value);
    }

데이터를 용도에 맞게 발라내는 기능들을 모아놓을 유틸 클래스를 추가해줍니다.
이제 본격적인 API 호출에 들어갈텐데요

전편에 기억해놓으라고 말씀드렸던 QAuth 2.0 클라이언트 ID와 비밀번호를 가지고 서비스를 호출합니다.

테이블 키는 스프레드시트 Url 에서 저부분을 복붙하시면 됩니다.


private void MakeSheetDatset(DataSet dataset)
    {
        var pass = new ClientSecrets(); 
        pass.ClientId = "QAuth 2.0 클라이언트 ID"
        pass.ClientSecret = "QAuth 2.0 클라이언트 PW"

        var scopes = new string[] { SheetsService.Scope.SpreadsheetsReadonly };
        var credential = GoogleWebAuthorizationBroker.AuthorizeAsync(pass, scopes, "QAuth2.0 클라이언트 이름", CancellationToken.None).Result;

        var service = new SheetsService(new BaseClientService.Initializer()
        {
            HttpClientInitializer = credential,
            ApplicationName = "GCP 프로젝트 이름"
        });
        
       var request = service.Spreadsheets.Get("테이블 키").Execute();
		
        foreach(Sheet sheet in request.Sheets)
        {
            DataTable table = SendRequest(service, sheet.Properties.Title);
            dataset.Tables.Add(table);
        }

    }

스프레드시트 파일에 존재하는 시트들의 목록을 받아와서 시트별로 데이터를 받아옵니다.

private DataTable SendRequest(SheetsService service, string sheetName)
    {
        DataTable result = null;
        bool success = true;

        try
        {
	    //!A1:M은 스프레드시트 A열부터 M열까지 데이터를 받아오겠다는 소리
            var request = service.Spreadsheets.Values.Get("테이블키", sheetName + "!A1:M");
	    //API 호출로 받아온 IList 데이터
            var jsonObject = request.Execute().Values;
            //IList 데이터를 jsonConvert 하기위해 직렬화
            string jsonString = ParseSheetData(jsonObject);
			
            //DataTable로 변환
            result = SpreadSheetToDataTable(jsonString);
        }
        catch (Exception e)
        {
            success = false;
            Debug.LogError(e);
            string path = string.Format("JsonData/{0}", sheetName);
            //예외 발생시 로컬 경로에 있는 json 파일을 통해 데이터 가져옴
            result = DataUtil.GetDataTable(path, sheetName);
            Debug.Log("시트 로드 실패로 로컬 " + sheetName + " json 데이터 불러옴");
        }

        Debug.Log(sheetName + " 스프레드시트 로드 " + (success ? "성공" : "실패"));

        result.TableName = sheetName;

        if (result != null)
        {
        	//변환한 테이블을 json 파일로 저장
            SaveDataToFile(result);
        }

        return result;
    }
    private DataTable SpreadSheetToDataTable(string json)
    {
        DataTable data = JsonConvert.DeserializeObject<DataTable>(json);
        return data;
    }

시트이름을 통해 받아온 데이터를 DataTable 으로 만들어서 반환합니다.

[{"컬럼1":"값","컬럼2":"값","컬럼3":"값"},
{"컬럼1":"값","컬럼2":"값","컬럼3":"값"},
{"컬럼1":"값","컬럼2":"값","컬럼3":"값"},
{"컬럼1":"값","컬럼2":"값","컬럼3":"값"},
{"컬럼1":"값","컬럼2":"값","컬럼3":"값"},
{"컬럼1":"값","컬럼2":"값","컬럼3":"값"}]

DataTable을 jsonConvert로 직렬화를 하게되면 이런 데이터가 뽑아져 나오는데요,

    private string ParseSheetData(IList<IList<object>> value)
    {
        StringBuilder jsonBuilder = new StringBuilder();

        IList<object> columns = value[0];

        jsonBuilder.Append("[");
        for (int row = 1; row < value.Count; row++)
        {
            var data = value[row];
            jsonBuilder.Append("{");
            for (int col = 0; col < data.Count; col++)
            {
                jsonBuilder.Append("\"" + columns[col] + "\"" + ":");
                jsonBuilder.Append("\"" + data[col] + "\"");
                jsonBuilder.Append(",");
            }
            jsonBuilder.Append("}");
            if(row != value.Count - 1)
                jsonBuilder.Append(",");
        }
        jsonBuilder.Append("]");
        return jsonBuilder.ToString();
    }

API를 통해 가져온 IList 데이터를 jsonConvert로 DataTable화 시키기 위해
위 포맷에 맞게 직렬화해줍니다.

    private void SaveDataToFile(DataTable newTable)
    {
    	//로컬경로
        string JsonPath = string.Concat(Application.dataPath + "/Resources/JsonData/" + newTable.TableName + ".json");
        FileInfo info = new FileInfo(JsonPath);
        //동일 파일 유무 체크
        if(info.Exists)
        {
            DataTable checkTable = DataUtil.GetDataTable(info);
			//파일 내용 체크
            if (BinaryCheck<DataTable>(newTable, checkTable))
            {
                return;
            }
        }
        //json파일 저장
        DataUtil.SetObjectFile(newTable.TableName, newTable);
    }
    private bool BinaryCheck<T>(T src, T target)
    {
    	//두 대상을 바이너리로 변환해서 비교, 다르면 false 반환
        BinaryFormatter formatter1 = new BinaryFormatter();
        MemoryStream stream1 = new MemoryStream();
        formatter1.Serialize(stream1, src);

        BinaryFormatter formatter2 = new BinaryFormatter();
        MemoryStream stream2 = new MemoryStream();
        formatter2.Serialize(stream2, target);

        byte[] srcByte = stream1.ToArray();
        byte[] tarByte = stream2.ToArray();

        if (srcByte.Length != tarByte.Length)
        {
            return false;
        }
        for (int i = 0; i < srcByte.Length; i++)
        {
            if (srcByte[i] != tarByte[i])
                return false;
        }
        return true;
    }
    

에디터 상에서는 스프레드시트를 통해 가져온 데이터를 쓰지만, Android나 Ios 환경에서는 로컬json파일에서 데이터를 뽑아오기 위해 빌드시에 포함이 되는 Resources 폴더 내에 json 파일을 저장합니다.

에디터 실행시마다 로컬데이터 동기화를 위해 로컬 파일과 시트 데이터의 내용이 다르면 덮어씌웁니다.

다음은 에디터상이 아닐 때의 로컬 데이터 로드입니다.

  private void LoadJsonData(DataSet dataset)
    {
    	
        string JsonPath = string.Concat(Application.dataPath + "/Resources/JsonData/");
        DirectoryInfo info = new DirectoryInfo(JsonPath);
        foreach(FileInfo file in info.GetFiles())
        {
        	//로컬 경로에서 json 가져와서 DataTable으로 변환
            DataTable table = DataUtil.GetDataTable(file);
            dataset.Tables.Add(table);
        }
    }

경로내에 모든 json 파일들을 DataTable 으로 변환해서 DataSet 에 포함시켜 사용합니다.
테이블을 받아오는 부분은 여기까지구요

테이블 내의 데이터를 가져오고 싶을때는

 public string SelectTableData(string tableName, string primary, string column)
    {
        DataRow[] rows = _database.Tables[tableName].Select(string.Concat(primary, " = '", column, "'"));

        return rows[0][column].ToString();
    }

이런식으로 pk 역할을 할 키를 지정하여 데이터를 가져오는 등 용도에 맞게 함수를 만들어서 데이터에 엑세스 할 수 있겠죠

이제 매니저 코드 작성은 끝났습니다. 실행을 시켜보면

이런식으로 스프레드시트 데이터를 잘 불러오는 것을 확인할 수 있습니다.

파일도 잘 저장이 되구요
데이터 관리가 필요한데 db구축은 땨로 할 생각이 없으신 분들은 유용하게 쓸 수 있을 거라고 생각합니다.

7개의 댓글

comment-user-thumbnail
2020년 6월 6일

덕분에 많은 도움이 되었습니다!! 감사합니다.

1개의 답글
comment-user-thumbnail
2020년 7월 29일

유용한 정보 올려 주셔서 감사하고, 질문이 하나 있습니다. 에디터에서는 잘 작동하는데, 안드로이드에서 테스트하여 보니 실행이 되지 않네요. 기존 코드로도 해 보고 if문을 없애서 MakeSheetDatset()함수도 실행해 보고 로그도 찍어 봤는데 실행이 되지 않더라고요. 혹시 안드로이드 환경에서는 불가능한 건가요?

2개의 답글
comment-user-thumbnail
2020년 7월 29일

제가 할때
var credential = GoogleWebAuthorizationBroker.AuthorizeAsync(pass, scopes, "OAuth이름", CancellationToken.None).Result; 이부분에서 invaild cast exception이 발생하는데 왜그런가요?

1개의 답글