Hilt - 지연 초기화를 사용하는 의존성 주입

KEH·2024년 12월 22일
post-thumbnail

Hilt 는 안드로이드에서 의존성을 주입할 때 많이 사용되는 의존성 주입 라이브러리 입니다.

class Bread @Inject constructor(
	egg: Egg,
    milk: Milk
) {}

@Inject 어노테이션을 사용하면 Bread 클래스에 Egg, Milk 인스턴스를 생성하여 주입할 수 있습니다.

EggMilk 인스턴스 모두 초기화되는 시점은 Bread 인스턴스가 생성될 때 입니다.

만약 EggMilk 인스턴스를 실제 Bread 객체에서 사용되는 시점에 초기화 하고 싶다면 어떻게 해야 할까요 ??

이번 글에서는 Hilt 에서 제공하는 지연 초기화 방법 을 소개합니다.

Provider

Provider@Inject 시점 시 객체를 생성하는 것이 아닌 Providerget() 을 호출했을 때 객체를 생성합니다.
따라서 주입받는 객체가 특정 상황에서만 사용될 경우 Provider 를 통해 사용되는 시점에서 인스턴스가 생성되도록 지연 초기화를 적용할 수 있습니다.

class LoginReducer @Inject constructor() : Reducer<LoginMutation, LoginUiState> { }

@HiltViewModel
class LoginViewModel @Inject constructor(
    actionProcessor: LoginActionProcessor,
    reducer: Provider<LoginReducer>,
) : BaseViewModel<LoginAction, LoginMutation, LoginUiEvent, LoginUiState>(
    actionProcessor = actionProcessor,
    reducer = reducer.get(),
    initialUiState = LoginUiState()
)

LoginViewModelLoginReducer 에 의존합니다. 이 때 Provider<T> 를 사용하여 LoginReducer 를 주입 받습니다.

this.loginReducerProvider = new SwitchingProvider<>(singletonCImpl, activityRetainedCImpl, viewModelCImpl, 12);

private static final class SwitchingProvider<T> implements Provider<T> {
	public T get() {
		switch (id) {
			case 10: // LoginViewModel
				return (T) new LoginViewModel(viewModelCImpl.loginActionProcessorProvider.get(), viewModelCImpl.loginReducerProvider);

			case 12: // LoginReducer
				return (T) new LoginReducer();
		}
	}
}

내부 코드를 확인하면 LoginViewModelreducerloginReducerProvider 이고, loginReducerProviderSwitchingProvider<LoginReducer> 입니다.

LoginViewModel 에서 reducer.get() 함수를 호출하면 SwitchingProvider<LoginReducer>get() 이 호출됩니다.

SwitchingProvider<LoginReducer>get() 함수의 case 12 일 경우 매번 LoginReducer 인스턴스를 생성하게 됩니다. 이 말은 LoginViewModel 에서 reducer.get() 을 할 때마다 매번 다른 인스턴스를 생성한다는 뜻입니다.

따라서 Provider 를 사용하면 지연 초기화를 통해 실제 객체가 필요할 때만 인스턴스를 생성할 수 있다 는 장점이 있지만 사용할 때마다 새로운 인스턴스를 생성한다 는 문제가 발생합니다.

Lazy

LazyProvider 와 마찬가지로 지연 초기화를 제공 하며 Provider 의 단점인 get() 함수 호출마다 새로운 인스턴스가 생성되는 것을 방지합니다.

class LoginReducer @Inject constructor() : Reducer<LoginMutation, LoginUiState> { }

@HiltViewModel
class LoginViewModel @Inject constructor(
    actionProcessor: LoginActionProcessor,
    reducer: Lazy<LoginReducer>,
) : BaseViewModel<LoginAction, LoginMutation, LoginUiEvent, LoginUiState>(
    actionProcessor = actionProcessor,
    reducer = reducer.get(),
    initialUiState = LoginUiState()
)

LoginViewModelLoginReducer 에 의존합니다. 이 때 Lazy<T> 를 사용하여 LoginReducer 를 주입 받습니다.

this.loginReducerProvider = new SwitchingProvider<>(singletonCImpl, activityRetainedCImpl, viewModelCImpl, 12);

private static final class SwitchingProvider<T> implements Provider<T> {
	public T get() {
		switch (id) {
			case 10: // LoginViewModel
				return (T) new LoginViewModel(viewModelCImpl.loginActionProcessorProvider.get(), DoubleCheck.lazy(viewModelCImpl.loginReducerProvider));
							
			case 12: // LoginReducer
				return (T) new LoginReducer();
		}
	}
}

public final class DoubleCheck<T> implements Provider<T>, Lazy<T> {
	private static final Object UNINITIALIZED = new Object();

	private volatile Provider<T> provider;
	private volatile Object instance = UNINITIALIZED;

	private DoubleCheck(Provider<T> provider) {
		assert provider != null;
		this.provider = provider;
	}

	@SuppressWarnings("unchecked") // cast only happens when result comes from the provider
	@Override
    public T get() {
		Object result = instance;
		if (result == UNINITIALIZED) {
			synchronized (this) {
				result = instance;
				if (result == UNINITIALIZED) {
					result = provider.get();
					instance = reentrantCheck(instance, result);
					/* Null out the reference to the provider. We are never going to need it again, so we can make it eligible for GC. */
					provider = null;
				}
			}
		}
		return (T) result;
	}
  
	//...
}

내부 코드를 살펴봅시다.

LoginViewModelreducerloginReducerProvider 이고, loginReducerProviderLazy<LoginReducer> 를 구현한 DoubleCheck<LoginReducer> 클래스입니다.

LoginViewModel 에서 reducer.get() 을 하게 되면 DoubleCheck<LoginReducer> 클래스의 get() 이 호출됩니다.

DoubleCheck 클래스의 get() 에서는 result == UNINITIALIZED 일 때에만 provider.get() 함수를 호출하여 인스턴스를 생성합니다.

따라서 LoginReducer 는 단 하나의 인스턴스만 생성할 수 있습니다.


하지만 Lazy 에서도 문제가 발생합니다.

class LoginReducer @Inject constructor() : Reducer<LoginMutation, LoginUiState> { }

@HiltViewModel
class LoginViewModel @Inject constructor(
    actionProcessor: LoginActionProcessor,
    reducer: Lazy<LoginReducer>,
) : BaseViewModel<LoginAction, LoginMutation, LoginUiEvent, LoginUiState>(
    actionProcessor = actionProcessor,
    reducer = reducer.get(),
    initialUiState = LoginUiState()
)

@HiltViewModel
class TestViewModel @Inject constructor(
    actionProcessor: LoginActionProcessor,
    reducer: Lazy<LoginReducer>,
) : BaseViewModel<LoginAction, LoginMutation, LoginUiEvent, LoginUiState>(
    actionProcessor = actionProcessor,
    reducer = reducer.get(),
    initialUiState = LoginUiState()
)

LoginViewModelTestViewModel 에서 Lazy<LoginReducer> 를 사용하여 LoginReducer 에 의존하는 코드를 작성했다고 가정해 봅시다.

this.loginReducerProvider = new SwitchingProvider<>(singletonCImpl, activityRetainedCImpl, viewModelCImpl, 12);

private static final class SwitchingProvider<T> implements Provider<T> {
	public T get() {
		switch (id) {
			case 10: // LoginViewModel
				return (T) new LoginViewModel(viewModelCImpl.loginActionProcessorProvider.get(), DoubleCheck.lazy(viewModelCImpl.loginReducerProvider));
								
			case 11: // TestViewModel
				return (T) new TestViewModel(viewModelCImpl.loginActionProcessorProvider.get(), DoubleCheck.lazy(viewModelCImpl.loginReducerProvider));
							
			case 12: // LoginReducer
				return (T) new LoginReducer();
		}
	}
}

LoginViewModelTestViewModel 에는 서로 다른 DoubleCheck<LoginReducer> 인스턴스를 갖게 됩니다.

따라서 두 DoubleCheck<LoginReducer> 에서는 각 ViewModel 에서 처음 생성된 LoginReducer 를 주입하게 되어 결론적으로 두개의 LoginReducer 인스턴스가 생성 됩니다.

개발자가 LoginReducer 인스턴스를 LoginViewModelTestViewModel 이 공유하게 하고 싶었다면 원했던 결과를 얻을 수 없을 것입니다.

Provider + Scope

Provider + Scope지연 초기화를 제공 하고, Lazy 에서 발생했던 문제를 보완하여 선언한 Scope 내에서 단 하나의 인스턴스를 모든 객체가 공유 하도록 합니다.

@Singleton
class LoginReducer @Inject constructor() : Reducer<LoginMutation, LoginUiState> { }

@HiltViewModel
class LoginViewModel @Inject constructor(
    actionProcessor: LoginActionProcessor,
    reducer: Provider<LoginReducer>,
) : BaseViewModel<LoginAction, LoginMutation, LoginUiEvent, LoginUiState>(
    actionProcessor = actionProcessor,
    reducer = reducer.get(),
    initialUiState = LoginUiState()
)

@HiltViewModel
class TestViewModel @Inject constructor(
    actionProcessor: LoginActionProcessor,
    reducer: Provider<LoginReducer>,
) : BaseViewModel<LoginAction, LoginMutation, LoginUiEvent, LoginUiState>(
    actionProcessor = actionProcessor,
    reducer = reducer.get(),
    initialUiState = LoginUiState()
)

LoginReducer 의 Scope 를 Singleton 으로 설정하고, LoginViewModelTestViewModel 에서 Provider<LoginReducer> 를 사용하여 LoginReducer 에 의존하는 코드를 작성하였습니다.

this.loginReducerProvider = DoubleCheck.provider(new SwitchingProvider<LoginReducer>(singletonCImpl, 44));

private static final class SwitchingProvider<T> implements Provider<T> {
	private final SingletonCImpl singletonCImpl;

	private final int id;

	SwitchingProvider(SingletonCImpl singletonCImpl, int id) {
		this.singletonCImpl = singletonCImpl;
        this.id = id;
	}

	@SuppressWarnings("unchecked")
	@Override
	public T get() {
		switch (id) {
			case 44: // LoginReducer
				return (T) new LoginReducer();
		}
	}
}

private static final class SwitchingProvider<T> implements Provider<T> {
	private final SingletonCImpl singletonCImpl;
	private final ActivityRetainedCImpl activityRetainedCImpl;
	private final ViewModelCImpl viewModelCImpl;
	private final int id;

	SwitchingProvider(SingletonCImpl singletonCImpl, ActivityRetainedCImpl activityRetainedCImpl, ViewModelCImpl viewModelCImpl, int id) {
		this.singletonCImpl = singletonCImpl;
        this.activityRetainedCImpl = activityRetainedCImpl;
        this.viewModelCImpl = viewModelCImpl;
        this.id = id;
	}

	@SuppressWarnings("unchecked")
	@Override
	public T get() {
		switch (id) {
        	case 21: // LoginViewModel
          		return (T) new LoginViewModel(viewModelCImpl.loginActionProcessorProvider.get(), singletonCImpl.loginReducerProvider);
                
			case 43: // TestViewModel
				return (T) new TestViewModel(viewModelCImpl.loginActionProcessorProvider.get(), singletonCImpl.loginReducerProvider);
		}
	}
}

LoginViewModelTestViewModelreducerloginReducerProvider 이고, loginReducerProviderProvider<LoginReducer> 를 구현한 DoubleCheck<LoginReducer> 클래스입니다.

Lazy 와 다른 점은 LoginViewModel 을 생성할 때 loginReducerProviderDoubleCheck<LoginReducer> 로 했다면, Provider + ScopeLoginViewModel 생성 전 이미 DoubleCheck<LoginReducer> 로 선언되어 있습니다.

이 말은 LoginViewModelTestViewModelLoginReducer 에 의존할 때 두 객체 모두 동일한 DoubleCheck<LoginReducer> 를 가진다 는 말입니다.



이번 글에서 Hilt 를 사용하여 의존성 주입 시 초기화 시점을 지연시킬 수 있는 Provider, Lazy, Provider + Scope 방법에 대해 소개하였습니다.

이 글이 효율적인 의존성 관리를 통해 더 나은 앱을 개발하는 데 도움이 되길 바랍니다. 🙏

profile
:P

0개의 댓글