koin 에서 hilt 로 이전하기

김종식
원티드랩 기술 블로그
10 min readJul 2, 2021
정식 출시를 기다리고 있던 친구 중 하나입니다 :)

원티드 안드로이드 앱은 DI 프레임워크로 Koin 을 사용중이었는데요, 6.6.0 출시 버전부터 Hilt 가 도입되었습니다. 새로운 기술을 실제 프로덕트에 적용하여 기술적, 경험적으로 성장하고자 하는 니즈와 Jetpack AAC 를 활용하여 프로덕트를 개발하고 있기 때문에 Hilt 를 사용하는 것이 장기적으로 필요할 것이라는 공감대가 있어, 작은 범위부터 시작하여 순차적으로 적용하는 식으로 전환하는 것을 목표로 시작하였습니다. 이번 출시버전에는 ‘익명투표 두런두런’ 기능에 반영이 이루어 졌고, 앞으로 신규 프로젝트나 기존 코드에서 변환 작업을 계속 진행할 예정입니다.

hilt 는 어떤 장점이 있을까?

Hilt 는 Dagger2 기반으로 안드로이드 프레임워크에서 표준적으로 사용되는 DI 컴포넌트 및 스코프를 기본적으로 제공합니다. DI 는 Dependency Injection (의존성 주입) 의 약자로, 외부에서 의존 객체를 생성하여 전달하는 것을 의미합니다. 의존성 주입에 대하여 잘 고려하여 설계 및 코드를 작성한다면, 소스코드의 유지보수가 크게 용이해지고 효율적으로 테스트코드 작성이 가능해지는 장점이 있는데요, 좀 더 자세한 내용은 Hilt 의 종속성 주입 개요 섹션을 추천 드립니다.

안드로이드 개발에서 DI 를 제공하는 도구들은 몇 가지가 있습니다. 크게 생각나는 것은 아래와 같습니다. 각 라이브러리에 대한 차이 및 비교는 Dagger, Koin, Hilt 는 어떤 차이가 있는지 소개하는 글을 추천 드립니다.

Koin 에서 Hilt 로 변경 후 개인적으로 좋았던 점은, 유저는 더 빠르게 서비스를 이용할 수 있는 것과 개발자는 더 안정적으로 서비스를 제공이 가능해졌다는 부분입니다. Dagger 기반으로 동작방식, 즉 어노테이션에 따라 컴파일 타임에 Generated Code 로 의존 객체 주입이 실행되므로 런타임 퍼포먼스에 영향을 주지 않으며, 기존 Dagger 의 단점이었던 러닝커브가 높고 안드로이드 프레임워크 디펜던시한 요소를 주입하기 위하여 필요했던 많은 보일러플레이트코드를 생성하지 않아도 되어, 다른 개발자 분들도 손쉽게 적응을 할 수 있었습니다. 또한, Koin 을 어느정도 사용하다보니 의존성 주입 시 파라미터를 누락하였을 때 출시 후 예상치 못한 곳에서 사이드이펙트가 발생하곤 했는데요, 이러한 부분을 이제는 컴파일타임에서 체크가 되어 더욱 더 안정성 있게 의존객체 주입이 가능해졌다고 생각합니다.

get(), get(), get() … 그땐 어쩔수 없었지만, 이제 안녕

Hilt 에 대한 종속 항목 삽입은 공식 문서Hilt - Jetpack 통합 문서를 추천 드립니다.

Hilt 적용 상세 내용 및 Trouble-Shooting

Fragment Transition Animation 리소스 참조 오류

Jetpack Navigation 으로 구성되어있는 요소 중 화면 전환 시 효과로 지정되어있는 리소스 참조가 더 이상 되지 않는 현상이 있었습니다. Hilt 의 경우 2.36버전을 추가 하였는데, 해당 버전의 컴파일 디펜던시 중 fragment 버전이 기존 사용하던 버전과 차이가 있어 발생되는 현상이었습니다.

해당 이슈는 힐트 추가 이전의 애니메이션 효과를 정의한 리소스를 애플리케이션 모듈에 별도 추가를 하여 큰 어려움 없이 문제를 해결할 수 있었습니다.

ViewModel 생성 시 Activity, Fragment argument 인자 처리

Koin 으로 객체를 얻어올 때 parametersOf 를 활용하여, module 에서 뷰 모델 객체 전달 시 별도 생성자를 직접 추가하여 생성이 가능합니다.

AAC ViewModel 은 ViewModelProvider 를 활용하면 생성자로 의존정보를 주입할 수 있습니다. Hilt 를 이용할 경우 Activity, Fragment 실행시 전달되는 argument 를 별도 생성자로 전달하기 위하여 Assisted 활용 가능하지만, 뷰모델 객체를 생성하기 위한 보일러플레이트 코드가 상당히 증가될 것이라는 우려가 있었습니다.

뷰 모델 너에게 id 하나를 보내어본다 …

Hilt 는 뷰모델 생성 시 SavedStateHandle 을 주입받을 수 있도록 제공하여 손쉽게 Activity, Fragment 생성시 전달되는 argument 를 뷰모델에서 이용 가능합니다. 따라서, Activity / Fragment 실행 시 전달되는 argument 는 SavedStateHandle 객체를 활용하는 것으로 결정했습니다.

저는 A요, 저두요

ViewModel > Context 의존 주입 이슈

ViewModel 에서 Context가 필요할 경우, Hilt 에서는 ApplicationContext 식별자를 활용하여 ApplicationContext 객체를 주입 받을 수 있습니다. 하지만, 이 경우 지역설정 변경, 테마 변경 등 configuration changes 이벤트 발생 시, ApplicationContext 로부터 getString / getColor 등을 호출한다면, 변경 이전의 Context 정보로 값이 로드되는 이슈가 있습니다. (사실, Android ViewModel 에서 Context 를 참조하는 것은 좋지 않습니다.)

원티드 앱 서비스에서는 서비스 지역 변경 옵션이 제공되는데요, 위에서 소개드린 이슈때문에 Koin 을 이용하여 ViewModel 을 생성할 경우에는 Activity Context 활용한 Delegate 객체를 생성자로 전달하고 있었습니다. 또 한가지는, 테스트코드 작성 시 뷰모델에서 뷰에 필요한 상태 정보 중 다국어 리소스에 선언된 값의 경우 실제 xml 상의 리소스가 어떤 값인지 테스트코드를 작성하는 것도 중요했습니다. 원티드에서는 lokalise 라는 도구를 이용하여 번역 업무 수행 및 빌드 시 lokalise 에서 제공하는 API를 통해 strings.xml 파일을 갱신 후 빌드하기 때문에, 번역값이 변경될 경우 다른 화면에서 사용중인 문구가 함께 변경되어 사이드이펙트가 발생되는 것을 방어하기 위한 목적으로 테스트코드가 필요하다고 판단하고 있었던 상황이었습니다.

Koin 을 사용할 때는, Activity / Fragment 에서 ContextDelegate 객체를 생성및 전달하여 ViewModel 객체를 주입받도록 하여 위 제약을 해결하였습니다.

Context없는 안드로이드에서 살고싶다.

하지만, Hilt 를 적용할 경우에는 ViewModel 생성 할 때 Activity Context를 주입시키려면 위에서 소개드린 것 처럼 ViewModelProvider 를 생성해야 하는데, 불필요한 보일러플레이트 코드 생성도 좋지 않아 Hilt 가 추가된 ViewModel 에서는 리소스 getString 결과를 리턴하지 않고, Resource Id 만 전달하도록 변경하는 것으로 결정하였습니다. (점점 작업량이 많아지고…)

뷰에 온전하게 표시되어야 하는 값을 ViewModel 에서 처리하고 있는 케이스가 생각보다 복잡했습니다. 이번 작업 범위에서 확인된 케이스는 대략 아래 정도입니다.

  • null value (뷰에 null 을 설정활 때와, “” 을 설정할 때 동작이 다른 케이스)
  • text value (서버에서 응답 받은 텍스트를 그대로 사용)
  • text resource value (앱 내 참조 리소스를 그대로 사용, *formatArgs 포함)
  • text + text resource formatted value (앱 내 참조 리소스를 포멧으로, 서버 응답과 혼용하여 사용)
  • color resource value (앱 내 참조 리소스 컬러)
  • color text formatted value (#RRGGBB 텍스트를 컬러로 변환하는 경우)

위와 같은 케이스를 View 로 전달하기 위하여, ValueWrapper 라는 sealed class 로 정의 및 View 로 해당 타입의 객체를 생성하여 전달, View 에서는 Context.get 확장함수로 각 케이스별 실제 뷰에 표현되어야 하는 값을 가져오도록 하여 기존 코드의 최소한의 수정을 통해 기존에 주입되어있던 ViewModel 에서 context 를 제거하였습니다.

observe 는 내부에서 사용중인 LiveData observing 처리 extension 함수입니다.
Context없는 안드로이드에서 살고싶다. 2

자동으로 업데이트되는 다국어 리소스들이 실제로 변경되었는지 테스트되어야 하는 부분은 별도 테스트파일로 작성해야 할 것 같습니다. 조금 아쉬움이 있는 부분이며, ViewModel 과 Context 관계에서 고민했던 내용은 별도로 한번 정리해 봐도 좋겠다는 생각이 들었습니다.

처음 DI 도입을 검토할 때, Dagger 와 Koin 중 어떤 것을 도입할지 고민했었던 것 같습니다. Koin 으로 이미 DI 가 구축되어 있다면, Hilt 로 변경하는 것을 적극 추천드립니다. 이미 DI에 대한 이해도가 있으실 것으로 예상되며, Hilt 는 Dagger 보다 안드로이드 구성요소를 좀 더 쉽게 사용할 수 있도록 잘 제공되고 Dagger 를 시작하기 전에 두려웠었던 러닝커브도 상당히 낮다고 생각됐습니다. 또, Jetpack 의 다른 구성요소 와도 잘 호환되기 때문입니다.

개인적으로 좋았던 것은, 기존에 DI 로 구성되어있던 것을 새로 구축할 때는 Koin으로 구성했을 때의 아쉬웠던 부분이나 좀 더 의존관계를 명확하게 module 로 분리하기 위해 좀 더 고민해서 적용함으로써, 장기적으로 안정적으로 서비스를 제공할 수 있게 되어서 좋았습니다.

--

--