Hilt 적용 후기
눈여겨보고 있던 Hilt가 공식 버전이 나옴에 따라, 나온지는 좀 됐습니다만, 적용을 해 보았습니다. Dagger 쓰다가 변경했는데 가이드 보고 따라하니 이틀만에 작업이 끝났네요. 물론 중간중간에 겪은 삽질도 많긴 합니다만, 적용하고나니 코드가 정말 깔끔해 지더군요. 개인적으로 느꼈던 장점 몇가지 적고 마주쳤던 사소한 문제들도 적어보겠습니다.
복잡한 Component 생성은 그만
Dagger에서는 의존성 주입에 사용할 객체를 생성 혹은 전달하는 Module을 생성하고, 이 Module을 탄창처럼 각 Component에 달아준 다음, 의존성 주입을 할 곳에 Component를 이용해서 의존성을 주입했습니다. 앱이 커지면 어떤 Component가 어느 Module에서 어떤 객체를 주입하는지 헷갈리기도 하고 주석으로 정리를 해 두기도 합니다만, 가끔씩 헷갈릴 때가 있습니다. 그러다 보면 버그도 자연스레 발생할 수 있습니다.
하지만 Hilt는 자체적인 Component를 만들지 않아도 됩니다. 안드로이드에서 기본적으로 제공하는 클래스에 사용할 Component가 미리 준비되어 있고, 그러한 Component는 각 Component별 Scope에서만 존재합니다. 같은 Scope에는 동일한 객체를 주입하기 때문에 해당 Scope에서 사용할 공용 객체를 생성하거나, side effect 없는 순수함수로 된 객체, 이를테면 DAO따위를 만들어서 마음껏 사용 가능합니다.
개인적으로는 이게 제일 편했습니다. Dagger 사용할 때에는 clean 한번 수행하고 나면 DaggerComponent를 참조하는 부분에서 잘못된 참조로 에러가 떴는데, Hilt에서는 어노테이션 @ 이걸로 다 처리하니 코드가 훨씬 간결하고, 가독성도 향상됩니다. 코드도 상당부분 줄어들어서 비즈니스 로직에 더 집중할 수도 있습니다. 써놓고 보니 홍보문구 같네요.
의존성 주입이 너무 쉬워졌어요
Dagger에서도 @Inject 어노테이션을 이용해서 의존성을 주입했지만, Hilt에선 자주 쓰이는 몇 가지 Predefined qualifer들도 있습니다.
class AnalyticsAdapter @Inject constructor(
@ActivityContext private val context: Context,
private val service: AnalyticsService
) { ... }
위의 코드를 보면 @Inject 어노테이션을 사용하는 생성자인데, 그 안에는 @ActivityContext 라는 미리 만들어진 qualifer, 한정자가 있습니다. 이름만 봐도 알겠지만 activity의 context입니다. 이렇게 Hilt에서 미리 준비해둔 한정자를 이용하면 adapter와 같은 클래스에 쉽고 편하게 context를 전달할 수 있습니다.
@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {
@Inject lateinit var analytics: AnalyticsAdapter
...
}
Dagger 시절에는 각각의 변수에 DaggerComponent에서 받은 객체로 초기화를 했습니다. Hilt는 Module에서 의존성 주입을 할 레퍼런스의 타입을 확인한 다음 일치하는 객체를 주입해 줍니다. 동일한 타입 여러개를 각기 다르게 생성해서 다른 변수에 주입해야 되는 경우에는 custom qualifer를 사용하면 됩니다. 사실, Hilt에서 기본적으로 제공하는 @ActivityContext 등의 어노테이션도 custom qualifer입니다.
적용해놓고 보니 장점밖에 찾을수 없었지만, 적용하는 과정에서는 이런저런 사소한 이슈가 발생했습니다. 제가 겪은 두 가지 사소한 이슈만 적어보겠습니다.
@AndroidEntryPoint 미사용 예외
Hilt로 의존성을 주입하기 위해서는 의존성을 주입할 안드로이드 클래스에 @AndroidEntryPoint 어노테이션을 적어줘야 됩니다. 프래그먼트에서 Hilt를 사용한다면 그 프래그먼트를 가지고 있는 액티비티에도 적어줘야 되고요. 한데 이런 것들을 전부 다 적어줬음에도 불구하고 entry point 어노테이션을 모두 다 적어줬냐는 예외가 발생했었습니다.
문제 해결은 간단했습니다. 모듈 수준의 gradle에서 아래 arguments에 =만 쓰여 있어서 였습니다.
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
}
}
해당 arguments에서 사용하는 Hilt의 특정한 값을 room schema 설정한답시고 전부 덮어써서 그렇게 된 것으로 보이더군요.
javaCompileOptions {
annotationProcessorOptions {
arguments += ["room.schemaLocation": "$projectDir/schemas".toString()]
}
}
아래처럼 argments에 += 를 해주고 나니 정상적으로 빌드가 되는 것을 확인했습니다.
변수 초기화시 의존성 주입 변수 참조
의존성 주입이 이루어지는 변수는 lateinit var로 선언을 해서 Hilt에서 알아서 해당 변수에 적절한 객체를 주입하게 됩니다. 한데 클래스 내부에서 이 느지막하게 초기화되는 변수를 val로 선언된 변수의 초기화에 사용하면 초기화하지 않은 변수 참조했다는 익셉션이 뜹니다. 당연한 이야기죠. 한데 이 당연한 일을 미처 생각을 못해서 실행 시점에 앱이 멈추는 것을 보고서야 발견을 했습니다. 코드로 적어놓는게 좋겠네요.
아래와 같은 DAO 변수에 의존성을 주입했습니다. 물론 다 잘 돌아갔습니다. 문제는 아래의 변수에 의존성 주입이 끝나기 전에 다른 곳에서 사용하려면 초기화가 되지 않았다고 예외가 발생하게 됩니다.
@Inject
lateinit var examDao: ExamDao
다행히도 코틀린에서는 변수 사용 시점에 초기화를 진행하는 by lazy가 있습니다. 변수 초기화를 아래와 같이 바꾸니 잘 동작했습니다.
val examPager: Pager<Int, Exam> by lazy {
Pager(
config = PagingConfig(pageSize = 10),
remoteMediator = ExamRemoteMediator(examDb, getExamService)
) {
Log.d(TAG, "pager lazy init")
examDao.pagingSource()
}
}
이런 문제는 변수에 lateinit var로 선언을 해서 발생을 합니다. 생성자에서 의존성 주입을 한 경우에는 객체 생성 시점에 Hilt에서 의존성 주입을 완료하여 by lazy를 이용하지 않고도 초기화를 할 수 있습니다. repository 클래스에 DAO 변수가 너무 많아 생성자 의존성 주입을 피했는데, 코드랩의 예제 프로젝트에선 repository 생성 시점에 DAO를 주입하고 있더군요. 아무래도 이런 이유 때문에 생성자에서 의존성을 주입한게 아닌가 싶습니다.
한글 가이드 페이지 내용이 오래됐어요
사소한 이야기입니다만, ViewModel에 Hilt를 사용하기 위해서 안드로이드 개발자 사이트의 영문 가이드는 아래와 같은 예시 코드를 제공합니다.
@HiltViewModel
class ExampleViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val repository: ExampleRepository
) : ViewModel() {
...
}
한데 언어를 한글로 바꾸면 아래와 같이 가이드를 해 줍니다. gradle에서 사용할 viewmodel용 Hilt 라이브러리 설정 가이드도 별도로 존재하네요.
class ExampleViewModel @ViewModelInject constructor(
private val repository: ExampleRepository,
@Assisted private val savedStateHandle: SavedStateHandle
) : ViewModel() {
...
}
검색을 해 보니 @ViewModelInject 어노테이션은 deprecated되었습니다. 알파 버전에서 @Inject 어노테이션으로 모두 통일이 되었는데, 한글 가이드는 아직 갱신이 되어 있지 않네요. deprecated되기 이전 버전 기준으로 Hilt 적용한 적용기에는 여전히 @ViewModelInject을 사용하고 있어서, 한글 자료 찾다 왜 안되나 찾아보고 그러다 영문 페이지로 가서 최신 내용 다시 확인하는 등 약간은 헷갈렸습니다.
지금에서야 jetpack의 여러 라이브러리들이 쏟아져 나오는 과도기인지라 모든 언어별 모든 가이드 페이지가 일관성이 없을 수도 있겠다는 생각이 듭니다. 구글이 가이드 변경사항 발생에 따라 각 언어별 번역이 즉각 필요하다는 것만 알면 내부적으로 바로 해결책을 내주리라 생각하고 지금은 이런 경험이 있었네 정도로 지나가야겠습니다.
Hilt를 적용하고 보니 적용하길 정말 잘했다는 생각이 들었습니다. 테스트 코드에는 아직 적용하지 않았지만, 유닛 테스트에 의존성 주입도 쉬워져서 테스트 작성에 신경을 더 쓸수 있게 된다고 합니다. 그에 따른 가독성, 테스트 명료성 또한 엄청나게 향상된다고 하니까 테스트에도 적용을 해봐야겠습니다.
Hilt 적용 후기에 칭찬이 많아 새로운 라이브러리의 출시 때마다 흔히 하는 립서비스인줄 알았는데, 적용해보니 립서비스가 아니라 실제로 좋아졌구나 라는 생각이 들었습니다. 적고 보니 정말 별거 아닌 내용인데 중간중간에 왜 막혔나 싶기도 하고, 가면 갈수록 배울게 많아지는데 더 열심히 해서 따라잡아야겠습니다.