공부블로그

[공부 블로그] DependencyInjector(의존성 주입기)

bimtaeur30 2026. 4. 30. 12:40

안녕하세요.
오늘은 엔진 심화 교과에서 배운 '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에 대해서 복습하고 공부해 보았습니다. 수업을 들을 때는 이해가 잘 되지 않았던 부분이었는데 복습하며 공부해 보니 이해가 잘 되어서 앞으로 프로젝트에 적극 활용해 볼 생각입니다.

감사합니다.