이번에 새로 시작한 프로젝트에서 기획을 마치고 우선 큰 기능부터 빠르게 제작하는 프로토타입을
진행하고 있다. 여기에서 나는 인벤토리를 맡았고 다른 팀원은 CSV 데이터를 받아오는 기능을 맡았다.
아무래도 인벤토리와 아이템 데이터는 연관이 깊기 때문에 빠르게 팀원의 코드를 분석하는 것이 소통과
개발에 있어서 유연하게 진행될 것 같다고 생각했다. 그리하여 팀원의 CSV 데이터 코드 구조를 분석하고
코드의 내용을 하나씩 파헤쳐보려 한다.
1. CSV 란?
코드를 먼저 분석하는 것도 좋지만 이 코드를 왜 짰는지 근간을 알아야 한다고 생각하기 때문에
먼저 CSV가 무엇인지 파악하고자 한다.
CSV(comma-separated values)
몇 가지 필드를 쉼표로 구분한 텍스트 데이터 및 텍스트 파일이다.
나는 CSV가 엄청나게 복잡한 영어로 되있는 약자인줄 알았는데 정말 개념그대로 단어를
나열한거였다.. 그럼 여기서 알 수있는 중요한 포인트는 <쉼표, 텍스트> 인 것으로 보인다.
하지만 CSV파일을 열면?
액셀 파일로 나온다.
쉼표는 어디가고 깔끔하게 정렬된 정보들이 보인다.
그래서 찾아보니 CSV 파일을 메모장으로 열면 된다고 한다.
정말로 데이터의 값들이 쉼표로 구분되며 값을 이루고 있는 것을 확인할 수 있다.
그럼 뭔가 데이터를 읽어오는 코드를 구성할 때 쉼표로 구분하여 받아오면 될 것 같다.
이를 통해 CSV가 무엇인지 파악했으며 중요한 키워드들을 알아냈다.
그럼 이제 코드의 구조를 한번 살펴보자.
2. 구조
깃 허브에서 브랜치에 들어가 코드파일을 한 번 살펴봤다.
뭔가 모르겠는 기능들이 잔뜩 있었지만 스크립트의 네이밍을 보면
DataManager에서는 CSV 데이터를 저장해서 관리하는 역할을 하는 것으로보인다.
DataReader는 DataManager에서 CSV데이터를 저장하기 위한 기능으로 보인다.
IDataContent는 I가 붙었으니 인터페이스 파일이며 Data 컨텐츠들을 하나로 묶어서 관리하려는 것으로 보인다.
Item은 CSV데이터의 형태를 저장하기 위한 클래스로 보인다.
결론은 DataManager는 DataReader를 필요로 하고 Item은 DataContent의 인터페이스를 상속받아
DataReader에서 사용되는 것으로 추측할 수 있다.
3. DataManger
DataManager 스크립트의 일부다.
Manager단이기 때문에 싱글톤 패턴으로 고유한 데이터를 관리하고 있다.
LoadDefaultData메서드는 CSV데이터 파일을 읽어와서 IDataContent형의 리스트 공간에
추가하는 것으로 보인다. 이를 통해 _dataList를 통해 다른 곳에서 CSV데이터에 접근할 수
있을 것이다. 참고로 AddRange의 기능은 리스트의 끝에 요소들을 모두 추가하는 기능이다.
-> 리스트 데이터를 차곡 차곡 단계별로 쌓아갈 수 있다는 장점이 있다.
4. DataReader
DataReader의 ReadData 메서드 부분이다.
데이터들을 유연하게 받을 수 있도록 제네릭으로 구현해놓은 것을 확인할 수 있다.
이 제네릭은 IDataContent와 기본 생성자 형태를 받도록 where로 조건이 걸려있다.
그럼 DataManger에서 ReadData 메서드를 IDataContent를 상속받은 Item과 CSV 파일 경로를 넣어서
호출 했기 때문에 제네릭 T는 IDataContent형이 됐다.
첫 시작은 IDataContent 값을 가지는 T의 dataList의 리스트를 만든다. 아무래도 메서드가 리스트 형태이니
dataList를 반환해줘야 하기 때문일 것이다.
다음은 using이 나온다. using은.. 뭔가 위에서 패키지 부를 때 사용하는 거로 알고 있는데 왜 여기서 갑자기 나왔을 까..
해서 찾아보니 using은 IDisposable을 구현하는데 사용된다고 하며 주 사용 목적은 리소스 관리다.
기능을 살펴보면 해당 객체가 사용되지 않을 시 폐기하고, 리소를 해제한다고 나온다.
그럼 using안에 StreamReader 객체가 생성되니 이 객체가 더 이상 사용되지 않으면 자동적으로 객체를 폐기하고 리소스를 해제해주는 과정을 거치는 것이다. 아무래도 객체는 힙 영역에 들어가서 언제 해제될지 모르니 이런 기능을 사용한 것으로 보인다.
그럼 StreamReader는 무엇인가?
앞에서 말했듯이 CSV파일은 '텍스트' 파일이다. 그러므로 텍스트 파일을 읽기 위한 무언가가 필요한데
StreamReader가 텍스트 파일을 읽는 역할을 해준다.
그럼 CSV파일 경로를 받은 StreamReader객체가 생성됐다.
이제 이를 이용하여 텍스트를 어떻게 읽는지 과정이 나올 것이다.
다음 과정을 보면 우선 ReadLine을 한 번 실행해준다. ReadLine이란 다음 게행이 나올 때 까지
한 줄을 읽어 내는 기능인데 굳이? 왜 썼는지 생각했다.
그래서 메모장을 다시 한번 봐보니
실제 데이터는 한 줄 아래에 있었다. 그래서 ReadLine으로 한 줄 넘기고 시작한 것이었다.
그럼 이제 쭉 코드를 살펴보면 streamReader객체의 스트림이 끝날 때 까지 _inputData에 한 줄씩 받아가며
데이터를 읽어서 넣을 것이다. ProPertyInfo는 잠시 냅두고 string values를 보면 ',' 를 기준으로 Split 하여
문자열 값을 받아내고 있다. 앞서 CSV의 C는 comma를 뜻하기 때문에 쉼표를 기준으로 데이터를 Split하는 모습을
볼 수 있다. 그 후 dataList에 IDataContent형의 T값을 Process메서드를 거쳐 Add하고 있다.
그럼 이제 Property, GetTypeProperties메서드, Process메서드가 뭔지 알아내야 한다.
PropertyInfo 배열을 반환하는 GetTypeProperties 메서드 부분이다.
메서드 명만 봐도 뭔가 Type을 빨리 얻어내야만 할 것 같다.
그럼 과정을 살펴보면 현재 제네릭으로 들어온 값을 String으로 반환하여 typeName에 넣어주고 있다.
그리고 아래에서 typePropertiesCache를 TryGetValue를 통해 값을 가져오지 못한다면
properties변수에 제네릭 T에 대한 프로퍼티를 집어넣고 typePropertiesCache 딕셔너리에 typeName에 대한
프로퍼티 값을 넣어준다. 그리고 반환해준다.
그럼 여기서 의문점은 '프로퍼티가 무엇이고 왜 이런 과정을 진행하는가' 였다.
그래서 PropertyInfo를 찾아보니 이름, 데이터 유형, 접근 제한자 같은 해당 속성에 대한 정보를
제공해줄 수 있기 때문이다. 그렇기 때문에 제네렉 T 타입을 프로퍼티로 만들어 배열로 반환 한다면
이 속성에대한 여러가지를 접근하기 쉽기 때문에 사용한 것으로 보인다.
다음으로 if(!typePropertiesCache.TryGetValue(typeName, out var properties) 이 부분이 헷갈렸다.
과정을 한 번더 살펴보니 만약 typePropertiesCache라는 딕셔너리에 프로퍼티값이 이미 존재한다면
값을 반환하고 프로퍼티가 없다면 properties = typeof(T).GetProperties();와 같은 리플렉션 과정을 통하여
T유형의 속성을 나타내는 프로퍼티 배열을 검색하여 찾아낸 후 딕셔너리에 typeName에 알맞는 프로퍼티 값을 넣어준다.
조금 더 추가 설명을 하자면 제네릭 T타입의 모든 프로퍼티들을 properties배열에다가 담아서 단순히 properties로 모든 특정 T타입 안에 있는 것들을 접근할 수 있다는 것이다.
그럼 또 리플렉션이 무엇인지 알아봐야한다.
리플렉션이란?
런타임시 자체 구조를 검사하고 상호작용 할 수 있는 기능이다. 이 말은 즉, 런타임 도중에도 동적으로
조작할 수 있다는 것이다. 또한 리플렉션을 사용하면 해당 메서드, 속성, 필드, 이벤트 및 기타 멤버와 같은 유형의 정보를
얻어낼 수 있다.
그럼 이 메서드의 주요 기능을 결론 짓자면
CSV 데이터를 동적으로 처리하는 시스템으로 보인다. 이유는 리플렉션을 통하여 유형 정보를 동적으로검색하고
후속 작업을 중 성능 향상을 위해 결과를 캐시하고 있기 때문이다.
그리고 IDataContent를 상속받는 다양한 데이터 유형에 대응하기 위해서 제작한 것으로도 보인다.
그럼 마지막으로 Process메서드를 분석할 차례다.
4. Process
private T Process<T>(string[] values, PropertyInfo[] properties) where T : IDataContent, new()
{
T instance = new T();
for (int i = 0; i < properties.Length; i++)
{
var propertyType = properties[i].PropertyType;
if (propertyType.IsEnum) // Enum 처리
{
properties[i].GetSetMethod().Invoke(instance, new object[] { Enum.Parse(propertyType, values[i]) });
}
else if (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(Dictionary<,>))
{
Type[] genericArguments = propertyType.GetGenericArguments();
// 일반적인 처리를 위해 제네릭 인자의 형식을 가져옴
Type keyType = genericArguments[0];
Type valueType = genericArguments[1];
// Dictionary 형식을 만들어 인스턴스 생성
Type dictionaryType = typeof(Dictionary<,>).MakeGenericType(keyType, valueType);
object dictionary = Activator.CreateInstance(dictionaryType);
int dictionaryCount = int.Parse(values[i]);
for (int j = 0; j < dictionaryCount; j++)
{
if (!Enum.TryParse(keyType, values[i + 1 + j * 2], out object key))
{
key = Convert.ChangeType(values[i + 1 + j * 2], keyType);
}
object value = Convert.ChangeType(values[i + 2 + j * 2], valueType);
((IDictionary)dictionary).Add(key, value);
}
properties[i].GetSetMethod().Invoke(instance, new object[] { dictionary });
// dictionary 사용 후 처리
i += 2 * dictionaryCount;
}
else
{
properties[i].GetSetMethod().Invoke(instance, new object[] { Convert.ChangeType(values[i], propertyType) });
}
}
return instance;
}
최종적으로 프로퍼티의 정보가 담긴 인스턴스 T를 반환해줘야 하기 때문에 처음에 T 타입의 인스턴스를 생성해준다.
다음으로 T타입의 모든 프로퍼티 정보가 담긴 properties 배열을 돌면서 정보 값을 하나 씩 처리 해줄 것이다.
그러기 위해선 현재 properties가 무슨 타입인지 확인해야 하고 그에 맞는 값은 반환해줘야 한다.
if문을 보면 Enum타입일 경우 현재 string인 value값을 Enum형으로 변환해주고 instance안에 있는 프로퍼티에 값을
넣어준다.
다음은 Enum이 아니고 제네릭 타입이면서 제네릭타입의 형태가 Dictionart<, >형태이면
현재 프로퍼티타입의 제네릭 구조를 가져와 Key 와 Value로 나누고 있다. 아무래도 string 인 value 값을 돌면서
각각 알맞는 값을 변환해서 넣어줄 것으로 예상된다.
이제 아래를 보면 Type변수에 이전의 Key 와 Value를 사용한 딕셔너리 타입을 넣고 있다.
그리고 Activator.CreateInstance 기능을 사용하여 object형의 변수에 객체를 생성하여 넣어주고 있다.
여기서 Activator.CreateInstance기능이란 영어 뜻 그대로 인스턴스를 만든다는 것이며 new와 같은
기능을 한다. 하지만 new는 컴파일 타임에 유형을 알 때 사용하면 효율적인 방법이다.
지금은 위에서 알아냈듯이 유형을 동적으로 받아서 처리하는 기능을 만들고 있는 것으로 확인했기 때문에 런타임까지
유형을 알 수 없는 경우 인스턴스를 동적으로 생성할 수 있는 Activator.CreateInstance기능을 사용한 것으로 본다.
다음 for문은 우선 CSV 데이터를 봐야만 이해할 수 있는 부분이었다.
현재 AbilityCou... 부분에서 딕셔너리 길이를 우선 받고있다. 그래서 현재 value를 int로 바꿔서 딕셔너리의 크기를
받아내고 있다.
그리고 [i + 1 + j * 2] 인 부분이 나오는데 G부분이 0번째 프로퍼티라고 가정하면 (i = 0)
1, 3, 5이렇게 값이 나온다. 처음 봤을 땐 뭔가 했는데 이 숫자의 위치를 보면 Ability 키값의 위치인 것을 확인할 수 있다.
그래서 이 부분은 Enum이 아니라면 바로 key값을 out해서 값을 넘겨주고 아니라면 keyType으로 values값을 반환해서 key값을 반환해준다.
아래도 마찬가지로 Stat부분을 위치로 판단하여 value값을 얻어내고 있다.
마지막으로 프로퍼티 i번째에 GetSetMethod를 사용하여 프로퍼티에 값을 넣어주고 있다.
properties[i].GetSetMethod().Invoke(instance, new object[] { dictionary });
이 형태를 자세히보면 properties[i]번째의 프로퍼티에 현재 Method를 반환하여 내가 만든 객체 인스턴스에 dictionary값을 넣어주겠다는 의미다. 주의해야 할것은 GetSetMethod는 Method를 불러올 수 없다면 null을 반환하기 때문에
GetSetMethod()?.Invoke() 의 형식으로 가지않으면 터질 수도 있다.
다음으로 모든 과정을 거친 인스턴스를 반환해주며 끝이난다.
최종 결론
이 코드는 CSV의 데이터를 동적으로 유연하게 받기 위한 코드이다.
'프로젝트 기록 > Project N' 카테고리의 다른 글
인벤토리 개발 기록 (창고와 인벤토리) (0) | 2024.04.03 |
---|---|
UIManager 구현 기록 (1) | 2024.02.13 |
UIManager는 왜 만드는 걸까? (0) | 2024.02.07 |
인벤토리 기능 MVC 적용 (0) | 2024.02.01 |
MVC 패턴에 대한 이해 (1) | 2024.01.30 |