DI_관리_기술_선정 - boostcampwm-2024/and04-Nature-Album GitHub Wiki

💡 의존성 관리 기술 선정 개요

우리 팀원들이 각각 의존성 관리를 수동으로 구현해본 결과, 각 클래스나 모듈에서 어떤 의존성이 주입되는지 파악하기 어려운 문제가 있었다. 특히, Service Locator 패턴을 사용했던 팀원도 Hilt를 통해 의존성 주입을 자동화하면 신경 써야 할 부분이 줄어들어 코드 관리가 더 수월해진다고 언급했다. 이러한 경험을 바탕으로, 수동 의존성 주입보다는 라이브러리를 사용하여 효율적으로 관리하는 것이 유리하다고 판단했다.

따라서 팀 전체의 의존성 관리 방식을 통일하고자 기술 선정에 대한 논의를 진행하였으며, Hilt와 Koin을 비교해 최적의 의존성 관리 기술을 선정하게 되었다.

아래의 DI 관련 정리는 앞서 논의 과정 중 도윤님께서 세미나를 진행하며 정리된 내용이다.

💡 DI 관련 정리

의존성 주입을 하지 않을 경우

class A(){
	val b = B()
	// 위와 같이 진행할 경우 
	A >> 교체 하려 할 떄 B 까지 신경
	B에 의해 A가 간섭 받음 테스트도 불편함 (
}

의존성 주입할 경우

class A(b: B){
	// 위와 같이 진행할 경우 
	A >> 교체 하려 할 떄 B 까지 신경 쓰지 않아도 됨
	테스트도 용이해짐
}

수동 의존성 주입

Service Locator

  • 각 클래스가 있을 때 Service Locator에 몰아두고 Application에 연결해서 각 Class가 사용하려고 할 때 Service Locator를 통해 전달

image

class A
class B
class C

class ServiceLocator{
	private val a = A()
	private val b = B()
	private val c = C()

	fun getA() = a
	fun getB() = b
	fun getC() = c
}
class MyApplication: Application() {

    override fun onCreate() {
        super.onCreate()

        serviceLocator = ServiceLocator()
    }

    companion object {
		    // 싱글톤 생략
		    
        lateinit var serviceLocator: ServiceLocator
    }
}
val testClass = TestClass(MyApplication.serviceLocator.getA()) // 직접 주입

단점

  1. 의존성 주입이 필요한 클래스가 많아지면 위와 같은 코드가 많아지고, 관리가 어려워진다. (수동)
    1. 어디서 어떻게 주입되는지 알기 어려워지는 경우가 생김
  2. 테스트가 어렵다.
  3. Service Locator가 갖고 있는 다른 클래스들에 대한 의존이 함께 발생
  4. 런타임에서 오류 가능성

자동 의존성 주입

Hilt

  • 컴파일 과정에서 의존성 주입 검증이 일어나기 때문에 컴파일 과정에서 에러 발생
  • 어플리케이션 시작 시 단 한 번만 반영

장점

  • LifeCycle 관리
  • 의존 관계 파악 용이
  • 보일러플레이트 감소
  • 테스트 용이

단점

  • 컴파일 속도 저하(어노테이션 사용)
  • Stub 파일 생성으로 인한 빌드 시간 증가
@HiltAndroidApp
class MyApplication: Application(){}
@AndroidEntryPoint
class Activity: AppCompatActivity(){

}
@HiltViewModel
class MyViewModel: ViewModel @Inject constructor(
	private val repo: AnalyticsService,
	private val repo2: AnalyticsService
){

}
@Module
@InstallIn(ActivityComponent::class)
object AnalyticsModule {

  @Provides
  fun provideAnalyticsService(
    // Potential dependencies of this type
  ): AnalyticsService {
      return Retrofit.Builder()
               .baseUrl("https://example.com")
               .build()
               .create(AnalyticsService::class.java)
  }
  
  @SameReturnValue
  @Provides
  fun provideAnalyticsService2(
    // Potential dependencies of this type
  ): AnalyticsService {
      return Retrofit.Builder()
               .baseUrl("https://example.com2")
               .build()
               .create(AnalyticsService::class.java)
  }
}

@Module
@InstallIn(ActivityComponent::class)
abstract class AnalyticsModule {

  @Binds
  abstract fun bindAnalyticsService(
    analyticsServiceImpl: AnalyticsServiceImpl
  ): AnalyticsService
}

interface AnalyticsService {
  fun analyticsMethods()
}

class AnalyticsServiceImpl @Inject constructor(
  private val repo: AnalyticsService 
) : AnalyticsService { ... }
@Qualifier
annotation class SameReturnValue
  • provideAnalyticsService 처럼 반환값이 같은 경우에는 1개만 주입된다. 이때 @Qualifier 를 사용하여 구분 지어 의존성 주입될 수 있다.

Hilt를 사용한 종속 항목 삽입  |  Android Developers

Koin

  • Service Locator 기반
  • 문제가 있을 경우 런타임에서 에러 발생
  • 유연성 증가 >> 동적으로 코드 변경 또는 확장 가능
  • 컴파일 시간 단축 (어노테이션 사용 x)
  • *리플렉션을 사용하기 때문에 성능이 저하 될 수 있음

[Why Koin? | Koin](https://insert-koin.io/docs/setup/why/)

💡 Hilt를 선택한 이유

  1. 빠른 시간 내에 프로젝트를 개발하고 도입해야 하는 우리 팀의 요구 사항에는 Koin이 적합해 보였으나, 장기적인 유지보수와 안정성을 고려했을 때 Hilt가 더 유리하다고 판단하였다.
  2. Koin은 런타임에서 오류가 발생할 가능성이 높아, 프로젝트 관리와 유지보수가 어려워질 수 있다.

따라서, 우리 팀은 Hilt를 선택하여 의존성 관리를 통일하기로 결정하였다. Hilt를 사용함으로써 코드의 복잡성을 줄이고, 유지보수성을 높이며, 프로젝트의 안정성을 보장할 수 있을 것이다.


*리플렉