안녕하세요.
오늘은 엔진 심화 교과에서 배운 'DependencyInjector'(이하 DI로 생략)에 대해서 복습 겸 공부 블로그를 작성하게 되었습니다.
DI는 유니티에서 컴포넌트 간 결합도를 낮추고 테스트 가능성을 높이기 위해 사용합니다. 예를 들어서 오디오 매니저와 플레이어가 있다고 가정해 봅시다. 플레이어가 걷는 소리를 내기 위해 AudioManager.Instance.PlaySFX(); 로 오디오 매니저의 메서드를 직접 호출하게 되면 이것은 두 클래스 간 강한 결합이 됩니다. 이때 직접 참조를 끊어내기 위해 DI를 사용할 수 있습니다.
이제 DependencyInjector 코드를 구현해 보면서 하나씩 설명해 보겠습니다.
우선 커스텀 어트리뷰트인 InjectAttribute, ProvideAttribute를 만들어줘야 합니다. 의존성을 주입할 때 의존성을 제공할 대상, 제공받을 대상이 명시되어야 하기 때문입니다.
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Method)]
public class InjectAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class ProvideAttribute : Attribute { }
위 코드에서 각각 Attribute를 상속받음으로써 커스텀 어트리뷰트를 만들 수 있었고, AttributeUsage로 해당 커스텀 어트리뷰트를 어디에 붙일 수 있는지 제한합니다. 다음으로 IDependencyProvider를 만들어보겠습니다. IDependencyProvider는 IDependencyProvider를 구현한 클래스는 의존성을 제공할 수 있다는 것을 명시하는 인터페이스로써 역할합니다.
public interface IDependencyProvider { }
이제 본격적으로 DI클래스 구현을 해보겠습니다. DI클래스는 씬에 있는 모든 모노비헤이비들을 자동으로 스캔해서, 의존성을 등록하고 주입하는 역할을 합니다. DI클래스의 동작 흐름은 다음과 같습니다.
씬에 있는 모든 모노비헤이비어 탐색
↓
IDependencyProvider 구현체 수집
RegisterProvider() 호출
_registry에 {타입:인스턴스} 저장
↓
[Inject] 어트리뷰트가 달린 모노비헤이비어 탐색
Inject() 호출
필드 또는 메서드에 _registry에서 꺼낸 인스턴스 주입
그럼 먼저 클래스를 만들고 _bindingFlags, _registry 딕셔너리를 선언해 주겠습니다. 여기서 BindingFlags란, 리플렉션으로 맴버를 탐색할 때 범위를 지정해 주는 '필터'입니다. 또한 DefaultExecutionOrder는 스크립트의 Awake/OnEnable 실행 순서를 제어하는 어트리뷰트입니다. 이때 숫자가 작을수록 먼저 시작됩니다.
[DefaultExecutionOrder(-10)]
public class DependencyInjector : MonoBehaviour
{
private const BindingFlags _bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public;
private readonly Dictionary<Type, object> _registry = new Dictionary<Type, object>();
이제 Awake를 구현해보겠습니다. Awake에서는 씬의 모노비헤이비어를 탐색하고, 구현체를 수집한 후 RegisterProvider를 수집하고, 마지막으로 Inject 어트리뷰트가 달린 모노비헤이비어를 탐색한 후 인스턴스 주입 메서드 Inject를 호출해 줍니다.
private void Awake()
{
IEnumerable<IDependencyProvider> providers = FindMonoBehaviours().OfType<IDependencyProvider>(); // 모든 모노비헤이비어중 IDependencyProvider를 구현한 클래스들을 찾아서 providers에 넣는다.
foreach (IDependencyProvider provider in providers)
{
RegisterProvider(provider);
}
IEnumerable<MonoBehaviour> injectables = FindMonoBehaviours().Where(IsInjectable);
foreach(MonoBehaviour injectable in injectables)
{
Inject(injectable);
}
}
private bool IsInjectable(MonoBehaviour mono)
{
MemberInfo[] members = mono.GetType().GetMembers(_bindingFlags);
return members.Any(member => Attribute.IsDefined(member, typeof(InjectAttribute)));
}
private static MonoBehaviour[] FindMonoBehaviours()
{
return FindObjectsByType<MonoBehaviour>(FindObjectsSortMode.None);
}
다음으로 RegisterProvider메서드를 구현해 보겠습니다. RegisterProvider는 _registry딕셔너리에 구현체를 등록해 주는 역할을 합니다. RegisterProvider는 클래스와 메서드들을 검사하여 [Provide]가 클래스에 붙은 경우 클래스를 키로 딕셔너리에 등록하고 클래스에 [Provide]가 붙지 않을 경우 메서드들을 전부 구해서 순회하고 [Provide]가 붙은 메서드인 경우 반환타입을 키로 딕셔너리에 등록해 주었습니다.
private void RegisterProvider(IDependencyProvider provider)
{
if (Attribute.IsDefined(provider.GetType(), typeof(ProvideAttribute)))
{
_registry.Add(provider.GetType(), provider);
return;
}
// 해당 클래스에서 Provide되고 있는 메서드가 있는지를 찾아서 그걸 실행해서 값을 가져온다.
MethodInfo[] methods = provider.GetType().GetMethods(_bindingFlags);
foreach(MethodInfo method in methods)
{
if (Attribute.IsDefined(method, typeof(ProvideAttribute)))
{
Type returnType = method.ReturnType; // 메서드의 반환타입
object providedInstance = method.Invoke(provider, null); // 메서드 실행
Debug.Assert(providedInstance != null, $"Provide 메서드에서 null이 반환되었습니다. : {method.Name}");
_registry.Add(returnType, providedInstance);
}
}
}
마지막으로 구현체를 주입해 주는 Inject메서드를 구현해 보겠습니다. 매개변수로 들어온 injectableMono클래스의 타입을 받고 [Inject] 어트리뷰트가 붙은 모든 필드를 탐색해 fields에 등록합니다. 이후에 fields를 순회하면서 해당 필드가 필요로 하는 필드타입을 구하고 _registry에 등록된 구현체를 받아와서 값을 넣어줍니다. 필드에 값을 모두 넣어준 후 [Inject]가 붙은 어트리뷰트를 메서드를 구해서 해당 메서드의 파라미터 타입 목록을 추출합니다. 각 파라미터 타입으로 _registry에서 인스턴스를 꺼내서 주입할 인자 배열 생성했습니다. 배열을 생성한 후 메서드를 실행시켜서 메서드 주입을 완료시켜 주었습니다.
private void Inject(MonoBehaviour injectableMono)
{
Type type = injectableMono.GetType();
IEnumerable<FieldInfo> fields = type.GetFields(_bindingFlags).Where(field => Attribute.IsDefined(field, typeof(InjectAttribute)));
foreach (FieldInfo item in fields)
{
Type fieldType = item.FieldType;
object injectInstance = Resolve(fieldType);
Debug.Assert(injectInstance != null, $"필드 타입에 대한 인스턴스가 등록되어 있지 않습니다. : {fieldType.FullName}");
item.SetValue(injectableMono, injectInstance);
}
IEnumerable<MethodInfo> injectableMethods= type.GetMethods(_bindingFlags).Where(method => Attribute.IsDefined(method, typeof(InjectAttribute)));
foreach(MethodInfo method in injectableMethods)
{
Type[] requiredParams = method.GetParameters().Select(p => p.ParameterType).ToArray();
object[] parameters = requiredParams.Select(Resolve).ToArray();
method.Invoke(injectableMono, parameters);
}
}
private object Resolve(Type fieldType)
{
_registry.TryGetValue(fieldType, out object instance);
return instance;
}
이렇게 오늘은 DependencyInjector에 대해서 복습하고 공부해 보았습니다. 수업을 들을 때는 이해가 잘 되지 않았던 부분이었는데 복습하며 공부해 보니 이해가 잘 되어서 앞으로 프로젝트에 적극 활용해 볼 생각입니다.
감사합니다.
'공부블로그' 카테고리의 다른 글
| [공부 블로그] 이벤트 구독에 대하여 (2) | 2026.01.19 |
|---|---|
| [공부 블로그] Delegate에 대하여 (0) | 2025.12.03 |
| [공부 블로그] Unitask를 사용해보자 (0) | 2025.12.01 |
| [공부 블로그] 메서드 오버로딩 (0) | 2025.11.15 |
| [공부 블로그] 모노싱글톤 (0) | 2025.11.06 |