목차
리플렉션
리플렉션은 프로그램이 실행 중에 자기 자신을 거울 보듯 살펴볼 수 있게 해주는 기능이라고 생각할 수 있습니다.
마치 게임에서 캐릭터가 자기 인벤토리를 확인하고, 무슨 아이템을 가지고 있는지, 어떤 능력을 사용할 수 있는지를 확인하는 것처럼, 프로그램도 리플렉션을 통해 자신이 가지고 있는 코드의 부품들, 예를 들어 클래스, 메서드, 변수 등을 살펴보고 사용할 수 있게 됩니다.
간단한 예시
리플렉션을 이용해 간단하게 게임의 아이템클래스 중 하나의 인스턴스를 만들고, 그 아이템의 메서드를 호출하는 예를 들어보겠습니다.
using System;
using System.Reflection;
public class HealthPotion
{
public void Use()
{
Debug.Log("체력 포션을 사용했습니다. 체력이 회복됩니다!");
}
}
public class PublicHelperUse
{
public static void UseItem
{
// 아이템의 이름을 문자열로 받음
var itemName = "HealthPotion";
// 문자열로부터 아이템 클래스의 타입을 얻음
var itemType = Type.GetType(itemName);
// 해당 타입의 인스턴스(여기서는 HealthPotion 객체)를 생성
var itemInstance = Activator.CreateInstance(itemType);
// 'Use' 메서드 정보를 얻음
var useMethod = itemType.GetMethod("Use");
// 아이템 인스턴스에 대해 'Use' 메서드를 호출
useMethod.Invoke(itemInstance, null);
}
}
C#
복사
리플렉션과 메타데이터
리플렉션과 메타데이터의 관계
리플렉션은 메타데이터를 사용하여 프로그램의 구조를 조회하고 조작합니다.
즉, 리플렉션을 통해 메타데이터에 저장된 정보를 읽어, 특정 클래스의 인스턴스를 생성하거나, 메서드를 호출하거나, 필드의 값을 변경할 수 있습니다.
이 모든 작업은 메타데이터에 기록된 정보를 기반으로 수행됩니다.
메타데이터란?
메타데이터는 데이터에 대한 데이터로, 다른 데이터를 설명하는 정보입니다.
메타데이터는 프로그램이 컴파일 될 때 생성되며, 클래스, 메서드, 변수 등 프로그램의 구조와 관련된 정보를 담고 있습니다.
이 정보에는 타입 이름, 타입이 가지고 있는 메서드와 프로퍼티, 메서드의 매개변수 등이 포함됩니다.
예를 들어, 어떤 클래스가 있을 때, 이 클래스의 메타데이터는 클래스의 이름, 클래스가 상속하는 부모 클래스, 클래스가 구현하는 인터페이스, 클래스에 정의된 메서드와 필드 등의 정보를 포함합니다. 이 메타데이터는 리플렉션을 통해 런타임에 조회할 수 있습니다.
리플렉션의 실제 이용 사례
CSV 데이터를 읽어서 Monster 인스턴스 생성
CSV 파일에서 데이터를 읽고, 각 라인에 대해 Monster 인스턴스를 생성한 후 속성 값을 설정하는 예시 코드입니다.
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
public class Monster
{
public string Type { get; set; }
public int Health { get; set; }
public int Attack { get; set; }
public Monster() { }
}
public static class PublicHelper
{
static void ReadMonster()
{
// CSV 파일 경로
var csvFilePath = "monsters.csv";
// Monster 인스턴스를 저장할 리스트
var monsters = LoadCsvData<Monster>(csvFilePath);
}
static List<T> LoadCsvData<T>(string filePath) where T : new()
{
var resultList = new List<T>();
var lines = File.ReadAllLines(filePath);
// 첫 번째 줄은 헤더로, 속성 이름을 포함
var headers = lines[0].Split(',');
// 데이터 라인 처리
for (int i = 1; i < lines.Length; i++)
{
string[] values = lines[i].Split(',');
T item = new T();
for (int j = 0; j < headers.Length; j++)
{
PropertyInfo propertyInfo = typeof(T).GetProperty(headers[j]);
if (propertyInfo != null)
{
// 속성 타입에 따라 적절한 변환을 수행
object value = Convert.ChangeType(values[j], propertyInfo.PropertyType);
propertyInfo.SetValue(item, value, null);
}
}
resultList.Add(item);
}
return resultList;
}
}
C#
복사
PlayGuideManager Ediotr 사용
이 부분은 제가 직접 Reflection을 활용한 방법입니다.
아래와 같이, 플레이가이드의 스텝 클래스들은 자동으로 플레이가이드 에디터에 추가되며, 해당 정보를 읽어 스크립터블 오브젝트를 생성하는 부분입니다.
private void CreateGUI()
{
rootVisualElement.styleSheets.Add(AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/Scripts/PlayGuide/Editor/PlayGuide_Properties_Style.uss"));
var titleContainer = new VisualElement();
var titleLabel = new Label(Title);
titleContainer.Add(titleLabel);
rootVisualElement.Add(titleContainer);
_playGuideStepTypeList = Assembly
.GetAssembly(typeof(PlayGuideStep))
.GetTypes()
.Where(t => t.IsSubclassOf(typeof(PlayGuideStep))).ToList();
_addGuideStepListView = new ListView(_playGuideStepTypeList, 20f, MakeItem, BindItem);
_addGuideStepListView.onSelectionChange -= OnSelectStepChange;
_addGuideStepListView.onSelectionChange += OnSelectStepChange;
_addGuideStepListView.AddToClassList("addPlayGuideStepListView-listView");
rootVisualElement.Add(_addGuideStepListView);
var buttonContainer = new VisualElement();
var AgreeButton = new Button(OnAgreeAction);
AgreeButton.text = "추가";
AgreeButton.AddToClassList("YESNO-button");
var DisAgreeButton = new Button(this.Close);
DisAgreeButton.text = "취소";
DisAgreeButton.AddToClassList("YESNO-button");
buttonContainer.Add(AgreeButton);
buttonContainer.Add(DisAgreeButton);
buttonContainer.AddToClassList("horizontal-container");
rootVisualElement.Add(buttonContainer);
}
C#
복사
리플렉션의 주의 사항
성능 저하
•
성능 비용 : 리플렉션은 일반적인 코드 실행보다 더 많은 성능 비용을 소모합니다. 특히, 게임 루프 내부나 빈번히 호출되는 메서드에서 리플렉션을 사용하는 경우, 게임의 프레임 속도에 영향을 줄 수 있습니다. 가능한 한 초기화 단계에서 한 번만 사용하거나, 캐싱 기법을 사용하여 성능 저하를 최소화해야 합니다.
리플렉션의 성능이 매우 느리다는 것을 명확히 인식하는 것이 중요합니다.
예를 들어, GetType()과 같은 작업을 자주 수행하면 낮은 성능 비용이 들지만, 이것도 리플렉션입니다.
그러나 클래스 인스턴스를 생성하거나 메소드를 정적으로 생성하는 것은 성능 차이가 10배에서 최대 450배까지 나게 됩니다.
실제 분석 과정에서 리플렉션의 성능에 얼마나 영향을 주는지 확인하는 것은 어려울 수 있습니다. 이와 관련하여 클래스를 생성할 때 GC.Alloc()이 발생하는 부분과 혼동하는 경우가 많습니다. 이 부분은 클래스를 생성할 때, 리플렉션의 사용과 무관하게 발생합니다.
제 개인적인 의견으로는, 매 프레임 호출하는 곳이 아니라면 리플렉션을 사용해도 괜찮다고 생각합니다. 무작정 사용하라는 말은 아닙니다.
예를 들어, Json과 같은 데이터를 처리해야 할 때 리플렉션이 효율적일 수 있습니다. 이런 경우에는, 리플렉션을 강제로 사용하지 않고 대응하는 것보다 리플렉션을 사용하는 것이 더 좋을 수 있습니다.
IL2CPP과의 호환성
•
IL2CPP 호환성 : IL2CPP는 런타임에 코드를 생성하거나 수정하는 Reflection.Emit과 같은 기능을 지원하지 않습니다.
Reflection.Emit이란?
Reflection.Emit 런타임에 동적으로 새로운 타입을 생성하고, 코드를 컴파일하여 실행할 수 있게 해주는 API입니다. 즉, 이를 사용하면 실행 중인 프로그램이 자기 자신의 코드를 생성하고 수정할 수 있습니다.
IL2CPP에서 Reflection.Emit 사용 불가의 이유
IL2CPP가 중간 언어를 C++ 코드로 변환한 다음 네이티브 코드로 컴파일하는 과정은 전적으로 빌드 타임에 이루어집니다. 즉, 모든 코드는 빌드 시점에 이미 "고정"되어 있으며, 실행 시점에 새로운 코드를 생성하거나 기존 코드를 변경할 수 있는 JIT 컴파일 기능을 지원하지 않습니다. Reflection.Emit은 런타임에 새로운 코드를 생성하고 실행하는 기능을 제공하기 때문에, AOT 컴파일 방식을 사용하는 IL2CPP 환경에서는 사용할 수 없습니다.