Unity에서 C# 리플렉션 제대로 활용하기! Type, FieldInfo, Activator 완벽 정리
리플렉션은 런타임에 코드 정보를 가져와서 동적으로 조작할 수 있는 강력한 기능인데요, 이를 활용하면 게임 개발에서 다양한 문제를 유연하게 해결할 수 있습니다.
- 리플렉션의 기본 개념과 Unity에서의 활용
- Type 클래스: 런타임에 타입 정보 얻기, 필드/프로퍼티/메서드 정보 추출
- FieldInfo 클래스: 필드의 값을 읽고 수정하는 방법
- Activator 클래스: 동적으로 객체를 생성하는 방법
- 실제 Unity 프로젝트에서의 응용 사례와 주의 사항
리플렉션이란?
리플렉션은 런타임에 어셈블리, 타입, 멤버 등의 정보를 조사하고 조작할 수 있는 메커니즘입니다.
Unity 개발에서는 다음과 같은 경우에 리플렉션을 활용할 수 있습니다:
- 인스펙터 확장: 에디터 스크립트에서 특정 필드나 속성을 자동으로 표시하거나 조작
- 데이터 매핑: JSON, XML, CSV 등의 데이터를 클래스에 동적으로 매핑할 때
- 디버깅/로그 기록: 런타임 객체의 상태를 출력해 문제를 파악할 때
- 동적 객체 생성: 게임 오브젝트나 컴포넌트를 이름이나 타입으로 생성할 때
리플렉션은 강력하지만, 잘못 사용하면 성능 저하나 코드 보안 문제를 일으킬 수 있으므로 주의해야 합니다.
Type – 런타임 타입 정보 다루기
Type 클래스는 C# 리플렉션의 핵심입니다. 런타임 시 객체의 타입 정보를 조회할 수 있을 뿐만 아니라, 해당 타입의 필드, 프로퍼티, 메서드, 생성자 및 기타 멤버들에 대한 다양한 정보를 제공해줍니다.
주요 API 및 활용법
- typeof(클래스명)
컴파일 타임에 타입 정보를 가져오는 가장 간단한 방법입니다.
Type exampleType = typeof(Example);
Debug.Log($"Example 타입 이름: {exampleType.Name}");
- object.GetType()
인스턴스에서 직접 타입 정보를 얻을 수 있습니다.
Example instance = new Example();
Type instanceType = instance.GetType();
Debug.Log($"instance의 타입: {instanceType.FullName}");
- Type.GetType("네임스페이스.클래스명")
문자열을 통해 타입을 가져올 수 있습니다. 네임스페이스와 어셈블리 정보가 정확해야 합니다.
Type dynamicType = Type.GetType("MyNamespace.Example, MyAssembly");
if(dynamicType != null)
Debug.Log($"동적으로 가져온 타입: {dynamicType.Name}");
else
Debug.Log("타입을 찾지 못했습니다.");
- GetFields()
해당 타입의 public 필드 목록을 반환합니다.
FieldInfo[] fields = exampleType.GetFields();
foreach (var field in fields)
Debug.Log($"Field: {field.Name}");
- GetProperties()
public 프로퍼티 목록을 조회합니다.
PropertyInfo[] properties = exampleType.GetProperties();
foreach (var prop in properties)
Debug.Log($"Property: {prop.Name}");
- GetMethods()
public 메서드 목록을 가져오며, BindingFlags를 통해 정적, 인스턴스, 또는 선언된 메서드만 선택할 수 있습니다.
MethodInfo[] methods = exampleType.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
foreach (var method in methods)
Debug.Log($"Method: {method.Name}");
- GetConstructors()
타입의 모든 생성자 정보를 배열로 반환합니다. 생성자에 전달되는 매개변수를 보고, 적절한 생성자를 선택할 때 유용합니다.
ConstructorInfo[] ctors = exampleType.GetConstructors();
foreach (var ctor in ctors)
Debug.Log($"생성자: {ctor.ToString()}");
- GetNestedTypes()
타입 내부에 선언된 중첩 타입(내부 클래스)들을 가져옵니다.
Type[] nestedTypes = exampleType.GetNestedTypes();
Debug.Log($"내부에 선언된 타입 수: {nestedTypes.Length}");
- GetCustomAttributes(bool inherit)
해당 타입에 적용된 모든 커스텀 어트리뷰트를 가져옵니다.
object[] attrs = exampleType.GetCustomAttributes(false);
foreach(var attr in attrs)
Debug.Log($"어트리뷰트: {attr.GetType().Name}");
- IsSubclassOf(Type)
특정 타입이 다른 타입의 서브클래스인지 확인할 때 사용됩니다.
if(exampleType.IsSubclassOf(typeof(MonoBehaviour)))
Debug.Log("Example은 MonoBehaviour를 상속받고 있습니다.");
이처럼 Type 클래스는 객체와 클래스의 구조에 대한 다양한 정보를 제공하여, 런타임에 코드를 동적으로 제어하고 수정할 수 있게 해 줍니다. Unity에서 스크립트 간의 의존성을 줄이고 유연한 시스템을 설계할 때 큰 도움이 됩니다.
FieldInfo – 필드 값 읽기와 수정
FieldInfo는 특정 타입의 필드에 관한 정보를 제공하며, 이를 통해 런타임에 필드 값을 읽거나 변경할 수 있습니다. 이 기능은 Unity에서 인스펙터에 노출되지 않은 변수에 접근하거나, 외부 데이터에 기반하여 필드 값을 업데이트할 때 특히 유용합니다.
주요 API 및 활용법
- GetValue(object instance)
지정된 객체의 해당 필드 값을 반환합니다.
FieldInfo scoreField = typeof(ExampleField).GetField("score");
int originalScore = (int)scoreField.GetValue(exampleFieldInstance);
Debug.Log($"원래 score: {originalScore}");
- SetValue(object instance, object value)
지정된 객체의 필드 값을 새로운 값으로 설정합니다.
scoreField.SetValue(exampleFieldInstance, 500);
Debug.Log($"변경된 score: {scoreField.GetValue(exampleFieldInstance)}");
- IsPublic, IsPrivate 등
해당 필드의 접근 제한자를 확인할 수 있습니다.
if(scoreField.IsPublic)
Debug.Log("score 필드는 public입니다.");
else
Debug.Log("score 필드는 public이 아닙니다.");
- FieldType
필드가 어떤 데이터 타입을 갖고 있는지 확인할 수 있습니다.
Debug.Log($"score 필드 타입: {scoreField.FieldType.Name}");
- GetCustomAttributes()
필드에 적용된 커스텀 어트리뷰트를 조회할 수 있습니다.
object[] customAttrs = scoreField.GetCustomAttributes(false);
foreach (var attr in customAttrs)
Debug.Log($"score 필드 어트리뷰트: {attr.GetType().Name}");
고급 API: GetValueDirect와 SetValueDirect(TypedReference, object)
주로 값 형식(Struct)에서 직접 값을 읽거나 설정할 때 사용되지만, 일반적인 상황에서는 GetValue와 SetValue를 주로 사용합니다.
이처럼 FieldInfo를 사용하면 객체의 내부 상태를 런타임에 자유롭게 읽고 수정할 수 있습니다. Unity 프로젝트에서는 예를 들어, 플레이어의 스코어나 게임 상태와 같이 동적으로 변하는 데이터를 외부에서 제어하거나, 디버깅 중에 변수 값을 확인하는 데 매우 유용합니다.
Activator – 동적 객체 생성하기
Activator는 런타임에 타입 정보를 기반으로 객체를 동적으로 생성하는 기능을 제공합니다. 이 기능을 활용하면, 프로그램 실행 중에 문자열이나 외부 데이터로부터 타입을 결정하고 해당 타입의 인스턴스를 생성할 수 있습니다. 이는 플러그인 시스템, 동적 객체 생성, 또는 스크립트 기반 게임 오브젝트 생성 등 다양한 상황에서 유용하게 사용됩니다.
주요 API 및 활용법
- Activator.CreateInstance(Type)
기본 생성자를 호출하여 객체를 생성합니다.
Type enemyType = typeof(Enemy);
Enemy enemy1 = (Enemy)Activator.CreateInstance(enemyType);
Debug.Log($"생성된 enemy1 이름: {enemy1.enemyName}");
- Activator.CreateInstance(Type, object[] args)
매개변수를 전달하여 특정 생성자를 호출할 수 있습니다.
object[] args = { "보스 적" };
Enemy enemy2 = (Enemy)Activator.CreateInstance(enemyType, args);
Debug.Log($"생성된 enemy2 이름: {enemy2.enemyName}");
- 제네릭 버전: Activator.CreateInstance<T>()
제네릭 방식으로 객체를 생성할 수 있어, 코드의 가독성을 높여줍니다.
Enemy enemy3 = Activator.CreateInstance<Enemy>();
Debug.Log($"생성된 enemy3 이름: {enemy3.enemyName}");
예제 코드
using UnityEngine;
using System;
public class Enemy
{
public string enemyName;
public Enemy()
{
enemyName = "기본 적";
Debug.Log("Enemy 기본 생성자 호출됨");
}
public Enemy(string name)
{
enemyName = name;
Debug.Log($"Enemy 생성자 호출됨: {enemyName}");
}
}
public class ActivatorDemo : MonoBehaviour
{
void Start()
{
// 기본 생성자 사용
Type enemyType = typeof(Enemy);
Enemy enemy1 = (Enemy)Activator.CreateInstance(enemyType);
Debug.Log($"생성된 enemy1 이름: {enemy1.enemyName}");
// 인자를 받는 생성자 사용
object[] args = { "보스 적" };
Enemy enemy2 = (Enemy)Activator.CreateInstance(enemyType, args);
Debug.Log($"생성된 enemy2 이름: {enemy2.enemyName}");
// 제네릭 방식으로 생성
Enemy enemy3 = Activator.CreateInstance<Enemy>();
Debug.Log($"생성된 enemy3 이름: {enemy3.enemyName}");
}
}
이처럼 Activator를 사용하면 런타임에 객체를 동적으로 생성할 수 있으므로, 유연한 플러그인 시스템이나 데이터 기반의 오브젝트 생성 로직을 구현할 때 매우 유용합니다.
Unity 프로젝트에서 리플렉션 응용 사례
1) 인스펙터 확장 및 자동 바인딩
Unity 에디터에서 특정 스크립트의 필드나 프로퍼티에 자동으로 접근하여 커스텀 인스펙터를 구현할 때 리플렉션이 유용합니다. 예를 들어, 직렬화된 데이터를 자동으로 로드하거나 저장하는 기능을 구현할 때 리플렉션을 사용할 수 있습니다.
2) 데이터 매핑 시스템
게임 내 설정 값이나 외부 데이터 파일(JSON, XML 등)을 읽어와서, 이를 동적으로 특정 클래스의 인스턴스에 매핑하는 경우가 많습니다.
리플렉션을 통해 클래스의 필드 목록을 가져오고, 파일의 키와 일치하는 필드에 값을 할당하면, 유연한 데이터 매핑 시스템을 구현할 수 있습니다.
3) 동적 컴포넌트 추가 및 제거
리플렉션을 사용하면 런타임에 컴포넌트를 동적으로 추가하거나 제거할 수 있습니다.
예를 들어, 플러그인 시스템이나 모듈식 아키텍처를 설계할 때, 문자열로 전달된 클래스 이름을 기반으로 컴포넌트를 생성하고 GameObject에 붙이는 식으로 활용할 수 있습니다.
리플렉션 사용 시 주의사항
리플렉션은 강력하지만 다음과 같은 단점을 고려해야 합니다:
- 성능 저하: 리플렉션 호출은 일반적인 메서드 호출보다 느릴 수 있으므로, 빈번하게 호출해야 하는 로직에는 주의해야 합니다.
- 안전성 문제: 리플렉션을 통해 private 멤버에도 접근할 수 있기 때문에, 잘못된 값이 할당되면 버그나 예기치 못한 동작이 발생할 수 있습니다.
- 코드 가독성 저하: 리플렉션을 많이 사용하면 코드가 복잡해지고, 유지보수가 어려워질 수 있으므로 꼭 필요한 경우에만 사용하는 것이 좋습니다.
Unity에서 C# 리플렉션의 대표적인 API인 Type, FieldInfo, **Activator**를 통해 런타임에 타입 정보를 얻고, 필드 값을 조작하며, 동적으로 객체를 생성하는 방법에 대해 알아보았습니다.
이러한 기술들은 유연한 데이터 매핑, 인스펙터 확장, 동적 객체 생성 등 다양한 Unity 개발 상황에서 큰 힘을 발휘할 수 있습니다.
핵심 정리
- Type: 클래스의 메타데이터를 가져와 필드, 프로퍼티, 메서드를 조사할 수 있다.
- FieldInfo: 필드 값을 런타임에 읽고 수정하는 데 사용된다.
- Activator: 런타임에 동적으로 객체를 생성하여, 인스턴스를 제어할 수 있다.
리플렉션은 조금 복잡해 보일 수 있지만, 잘 활용하면 프로젝트의 유연성과 확장성을 크게 높일 수 있는 도구입니다.