예전에는 ViewModel, Repository, UseCase를 생성할 때 new로 직접 객체를 만들거나 생성자로 넘겨주었어야 함. 어떤 의존성이 필요했는지 혼동하는 등의 문제가 발생할 수 있었음.
Hilt를 사용하면?
@HiltViewModel
class TimerViewModel @Inject constructor(
private val timerUseCases: TimerUseCases,
@PrimaryTimer private val primaryTimerRepository: TimerRepository,
@SubTimer private val subTimerRepository: TimerRepository
)
이처럼 Hilt가 알아서 Repository와 UseCase를 생성해서 주입해주므로 코드가 깔끔해지고, 생성자에서 필요한 의존성을 바로 확인할 수 있음!
일반적으로 프로젝트를 진행하며 앱을 만들 때는 @singleton 어노테이션을 붙여서 모듈을 작성했다.
타이머를 한 개만 만들 때는 늘 하던 대로, 사실상 관성적으로 Singleton Scope를 활용했다. 애플리케이션 프로세스가 종료될 때 함께 destoryed 되도록 객체의 수명을 제한하고.
바인딩에는 범위가 없다고 한다.
그래서, 범위 없는 바인딩은 호출될 때마다 매번 새 객체를 반환하는데, 매번 같은 객체를 반환해야 하는 경우가 종종 있다! (나의 경우처럼…)
그럴 경우에는 Hilt의 scoped binding을 사용해서 컴포넌트가 특정 바인딩에 대해 항상 같은 객체를 반환하도록 할 수 있고, 바인딩에 scope를 부여하려면 클래스나 메소드에 특정 어노테이션을 붙이면 된다.

출처 : https://jtm0609.tistory.com/
보통 별 문제가 없다면 Singleton으로 두고 했었다.
@Module
@InstallIn(SingletonComponent::class)
abstract class TimerModule {
/* 인터페이스(TimerRepository)에 구현체(TimerRepositoryImpl)를 바인딩 */
@Binds
@Singleton
abstract fun bindTimerRepository(
timerRepositoryImpl: TimerRepositoryImpl
): TimerRepository
}
기존에 내가 구현했던 구현체는 단일 타이머 기준 한 개였기에 @Binds 어노테이션을 활용해 구현체 인스턴스 하나를 인터페이스로 묶었었다. 단일 구현체 → 단일 타입 매핑에 요긴했다.
다만, 듀얼 타이머는 같은 구현체로 구성된 2개의 인스턴스가 각각 필요했다.
그렇기에 abstact + @Binds 구조는 활용할 수 없었다. Primary Timer와 Sub Timer 각각 새 인스턴스를 생성하도록 명시해야 했기 때문이다.