핀수로그
  • [Android] Hilt 적용해보기
    2023년 01월 04일 22시 36분 38초에 업로드 된 글입니다.
    작성자: 핀수
    728x90
    반응형

    들어가며

    DI (Dependecy Injection)에 대해서는 어느정도 들어보거나 또는 알 것이라고 생각한다.

    한번 더 짚고 넘어가자면 의존 관계에 있는 클래스의 객체를 외부로부터 생성하여 주입받는 것을 의미한다.

     

     

    DI, 의존성 주입

    컴퓨터를 만들기 위한 클래스를 설계한다고 생각해보자.

    (getter, setter 및 생성자는 생략한다.)

    CPU가 필요하고 RAM, 저장장치(ROM)도 필요하겠지?

    public class Cpu {
        String name;
        String company;
    }

     

    Computer 클래스는 이렇게 구성된다고 치자.

    public class Computer {
        int ram;
        int ssd;
        Cpu cpu;
    
    }

    이때, Computer는 Cpu 클래스를 의존하고 있다.

    의존이란 쉽게 말해 클래스들이 서로를 알고 있는 지에 대한 관계를 의미한다.

    Computer는 Cpu를 알지만, Cpu는 Computer를 모른다.

     

        public static void main(String[] args) {
            Cpu cpu = new Cpu("i7", "Intel");
            Computer computer = new Computer(8, 512, cpu);
        }

    Computer를 인스턴스로 만들 때

    이런식으로 의존 관계에 있는 클래스의 객체를 외부에서 주입받는 것을 의존성 주입이라고 한다. 

    생성자로 전달하는 방법이 있고, 필드로 삽입하는 방법이 있다. (setter)

     

     

    나는 이 부분을 보면서 이게 왜 라이브러리까지 써야할 일이지? 라고 생각했다.

    하지만 생각해보자.

    외부에서 의존성을 주입해주어야 하는 객체가 많아진다면?

    그러니까 우리가 개발하고 있는 앱의 사이즈가 커질 수록..복잡해질수록..

    이를 개발자가 모두 컨트롤할 수는 없을 것이다.

     

    의존성 주입의 장단점

    장점

    • 의존성 주입은 인터페이스를 기반으로 설계되며, 코드를 유연하게 한다.
      • 클래스 간 의존하는 인터페이스만 알면 되기 때문에 여러 개발자가 서로 사용하는 클래스를 독립적으로 개발할 수 있다.
    • 주입하는 코드만 따로 변경하면 되니 리팩토링이 수월하다.
    • stub, mock 객체를 사용하여 단위 테스트가 수월해진다.
    • 클래스 간의 결합도를 느슨하게 한다.

    단점

    • 간단한 프로그램을 만들 때에는 번거롭다.
    • 의존성 주입은 동작과 구성을 분리해 코드를 추적하기 어렵게 하고, 가독성을 떨어뜨릴 수 있다. -> 개발자는 더 많은 파일을 참조해야만 한다.
    • Dagger2와 같은 의존성 주입 프레임워크는 컴파일 타임에 애노테이션 프로세서를 이용하여 파일을 생성하기 때문에 빌드에 시간이 좀 더 소요된다.

    무엇을 사용할까?

    관련하여 자주 보이던 것은 Dagger2, koin, Hilt가 있었다.

    어떤 차이점이 있는지 알아보자.

    Dagger2 koin Hilt
    자바와 안드로이드를 위한 강력하고 빠른 의존성 주입 프레임워크

    ✔️ 러닝커브가 가파르다. -> 환경 셋팅과 원활한 적용에 드는 비용이 다른 것에 비해 크다.
    ✔️ 컴파일 시 의존성을 주입함
    ✔️ 컴파일에 필요한 시간이 늘어난다.
    ✔️ 문제가 있으면 컴파일 시점에서 에러를 발생 시킴
    ✔️ 빌드가 완료된 파일은 어느 정도 의존성 주입에 대해서는 안정성이 보장된다고 할 수 있다.
    코틀린 개발 환경에 쉽게 적용할 수 있는 경량화 된 DI 프레임워크

    ✔️ 러닝커브가 낮다.
    ✔️ Kotlin DSL로 만들어졌다.
    ✔️ Dagger2에 비해 사용법이 쉽다.
    ✔️ 런타임에 의존성을 주입하므로 컴파일 시검에 오류 확인이 어려울 수 있다.
    Dagger를 기반으로 만들어짐

    ✔️ 러닝커브가 낮다.
    ✔️ 컴파일 시 의존성을 주입

     

    Dagger를 기반으로 만들어진 Hilt는 무엇이 다를까

    안드로이드에서 dagger를 사용하려면 안드로이드 컴포넌트가 가진 생명주기에 맞춰 객체를 주입하도록 하는 코드를 개발자가 직접 작성해야 했다. Hilt는 이 작업을 대신 처리해줌으로써 개발자가 더 편하게 의존성 주입을 할 수 있도록 도와준다.

     

     

    그래서 필자는 러닝커브가 가파르지만 안정적인 Dagger를 기반으로 만들어 진 러닝커브가 낮은 Hilt를 써보도록 할 것이다.

     

    Hilt 사용하기

    현재 개발중인 날씨 어플 'WW'에 적용하며 Hilt에 대해 배워보는 시간을 가져볼 것이다.

     

    알아두기- Dagger와 Hilt의 구성요소

    • Inject : 주입 요청, 객체 생성 방법을 안내
    • Module : 객체 생성해서 component에 전달해주도록 하는 역할
    • Component : Module이 생성한 객체를 Inject에 주입

    (이때 component는 안드로이드의 컴포넌트와는 다르다.) 

    종속 항목 추가

    1. hilt-android-gradle-plugin 플러그인을 앱 수준의 build.gradle 파일에 추가한다.

    plugins {
      ...
      id 'com.google.dagger.hilt.android' version '2.44' apply true
    }

    2. 나머지도 추가해준다.

    ...
    plugins {
      id 'kotlin-kapt'
      id 'com.google.dagger.hilt.android'
    }
    
    android {
      ...
    }
    
    dependencies {
      implementation "com.google.dagger:hilt-android:2.44"
      kapt "com.google.dagger:hilt-compiler:2.44"
    }
    
    // Allow references to generated code
    kapt {
      correctErrorTypes true
    }

    3. Hilt는 자바 8을 사용하기 때문에 자바 8을 사용 설정해준다.

    android {
      ...
      compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
      }
    }

    @HiltAndroidApp

    Hilt를 사용하는 모든 앱을 해당 주석으로 주석이 지정된 Application 클래스를 포함해야 한다.

    Hilt가 안드로이드의 생명주기에 맞춰 동작하기 때문이다.

    @HiltAndroidApp
    class WwApplication: Application() {...}

     

    @AndroidEntryPoint

    Android Component 임을 알려주고, 주입을 요청한다.

    Application 클래스에 Hilt를 설정하고 애플리케이션 수준의 구성요소를 사용할 수 있게 되면 Hilt는 @AndroidEntryPoint 주석이 있는 다른 Andorid 클래스에 종속 항목을 제공할 수 있다.

    @AndroidEntryPoint
    class CurrentLocationFragment : BaseFragment<FragmentCurrentLocationBinding>() {
    
        private val weatherViewModel: WeatherViewModel by viewModels<WeatherViewModel>()
    	...
        override fun initData() {
            ...
            weatherViewModel.getForecastLatLng(lat, lng)
        }
    }

    CurrentLocaitonFragment는 WeatherViewModel에 의존하고 있다.

    그런데 코드를 살펴보면 WeatherViewModel을 초기화 하는 부분을 찾을 수 없다.

    쉽게 생각하면 이를 Hilt가 대신 해주는 것이다.

    이게 어떻게 가능한건지 살펴보자.

     

    (위 코드에서는 주입 받는 부분에 @Inject 어노테이션이 없는데 Activity나 Fragment에서 객체를 주입받을 때에는 Android KTX의 기능을 통해 위와 같이 ViewModel을 주입 받을 수 있다.)

     

    @HiltViewModel

    ViewModel임을 알려주는 어노테이션이다.

    @HiltViewModel
    class WeatherViewModel @Inject constructor(private val repository: WeatherRepository) : ViewModel() {
    	...
        
        /**
         * 좌표를 통해 날씨 정보를 받아옵니다.
         */
        fun getCurrentWeatherLatLng(lat: Double, lng: Double) {
            disposable = repository.getCurrentWeatherLatLng(lat, lng, BuildConfig.APP_KEY, LANG).subscribe({...}))
        }
    }

    WeatherViewModel은 WeatherRepository에 의존하고 있다.

    WeatherViewModel의 객체를 생성 방법을 알려주고 WeatherRepository 주입을 요청한다.

     

    class WeatherRepository @Inject constructor(private val weatherService: WeatherService) {
    	
        fun getCurrentWeatherLatLng(
            lat: Double,
            lon: Double,
        ): Single<WeatherLatLng> {
            return weatherService.getCurrentWeatherLatLng(lat, lon, BuildConfig.APP_KEY, LANG)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
        }
    }

    WeatherRepository 객체 생성 방법을 알려준다.

     

    Interface인 WeatherService 를 초기화 하는 코드는 아래와 같았다.

    val weatherService = RetrofitInstance.getInstance().create(WeatherService::class.java)

    싱글톤으로 관리되는 객체는 어떻게 주입할 수 있을까?

     

    @Module
    @InstallIn(SingletonComponent::class)
    class RetrofitInstance {
    
        @Singleton
        @Provides
        fun getInstance(okHttpClient: OkHttpClient): Retrofit {
            return Retrofit.Builder().client(okHttpClient)
                .addConverterFactory(GsonConverterFactory.create(Gson()))
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .baseUrl(BuildConfig.BASE_URL)
                .build()
        }
    
        @Singleton
        @Provides
        fun getOkHttpClient(): OkHttpClient {
            return OkHttpClient.Builder()
                .connectTimeout(3, TimeUnit.SECONDS)
                .connectionPool(ConnectionPool(5, 50, TimeUnit.SECONDS))
                .build()
        }
    
        @Singleton
        @Provides
        fun getWeatherService(retrofit: Retrofit): WeatherService {
            return retrofit.create(WeatherService::class.java)
        }
    }

    @Module

    위에서 먼저 확인했듯 객체를 생성해 Component에 전달하는 역할을 한다. 말그대로 모듈을 생성하기 위함

    @InstallIn(SingletonComponent::class)

    module을 component로 연결하기 위해 사용한다.

    괄호 안 Component에 관한 설명을 아래에서 계속한다.

    Hilt는 어떻게 안드로이드의 생명주기에 맞춰 동작할 수 있는걸까?

    생명주기에 맞춰 정의된 Hilt의 Component들이 있고, 이 Component들이 안드로이드 생명주기에 맞춰 생기고 사라지며 주입을 해주는 것이다.

    기본적으로는 Hilt의 모든 바인딩은 범위가 지정되지 않는다. 즉, 앱이 바인딩을 요청할 때마다 Hilt는 필요한 유형의 새 인스턴스를 만드는 것이다. 하지만 Hilt는 특정 구성요소에 대한 바인딩의 범위를 지정할 수 있다.

    Hilt는 바인딩의 범위가 지정되는 구성 요소의 인스턴스 당 한 번만 범위가 지정된 바인딩을 생성하고

    해당 바인딩에 대한 모든 요청은 동일한 인스턴스를 공유한다.

    @Provides

    클래스가 외부 라이브러리에서 제공되어 클래스를 소유하지 않은 경우 또는 빌더 패턴으로 인스턴스를 생성해야하는 경우에는

    생성자 삽입이 불가능 하다.

    따라서 클래스를 직접 소유하지 않으면 Hilt 모듈 내에 함수를 생성하고 이 함수에 @Provides 어노테이션을 지정하여 해당 인스턴스를 제공하는 방법을 Hilt에 알릴 수 있다.

     

    @Singleton
    @Provides
    fun getInstance(okHttpClient: OkHttpClient): Retrofit {
        return Retrofit.Builder().client(okHttpClient)
            .addConverterFactory(GsonConverterFactory.create(Gson()))
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .baseUrl(BuildConfig.BASE_URL)
            .build()
    }

    @Provides 가 달린 함수는 Hilt에 다음 정보를 제공한다.

    • 리턴 타입은 함수가 어떤 유형의 인스턴스를 제공하는 지 Hilt에 알려줌
    • 함수의 매개변수는 해당 유형의 종속 항목을 Hilt에 알려줌
    • 함수 본문은 해당 유형의 인스턴스를 제공하는 방법을 알려줌. Hilt는 해당 유형의 인스턴스를 제공해야 할 때마다 함수 본문을 실행

    Hilt로 Module 구현 시 주의해야할 점은

    함수의 이름이 다르더라고 리턴 오브젝트 클래스가 동일하면 안된다는 것이다.

    주입 시 어떤 오브젝트를 주입해야할지 알 수 없기 때문이다.

    따라서 API URL이 여러개일 경우 이 방식으로는 힘들 수 있다.

     

    끝맺으며

    궁금했지만 기회가 닿지 않아 멀리했던 Hilt에 대해 학습할 수 있었다.

    쉽게 생각해서 나 대신 의존성을 주입해주는 Hilt라는 친구에게

    어떻게 주입하면 되는지 알려주면 되는 것 아닐..까?

    그렇지..일을 시키려면 정확하게 시켜야하니까 말이다.

    찍먹이었지만 느낄 수 있었던 점은

    객체를 주입하기 위해 아무 생각 없이 작성했던 코드(보일러플레이트)들을 줄일 수 있어 좋다고 느껴졌다.

    하지만 어떻게 보면 거의 마법같은 일(내부에서 어떤 일이 일어나는지 뜯어보지 않으면 알 수 없는 경우)이기 때문에

    주의 깊게 사용해야 할 것 같다고 느껴졌다.

     


    개인적으로 공부하며 작성한 것이므로 잘못된 정보가 있을 수 있습니다.

    틀린 정보 지적해주시면 수정하겠습니다. 🤗

    References

    아래 글을 참고하여 작성 되었습니다.

     

    '아키텍처를 알아야 앱 개발이 보인다.' 옥수환 지음

     

    아키텍처를 알아야 앱 개발이 보인다 - YES24

    설계부터 유지 보수까지 튼튼하고 유연한안드로이드 애플리케이션 만들기안드로이드 앱 시장이 성숙하고, 서비스가 고도화됨에 따라 앱 설계에 대한 중요성이 강조되고 있다. 안드로이드 앱

    www.yes24.com

     

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

    Hilt를 사용한 종속 항목 삽입 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Hilt는 프로젝트에서 종속 항목 수동 삽입을 실행하는 상용구를 줄이는 Android용

    developer.android.com

     

     

    Retrofit2, Hilt를 활용한 API 호출 사용기

    Retrofit2, Hilt, Coroutines, Flow 를 조합해서 만든 API 호출 기능 입니다.

    tejnote.github.io

     

    728x90
    반응형
    댓글