Reflex (Github Readme 번역)

2023. 12. 18. 00:44·개발툴/Unity

✅ Reflex

Unity용 DI(종속성 주입) 프레임 워크. Reflex를 사용하면 클래스를 종속성에서 독립적으로 만들어 관심사를 분리할 수 있고, 객체의 사용과 생성을 분리할 수 있다. 이는 SOLID의 종속성 반전 및 단일 책임 원칙을 따르는데 도움이 된다.

특징

  • 빠른 속도
    • VContainer 대비 약 3배 빠르며, Zenject 대비 약 7배 빠름
  • IL2CPP 친화적
    • AOT(사전 컴파일)를 지원 해서 런타임 Emit이 없고, 그래서 IL2CPP 빌드에서도 잘 작동한다.
  • GC 친화적
    • VContainer 대비 약 2배 적게, Zenject 대비 약 9배 적게 할당한다.
  • 다양한 플랫폼으로 호환 가능
    • iOS, Android, Windows/Mac/Linux, PS4/PS5, Xbox One/S/X and Xbox Series X/S, WebGL
  • 계약 테이블
    • container.All<IDisposable>과 같은 API 사용을 허용한다.
  • 불변 컨테이너
    • 성능 우수, lock 없이 스레드 안전 및 예측 가능한 동작을 제공한다.
  • 생산자 종속성 주입
  • [inject] 속성으로 프로퍼티, 필드 그리고 메소드 종속성 주입

📦 Scope

Container Scoping : 부모 컨테이너의 등록을 상속하면서 해당 컨테이너를 생성하고 확장할 수 있는 능력

Project Scope

  • root scope
  • [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] 를 기반으로 처음 씬이 열리기 직전에 생성 됨
using Reflex.Logging;
using UnityEngine;

namespace Reflex.Core
{
    public sealed class ProjectScope : MonoBehaviour
    {
        public void InstallBindings(ContainerDescriptor descriptor)
        {
            foreach (var nestedInstaller in GetComponentsInChildren<IInstaller>())
            {
                nestedInstaller.InstallBindings(descriptor);
            }

            ReflexLogger.Log($"{nameof(ProjectScope)} Bindings Installed", LogLevel.Info, gameObject);
        }
    }
}

바인딩 등록 방법

💡 Assets > Create > Reflex > ProjectScope 메뉴를 사용하면 간편하게 등록 가능하다.
  1. 프리팹을 생성한 후 “ProjectScope”로 이름 지정한 후 Resources 폴더에 넣고, “ProjectScope” 컴포넌트 추가
  2. MonoBehaviour로 installer를 만든 후 IInstaller 인터페이스 구현
  3. installer를 ProjectScope 프리팹에 붙히기
    • ProjectScope는 ProjectScope 컨테이너를 생성할때 IInstaller 인터페이스를 구현하는 모든 자식들을 검색하기 때문

주의사항

  • 예기치 못한 상황을 피하기 위해 ProjectScope는 단일이어야함
  • ProjectScope 프리팹은 필수는 아님
    • Reflex가 ProjectScope를 찾지 못하면 빈 루트가 생성된다.
  • 앱이 종료되거나 나가면 ProjectScope 인스턴스가 폐기 됨
    • 유니티에서 OnDestroy가 반드시 호출되지 않기 때문에 OnDestroy 이벤트에 종속성을 의존하면 안된다.

Scene Scope

  • ProjectScope에서 스코프(scope)되어 ProjectScope가 수행하는 모든 작업을 포함한다.
  • Awake 이후에 Start 이전에 생성되고 주입 된다.
using Reflex.Logging;
using UnityEngine;

namespace Reflex.Core
{
    public sealed class SceneScope : MonoBehaviour
    {
        public void InstallBindings(ContainerDescriptor descriptor)
        {
            foreach (var nestedInstaller in GetComponentsInChildren<IInstaller>())
            {
                nestedInstaller.InstallBindings(descriptor);
            }

            ReflexLogger.Log($"{nameof(SceneScope)} ({gameObject.scene.name}) Bindings Installed", LogLevel.Info, gameObject);
        }
    }
}

바인딩을 등록 방법

💡 Assets > Create > Reflex > Scene Context 메뉴를 사용하면 간편하게 등록 가능하다.
  1. 원하는 씬에 게임 오브젝트를 만들고 “SceneScope”로 이름 지정
  2. 루트 게임 오브젝트로 배치하고 “SceneScope” 컴포넌트 추가
  3. MonoBehaviour로 installer를 만든 후 IInstaller 인터페이스를 구현
  4. installer를 SceneScope 게임 오브젝트에 붙히기
    • SceneScope 컨테이너를 생성할 때 SceneScope가 IInstaller를 구현한 모든 하위 항목을 찾기 때문에

주의사항

  • 원치 않는 동작을 피하기 위해 단일 SceneScope만 가져야 함
  • SceneScope 프리팹은 필수는 아님
    • Reflex가 SceneScope를 찾지 못하면 비어있는 SceneScope가 생성된다.
  • 씬이 언로드되면 SceneScope 인스턴스가 폐기 됨

수동 스코핑(Manulal Scoping)

using var scopedContainer = parentContainer.Scope("Scoped", descriptor =>  
{  
  // 여기에 추가적인 등록을 하면 Scope 컨테이너를 확장할 수 있다.
});
  • Scope 메서드를 사용해서 scopedContainer라는 이름의 새로운 스코프를 만들고 있다.
  • 람다 함수 안에서 descriptor를 사용해서 스코프 컨테이너에 추가적인 등록을 할 수 있다.
  • 이 방법으로 부모 컨테이너(parentContainer)에서 상속받은 내용을 포함해서 새로운 스코프를 정의할 수 있다.

🔩 Bindings

AddSingleton(From Type)

ContainerDescriptor::AddSingleton(Type concrete, params Type[] contracts)
  • 구성할 타입(type to be constructed)과 계약(contracts)을 기반으로 객체의 지연 생성을 추가함
  • 해당하는 계약 중 하나를 처음 요청하는 순간, 객체는 게으르게(lazyli) 구성되고, 그 이후에는 항상 동일한 객체 반환
  • 만약 싱글톤이 컨테이너 빌드 후 즉시 생성되길 원한다면(none-lazyli, 지연 생성이 아닐 경우) 계약 중 하나로 typeof(IStartable)을 추가하면 된다.
  • 객체가 IDisposable을 구현한다면 부모 객체가 dispose될 때 같이 dispose된다.
  • 객체 dispose를 위해 IDisposable을 계약으로 전달할 필요는 없지만, Single<TContract>. Resolve<TContract>, All<TContract> API를 통해 모든 IDisposable을 검색하려면 지정해야 한다.

AddSingleton(From Value)

ContainerDescriptor::AddSingleton(object instance, params Type[] contracts)
  • 사용자에 의해 이미 생성된 객체를 컨테이너에 싱글톤으로 추가한다.
  • 주어진 계약이 해결되기를 요청할 때마다 동일한 객체를 반환한다.
  • 객체가 IDisposable을 구현한다면 부모 객체가 dispose될 때 같이 dispose된다.
  • 객체 dispose를 위해 IDisposable을 계약으로 전달할 필요는 없지만, Single<TContract>. Resolve<TContract>, All<TContract> API를 통해 모든 IDisposable을 검색하려면 지정해야 한다.

AddSingleton(From Factory)

ContainerDescriptor::AddSingleton<T>(Func<Container, T> factory, params Type[] contracts)
  • 주어진 팩토리와 계약에 기반해서 지연된 객체 생성을 추가한다.
  • 해당하는 계약 중 하나를 처음 요청하는 순간, 객체는 게으르게(lazyli) 구성되고, 그 이후에는 항상 동일한 객체 반환
  • 만약 싱글톤이 컨테이너 빌드 후 즉시 생성되길 원한다면(none-lazyli, 지연 생성이 아닐 경우) 계약 중 하나로 typeof(IStartable)을 추가하면 된다.
  • 객체가 IDisposable을 구현한다면 부모 객체가 dispose될 때 같이 dispose된다.
  • 객체 dispose를 위해 IDisposable을 계약으로 전달할 필요는 없지만, Single<TContract>. Resolve<TContract>, All<TContract> API를 통해 모든 IDisposable을 검색하려면 지정해야 한다.

AddTransient(From Type)

ContainerDescriptor::AddTransient(Type concrete, params Type[] contracts)
  • 구성할 타입(type to be constructed)과 계약(contracts)을 기반으로 객체의 지연 생성을 추가함
  • 이 객체는 해당하는 계약 중 하나를 처음으로 해결하려는 요청 시에만 생성되며, 그 이후에는 어떤 계약에 대한 요청이든 새 객체가 생성된다.
    • 그렇기 때문에 주의해서 사용해야 한다.
  • 객체가 IDisposable을 구현한다면 부모 객체가 dispose될 때 같이 dispose된다.
  • 객체 dispose를 위해 IDisposable을 계약으로 전달할 필요는 없지만, Single<TContract>. Resolve<TContract>, All<TContract> API를 통해 모든 IDisposable을 검색하려면 지정해야 한다.

AddTransient(From Value)

ContainerDescriptor::AddTransient(object instance, params Type[] contracts)
  • 사용자에 의해 이미 생성된 객체를 컨테이너에 일시적으로(transient) 추가한다.
  • 이 객체는 처음으로 해결될 때만 반환되며, 두 번째 해결 시에는 예외가 발생한다.
  • 객체가 IDisposable을 구현한다면 부모 객체가 dispose될 때 같이 dispose된다.
  • 객체 dispose를 위해 IDisposable을 계약으로 전달할 필요는 없지만, Single<TContract>. Resolve<TContract>, All<TContract> API를 통해 모든 IDisposable을 검색하려면 지정해야 한다.

AddTransient(From Factory)

ContainerDescriptor::AddTransient(Func<Container, T> factory, params Type[] contracts)
  • 주어진 팩토리와 계약에 기반해서 지연된 객체 생성을 추가한다.
  • 이 객체는 해당하는 계약 중 하나를 처음으로 해결하려는 요청 시에만 생성되며, 그 이후에는 어떤 계약에 대한 요청이든 새 객체가 생성된다.
    • 그렇기 때문에 주의해서 사용해야 한다.
  • 객체가 IDisposable을 구현한다면 부모 객체가 dispose될 때 같이 dispose된다.
  • 객체 dispose를 위해 IDisposable을 계약으로 전달할 필요는 없지만, Single<TContract>. Resolve<TContract>, All<TContract> API를 통해 모든 IDisposable을 검색하려면 지정해야 한다.

🔍 Resolving

생성자(Constructor)

  • 타입이 Mono가 아니라 컨테이너에 의해 생성되어야 한다면 가장 권장되는 종속성 주입 방법은 생성자 주입니다.
  • 생성자 주입은 Resolve<TContract> API에 의존하기 때문에 IInputManager 계약을 가진 두 개의 객체가 있는 경우 마지막 객체가 주입된다.🤔
private class Foo
{  
	...
  // 생성자 정의
  // inputManager: 숫자 관리에 사용되는 입력 매니저
  // managers: 여러 매니저들의 컬렉션
	public NumberManager(IInputManager inputManager, IEnumerable<IManager> managers)  
	{  
		// 생성자 내용 작성
		...
	}  
}

속성(Attribute)

  • 속성 주입은 MonoBehavior에 적합한 방법
  • 속성 주입은 Mono 클래스뿐만 아니라 일반 클래스에서도 작동한다.
  • 다음과 같이 필드, 쓰기 가능한 속성, 메서드를 주입하는데 사용할 수 있다.
class Foo : MonoBehaviour  
{  
	// 입력 매니저에 대한 의존성 주입
	[Inject] private readonly IInputManager _inputManager;  
	// 여러 매니저들에 대한 의존성 주입 (읽기 전용 속성)
	[Inject] public IEnumerable<IManager> Managers { get; private set; }  
  
	// 숫자 컬렉션에 대한 의존성 주입하는 메서드
	[Inject]  
	private void Inject(IEnumerable<int> numbers) // 메서드 이름은 중요하지 않습니다
	{  
	  ...
	}  
}

Single

  • Container::Single<TContract>는 주어진 계약을 구현하는 단일 바인딩이 있는지 실제로 확인하고 해당 객체를 반환한다.
  • 해당 계약을 구현하는 유일한(single) 바인딩이 있어야 한다고 확신하는 경우에는 모든 바인딩에 대해 권장된다.
  • 여러 개의 바인딩이 있는 경우 다음과 같은 예외가 발생할 수 있다.
// 이 예외는 시퀀스(컬렉션 또는 열거 가능한 요소 집합)에 둘 이상의 요소가 포함되어 있을 때 발생한다.
// Single 메서드를 호출하여 시퀀스에서 유일한 요소를 검색하려고 할 때 시퀀스에 여러 요소가 있다는 것을 나타낸다.
// 이 경우 Single 메서드는 정확히 하나의 요소만 있어야 하며, 그렇지 않으면 이 예외가 throw된다. 
// 문제를 해결하려면 시퀀스에서 하나의 요소만을 가져와야 함
InvalidOperationException: Sequence contains more than one element

Resolve

  • **Container::Single<TContract>**은 어떠한 유효성 검사도 수행하지 않고 주어진 계약을 구현하는 마지막 유효한 객체를 반환한다.

All

  • **Container::All<TContract>**은 주어진 계약을 구현하는 모든 객체를 반환한다.
private void Documentation_Bindings()
{
		// 컨테이너 생성 및 바인딩 예시
    var container = new ContainerDescriptor("")
        .AddSingleton(1)
        .AddSingleton(2)
        .AddSingleton(3)
        .Build();

		// 모든 int 계약을 가진 객체들을 출력
    Debug.Log(string.Join(", ", container.All<int>())); // 출력: 1, 2, 3
}
​

🎨 Decorating

  • Reflex는 ContainerDescriptor API의 AddDecorator를 통해 데코레이터 패턴을 지원한다.

인터페이스

// 숫자를 나타내는 인터페이스
public interface INumber
{
    int Get();
}

구현 클래스

// 기본 숫자 클래스
public class Number : INumber
{
    private int _value;

    // 숫자 반환 메서드
    public int Get() => _value;

    // 값을 이용하여 숫자 객체 생성하는 정적 메서드
    public static Number FromValue(int value)
    {
        return new Number
        {
            _value = value
        };
    }
}
// 숫자를 두 배로 만드는 데코레이터 클래스
public class DoubledNumber : INumber
{
    private readonly INumber _number;

    // 생성자로 기본 숫자 객체 주입
    public DoubledNumber(INumber number) => _number = number;

    // 두 배로 만든 숫자 반환 메서드
    public int Get() => _number.Get() * 2;
}
// 숫자를 반으로 나누는 데코레이터 클래스
public class HalvedNumber : INumber
{
    private readonly INumber _number;

    // 생성자로 기본 숫자 객체 주입
    public HalvedNumber(INumber number) => _number = number;

    // 반으로 나눈 숫자 반환 메서드
    public int Get() => _number.Get() / 2;
}

바인딩 설정과 사용

// 컨테이너 생성 및 바인딩 설정
var container = new ContainerDescriptor("")
    .AddSingleton(Number.FromValue(10), contracts: typeof(INumber)) // 기본 숫자 객체 바인딩
    .AddDecorator(typeof(DoubledNumber), typeof(INumber)) // 두 배로 만드는 데코레이터 바인딩
    .AddDecorator(typeof(HalvedNumber), typeof(INumber)) // 반으로 나누는 데코레이터 바인딩
    .AddDecorator(typeof(DoubledNumber), typeof(INumber)) // 두 배로 만드는 데코레이터 바인딩 (중복)
    .Build();

// 컨테이너에서 단일 INumber 객체 가져오기
var number = container.Single<INumber>();

// 결과 검증
number.Get().Should().Be(20); // 결과가 20이 되어야 함 (10X2/2X2=20)

🤙 Callbacks

ContainerDescriptor::OnContainerBuilt
  • **OnContainerBuilt**는 **ContainerDescriptor**의 인스턴스 콜백
  • 컨테이너가 완전히 구축되고 적절하게 초기화된 후에 호출된다.

🐛 Debugger

  • 메뉴에서 Reflex → Debugger로 들어갈 수 있다.

디버그 모드 활성화 방법

  • Edit → Project Settings → Player
  • Other Settings 패널에서 Script Compilation → Scripting Define Symbols로 스크롤하여 REFLEX_DEBUG를 추가
  • Reflex Debugger 창의 우측 하단에 있는 Bug 버튼을 클릭해도 된다.
💡 디버그 모드는 성능을 저하시키고 메모리 부하를 증가시키므로 신중하게 사용해야 한다.

확인 가능한 내용

  • 컨테이너 계층 구조
  • 구현
  • 계약
  • 해결 횟수
  • 바인딩 유형
  • 바인딩 할당 콜 스택

📻 Settings

  • Resources 폴더 내에 위치해야 하는 ReflexSettings scriptable object 인스턴스
  • 에셋 메뉴 항목 Assets → Create → Reflex → Settings로 생성 가능
  • 로깅 상세 수준이 구성되어 있으며 기본값은 Info로 설정되어 있다.
  • 필수는 아니지만 이 파일이 없는 프로젝트는 기본 설정을 사용하도록 되돌아간다.

📜참고자료

GitHub - gustavopsantos/Reflex: Minimal dependency injection framework for Unity

[Unity] 경량 DI 프레임워크 Reflex

Reflex, a minimal but complete DI library (Open Source)

GitHub - gustavopsantos/Reflex: Minimal dependency injection framework for Unity

'개발툴 > Unity' 카테고리의 다른 글

의존성 주입(Dependency Injection)  (1) 2023.12.18
Reflex 실습 - UniRx와 Reflex를 활용한 간단한 카운터 만들기  (0) 2023.12.18
Unity에서 ChatGPT 사용하기  (0) 2023.10.24
Unity Tilemap을 이용해서 원하는 크기의 격자맵 그리기  (0) 2023.09.30
IL2CPP  (0) 2023.09.30
'개발툴/Unity' 카테고리의 다른 글
  • 의존성 주입(Dependency Injection)
  • Reflex 실습 - UniRx와 Reflex를 활용한 간단한 카운터 만들기
  • Unity에서 ChatGPT 사용하기
  • Unity Tilemap을 이용해서 원하는 크기의 격자맵 그리기
가든_
가든_
  • 가든_
    Code Garden
    가든_
  • 전체
    오늘
    어제
    • 글 목록 (60)
      • 프로그래밍 언어 (11)
        • JAVA (0)
        • C++ (2)
        • C# (9)
      • 개발툴 (24)
        • Visual Studio (0)
        • Visual Studio Code (1)
        • Eclipse (1)
        • Unity (19)
        • Unreal (0)
        • Spring (1)
        • SpringBoot (0)
        • Vue (2)
      • 디자인 패턴 (6)
      • 백엔드 (4)
        • MySQL (1)
        • Servlet (3)
      • 프론트엔드 (4)
        • HTML (3)
        • CSS (0)
        • Javascript (1)
      • 알고리즘 (10)
        • 공식 (3)
        • 백준 (6)
        • SW Expert Academy (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    스택
    Reflex
    UniRX
    FixedUpdate
    ()=>
    컴파일 상수
    DI
    MVC
    Adaptee
    구조적 UML 다이어그램
    HTML
    Proxy 패턴
    chatGPT
    Adapter 패턴
    SetTile
    Java
    행동 UML 다이어그램
    오브젝터 어댑터
    swea2112
    런타임 상수
    클래스 어댑터
    구조패턴
    c#
    Factory 패턴
    Abstract Factory 패턴
    12738
    RDBM
    Unity
    다이어그램 그리기
    상태공간트리
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
가든_
Reflex (Github Readme 번역)
상단으로

티스토리툴바