Hilt 는 안드로이드에서 의존성을 주입할 때 많이 사용되는 의존성 주입 라이브러리 입니다.
class Bread @Inject constructor(
egg: Egg,
milk: Milk
) {}
@Inject 어노테이션을 사용하면 Bread 클래스에 Egg, Milk 인스턴스를 생성하여 주입할 수 있습니다.
Egg 와 Milk 인스턴스 모두 초기화되는 시점은 Bread 인스턴스가 생성될 때 입니다.
만약 Egg 와 Milk 인스턴스를 실제 Bread 객체에서 사용되는 시점에 초기화 하고 싶다면 어떻게 해야 할까요 ??
이번 글에서는 Hilt 에서 제공하는 지연 초기화 방법 을 소개합니다.
Provider 는 @Inject 시점 시 객체를 생성하는 것이 아닌 Provider 의 get() 을 호출했을 때 객체를 생성합니다.
따라서 주입받는 객체가 특정 상황에서만 사용될 경우 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()
)
LoginViewModel 은 LoginReducer 에 의존합니다. 이 때 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();
}
}
}
내부 코드를 확인하면 LoginViewModel 의 reducer 는 loginReducerProvider 이고, loginReducerProvider 는 SwitchingProvider<LoginReducer> 입니다.
LoginViewModel 에서 reducer.get() 함수를 호출하면 SwitchingProvider<LoginReducer> 의 get() 이 호출됩니다.
SwitchingProvider<LoginReducer> 의 get() 함수의 case 12 일 경우 매번 LoginReducer 인스턴스를 생성하게 됩니다. 이 말은 LoginViewModel 에서 reducer.get() 을 할 때마다 매번 다른 인스턴스를 생성한다는 뜻입니다.
따라서 Provider 를 사용하면 지연 초기화를 통해 실제 객체가 필요할 때만 인스턴스를 생성할 수 있다 는 장점이 있지만 사용할 때마다 새로운 인스턴스를 생성한다 는 문제가 발생합니다.
Lazy 는 Provider 와 마찬가지로 지연 초기화를 제공 하며 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()
)
LoginViewModel 은 LoginReducer 에 의존합니다. 이 때 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;
}
//...
}
내부 코드를 살펴봅시다.
LoginViewModel 의 reducer 는 loginReducerProvider 이고, loginReducerProvider 는 Lazy<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()
)
LoginViewModel 과 TestViewModel 에서 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();
}
}
}
LoginViewModel 과 TestViewModel 에는 서로 다른 DoubleCheck<LoginReducer> 인스턴스를 갖게 됩니다.
따라서 두 DoubleCheck<LoginReducer> 에서는 각 ViewModel 에서 처음 생성된 LoginReducer 를 주입하게 되어 결론적으로 두개의 LoginReducer 인스턴스가 생성 됩니다.
개발자가 LoginReducer 인스턴스를 LoginViewModel 과 TestViewModel 이 공유하게 하고 싶었다면 원했던 결과를 얻을 수 없을 것입니다.
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 으로 설정하고, LoginViewModel 과 TestViewModel 에서 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);
}
}
}
LoginViewModel 과 TestViewModel 의 reducer 는 loginReducerProvider 이고, loginReducerProvider 는 Provider<LoginReducer> 를 구현한 DoubleCheck<LoginReducer> 클래스입니다.
Lazy 와 다른 점은 LoginViewModel 을 생성할 때 loginReducerProvider 를 DoubleCheck<LoginReducer> 로 했다면, Provider + Scope 는 LoginViewModel 생성 전 이미 DoubleCheck<LoginReducer> 로 선언되어 있습니다.
이 말은 LoginViewModel 과 TestViewModel 이 LoginReducer 에 의존할 때 두 객체 모두 동일한 DoubleCheck<LoginReducer> 를 가진다 는 말입니다.
이번 글에서 Hilt 를 사용하여 의존성 주입 시 초기화 시점을 지연시킬 수 있는 Provider, Lazy, Provider + Scope 방법에 대해 소개하였습니다.
이 글이 효율적인 의존성 관리를 통해 더 나은 앱을 개발하는 데 도움이 되길 바랍니다. 🙏