티스토리 뷰

728x90

2022.04.28 - [안드로이드] - 도로명 주소 개발센터 API 사용해보기1 : Api활용 신청, Retrofit

 

도로명 주소 개발센터 API 사용해보기1 : Api활용 신청, Retrofit

앱을 개발을 하다 보면 주소를 받아야 하는 경우가 종종 있습니다. 저 같은 경우 웹뷰를 이용한 다음 우편 번호 서비스를 이용해왔습니다. 이 방식을 이용하면 개발자 입장에서는 진짜 편하게

alanboyce.tistory.com

 

Hilt, MVVM, Coroutine 다 적용할 예정인데 하나 하나가 다 분량이 좀 될 것 같아서 분리해서 알아보고 합쳐서 최종판(?)을 업로드하는 방향으로 진행해보려 합니다.
이번 글에서는 Hilt에 대해 다뤄봅니다.

1. DI (Dependency Injection)

우선 Hilt는 DI(의존성 주입)을 위해 사용하는 라이브러리 중 하나입니다.
이 부분을 이해하기 위해선 그 전에 의존성 Dependency에 대해 알아야 합니다.

1-1. Dependency

의존성이란 A가 B를 의존할 때 의존 대상 B가 변하면, 그것이 A에 영향을 미친다
라고 합니다. (참고자료에서 가져온 것으로 글 하단에 링크 첨부하였습니다 참고하시면 좋아요~ㅎ)

예제를 들면 

출처 : https://blog.naver.com/kmk265988/220865384178

포켓몬 게임에 보시면 위와 같이 포켓몬들은 각자 4개의 스킬을 가지고 있습니다.

class Pikachu {
    val skillA = Pair("전기", 60)
    val skillB = Pair("노말", 30)
}

간단하게 2개만 해서 구현을 한다고 하면 위와 같이 할 수도 있습니다.
이렇게 구현하게 되면 피카츄는 2가지의 스킬을 문제없이 가질 수 있지만
간단히 보면 3가지의 문제가 발생할 수 있습니다.

  • 위의 방식은 피카츄에 스킬을 직접 구현했기 때문에 전기 스킬을 가진 다른 포켓몬이 있다면 또다시 구현해야 합니다.
  • 또한 전기기술의 대미지를 60이 아닌 50으로 조정하라고 지시가 내려왔을 때 모든 전기 스킬을 가진 포켓몬을 조사하여 수정해야 합니다.
  • 여러 마리의 포켓몬을 다루다 보니 같은 스킬인데 다른 대미지를 가지고 있을 가능성이 생깁니다. (실수할 가능성)
abstract class Pokemon {
    abstract var skillA : Skill?
    abstract var skillB : Skill?
}

interface Skill {
    fun attack() : Pair<String, Int>
}

class ElectricitySkill : Skill {
    override fun attack() : Pair<String, Int> {
        val type : String = "전기"
        val damage : Int = 60
        return Pair(type, damage)
    }
}

class NormalSkill : Skill {
    override fun attack(): Pair<String, Int> {
        val type : String = "노말"
        val damage : Int = 30
        return Pair(type, damage)
    }
}

class Pikachu : Pokemon() {
    override var skillA: Skill? = ElectricitySkill()

    override var skillB: Skill? = NormalSkill()
}

이번에는 코드 양은 많이 증가했지만

피카츄가 몬스터를 상속받게 되었고 스킬들은 Skill이란 인터페이스에 의존하게 됩니다.

위에서 구현한 방식과의 차이점이라고 하면

Skill을 상속받은 클래스들은 어떤 포켓몬이 사용할지 알 필요가 없습니다.
각자의 특성에 맞게 type과 damage를 만들어 주면 끝입니다.

피카츄를 만들 때에는 스킬이 어떻게 구현되어있는지 알 필요가 없습니다.
다만, Skill을 가지고 있어야 하기 때문에 독립적이진 못하고 Skill에 의존하게 됩니다.

1-2 의존성 주입

의존성 주입은 외부에서 의존성을 설정해주는 것을 의미합니다.

위에서 피카츄가 스킬에 의존성 관계를 가진다고 하면
의존성 주입은 트레이너가 피카츄에게 스킬을 가르치는 것을 의미합니다.

class Trainer(private val pokemon: Pokemon) {

    fun pokemonSkillAChange(skill : Skill){
        pokemon.skillA = skill
    }

    fun pokemonSkillBChange(skill : Skill) {
        pokemon.skillB = skill
    }

}
val trainer = Trainer(pokemon = Pikachu())
trainer.apply {
    pokemonSkillAChange(GhostSkill())
    pokemonSkillBChange(FireSkill())
}

좀 더 실제 안드로이드 개발에서 사용할만한 예제를 보고 싶으시다면

https://developer.android.com/training/dependency-injection/manual?hl=ko 

 

수동 종속성 삽입  |  Android 개발자  |  Android Developers

수동 종속성 삽입 Android의 권장 앱 아키텍처는 코드를 클래스로 분할하여 관심사 분리의 이점을 누리길 권장합니다. 관심사 분리는 정의된 단일 책임이 계층 구조의 각 클래스에 있는 원칙입니

developer.android.com

위 글을 참조해보시기 바랍니다~

 

2.  Hilt Library 등록

Hilt와 Coroutine을 사용하기 위해 app 수준의 build.gradle에 다음 내용을 추가해줍니다.

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    id 'kotlin-kapt'
    id 'kotlin-parcelize'
    id 'dagger.hilt.android.plugin'
}

... 

dependencies {
    ...

    // Coroutine
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.4.3"

    // hilt
    implementation "com.google.dagger:hilt-android:2.38.1"
    kapt "com.google.dagger:hilt-android-compiler:2.38.1"
    implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha03'
    kapt 'androidx.hilt:hilt-compiler:1.0.0'

    def lifecycle_version = "2.4.1"
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
    implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"

    // KTX
    implementation "org.jetbrains.kotlin:kotlin-reflect:1.6.21"
    implementation 'androidx.activity:activity-ktx:1.4.0'
    implementation 'androidx.fragment:fragment-ktx:1.4.1'

    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

project 수준의 build.gradle

buildscript {
    dependencies {
        classpath 'com.google.dagger:hilt-android-gradle-plugin:2.38.1'
    }
}

 

3. @HiltAndroidApp

Hilt를 사용하기 위해서 가장 먼저 해야 할 일은 Application()을 상속받고 @HiltAndroidApp의 annotation을 추가한 클래스를 AndroidManifest.xml에 알리는 것입니다.

@HiltAndroidApp
class StandardStudy : Application() {

    companion object {
        private lateinit var application : StandardStudy
        fun getInstance() : StandardStudy = application
    }

    override fun onCreate() {
        super.onCreate()
        application = this
    }

}

저는 주로 앱 이름과 클래스 명을 동일하게 하고 있습니다.

<application
    android:name=".StandardStudy"
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/Theme.StandardStudy">

AndroidManifest.xml의 application에 name에 위에서 만든 클래스 명을 넣어줍니다.

@HIltAndoirdApp의 역할은 다음과 같습니다.

애플리케이션 수준 종속 항목 컨테이너 역할을 하는 애플리케이션의 기본 클래스를 비롯하여 Hilt의 코드 생성을 트리거합니다.
생성된 이 Hilt 구성요소는 Application 객체의 수명 주기에 연결되며 이와 관련한 종속 항목을 제공합니다. 또한 이는 앱의 상위 구성요소이므로 다른 구성요소는 이 상위 구성요소에서 제공하는 종속 항목에 액세스 할 수 있습니다.

 

4. @AndroidEntroyPoint

@AndroidEntryPoint
class CoroutineActivity : AppCompatActivity()

사용 방법은 위와 같이 사용하시면 됩니다

사용할 수 있는 범위는 아래와 같습니다.

  • Application(@HiltAndroidApp을 사용하여)
  • Activity
  • Fragment
  • View
  • Service
  • BroadcastReceiver

단, Activity의 경우 AppComatActivity와 같은 ComponentActivity를 확장해야 하고
Fragment의 경우 androidx.Fragment를 확장하고 있어야 합니다.

@AndroidEntryPoint는 프로젝트의 각 Android 클래스에 관한 개별 Hilt 구성요소를 생성합니다. 이러한 구성요소는 구성요소 계층 구조에 설명된 대로 각 상위 클래스에서 종속 항목을 받을 수 있습니다.
구성요소에서 종속 항목을 가져오려면 다음과 같이 @Inject 주석을 사용하여 필드 삽입을 실행합니다.

Hilt 사용 준비를 완료하였으니 이제 Inject 하기 위해 의존성 객체를 만들어 봅시다

class Trainer @Inject constructor() {

    private lateinit var pokemon: Pokemon

    fun newPokemon(pokemon: Pokemon) {
        this.pokemon = pokemon
    }

    fun pokemonSkillAChange(skill : Skill){
        pokemon.skillA = skill
    }

    fun pokemonSkillBChange(skill : Skill) {
        pokemon.skillB = skill
    }

    fun state() : String {
        if (!this::pokemon.isInitialized) return "포켓몬을 생성해주세요"

        val skillA = pokemon.skillA?.attack()
        val skillB = pokemon.skillB?.attack()

        return "이름 : ${pokemon.name}\n" +
                "A 스킬 : ${skillA?.first} / ${skillA?.second}\n" +
                "B 스킬 : ${skillB?.first} / ${skillB?.second}\n"
    }

}

class User @Inject constructor(
    private val trainer: Trainer
) {

    fun getNewPokemon(pokemon: Pokemon) {
        trainer.newPokemon(pokemon)
    }

    fun pokemonSkillAChange(skill : Skill){
        trainer.pokemonSkillAChange(skill)
    }

    fun pokemonSkillBChange(skill : Skill) {
        trainer.pokemonSkillBChange(skill)
    }

    fun pokemonState(state : (String) -> Unit) {
        state(trainer.state())
    }

}

DI예제에서 만든 클래스에서 Pokemon()에서는 단순히 name만 추가하였고

Trainer 클래스는 @Inject constructor()와 newPokemon(), state() 메소드가 추가되었습니다.
메소드들의 경우는 단순한 기능이므로 생략하겠습니다.

@Inject constrcotor()을 추가해줌으로써 누군가에게 주입될 수 있는 클래스가 됩니다.
또한 누군가에게 호출이 이루어지면

왼쪽 아이콘이 추가되게 됩니다.

주입을 받은 대상의 경우에는 2번째 줄의 아이콘이 추가됩니다.

해당 아이콘을 클릭하면 주입받은 대상 / 주입한 대상으로 이동이 가능합니다.

여기서 알 수 있듯이 User 클래스는 생성할 때 Trainer를 주입을 받습니다.
위에 DI 예제에서 트레이너가 포켓몬을 주입할 때에는 직접 생성자에서 포켓몬을 생성해서 넣어주었다면
@Inject를 사용하면 빌드 시에 Hilt가 내부적으로 클래스를 생성하여 의존성을 주입하게 됩니다.

@AndroidEntryPoint
class HiltActivity : AppCompatActivity() {

    @Inject lateinit var user: User

    private val binding : ActivityHiltBinding by lazy { ActivityHiltBinding.inflate(layoutInflater) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)

        user.apply {
            getNewPokemon(Pikachu())
            pokemonSkillAChange(GhostSkill())
            pokemonSkillBChange(FireSkill())
            pokemonState {
                binding.textView.text = it
            }
        }
    }
}

만든 User클래스를 @Inject lateinit var user: User로 주입받아서 생성합니다.
주의해야 할 점은 주입받은 필드는 private를 사용할 수 없습니다.

이 정도 하면 기본적인 Hilt사용은 끝입니다.
근데 개발을 하다 보면 외부 라이브러리를 이용하는 경우가 많이 있을 겁니다.
직접 구현한 클래스가 아니므로 @Inject를 넣어줄 방법이 기본적으로 없겠죠?

그래서 Hilt는 Module이라는 것을 지원합니다.

본 예제인 도로명 주소 개발센터 API 사용하기 쪽에서 사용하던 retrofit입니다.

object RetrofitUtil {

    val service : AddressService by lazy { getRetrofit().create(AddressService::class.java) }

    private fun getRetrofit() : Retrofit =
        Retrofit.Builder()
            // 공통으로 사용하는 주소
            .baseUrl(Constants.ADDRESS_API_BASE_URL)
            // OkHttpClient 등록
            .client(getOkHttpClient())
            // ConverterFactory 등록
            .addConverterFactory(getGsonConvertFactory())
            .build()

    // 각종 통신에 관련한 설정
    private fun getOkHttpClient() : OkHttpClient =
        OkHttpClient.Builder()
            .readTimeout(10, TimeUnit.SECONDS)
            .connectTimeout(10, TimeUnit.SECONDS)
            .writeTimeout(15, TimeUnit.SECONDS)
            .addInterceptor(getLoggingInterceptor())
            .build()

    // retrofit 에 관련한 로그를 볼 수 있는 범위 지정
    private fun getLoggingInterceptor() : HttpLoggingInterceptor =
        HttpLoggingInterceptor().apply {
            level = if(BuildConfig.DEBUG) {
                HttpLoggingInterceptor.Level.BODY
            } else {
                HttpLoggingInterceptor.Level.NONE
            }
        }

    // 서버로부터 받은 GSON 결과를 클래스에 맞게 변환해줍니다.
    private fun getGsonConvertFactory() : GsonConverterFactory = GsonConverterFactory.create()

}

기존 방식에서는 위와 같이 만들어서 사용했습니다.

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
    @Provides
    @Singleton
    fun provideOkHttpClient(
        loggingInterceptor: HttpLoggingInterceptor
    ) : OkHttpClient =
        OkHttpClient.Builder()
            .readTimeout(10, TimeUnit.SECONDS)
            .connectTimeout(10, TimeUnit.SECONDS)
            .writeTimeout(15, TimeUnit.SECONDS)
            .addInterceptor(loggingInterceptor)
            .build()

    @Provides
    @Singleton
    fun provideGsonConvertFactory() : GsonConverterFactory = GsonConverterFactory.create()

    @Provides
    @Singleton
    fun provideRetrofit(
        okHttpClient: OkHttpClient,
        gsonConverterFactory: GsonConverterFactory
    ) : Retrofit =
        Retrofit.Builder()
            .baseUrl(Constants.ADDRESS_API_BASE_URL)
            .client(okHttpClient)
            .addConverterFactory(gsonConverterFactory)
            .build()

    @Provides
    @Singleton
    fun provideLoggingInterceptor() : HttpLoggingInterceptor =
        HttpLoggingInterceptor().apply {
            level = if(BuildConfig.DEBUG) {
                HttpLoggingInterceptor.Level.BODY
            } else {
                HttpLoggingInterceptor.Level.NONE
            }
        }

    @Provides
    @Singleton
    fun provideAddressService(retrofit: Retrofit) : AddressService =
        retrofit.create(AddressService::class.java)

    @Provides
    @Singleton
    fun provideAddressClient(addressService: AddressService) : AddressClient =
        AddressClient(addressService)
}

이번에는 클래스를 생성할 때 @Module과 @InstalIn()라는 어노테이션을 붙이게 됩니다.
그 안에 있는 SingletonComponent:class 관련해서는 아래 첨부된 공식 사이트 링크로 들어가시면 자세히 나오므로 참고하시면 좋을 것 같아요~

https://developer.android.com/training/dependency-injection/hilt-android?hl=ko#generated-components
 

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

Hilt를 사용한 종속 항목 삽입 Hilt는 프로젝트에서 수동 종속 항목 삽입을 실행하는 상용구를 줄이는 Android용 종속 항목 삽입 라이브러리입니다. 수동 종속 항목 삽입을 실행하려면 모든 클래스

developer.android.com

 

@Provides 함수를 만들게 되면
다른 곳에서 @Inject할 때 해당 함수의 return type과 동일한 객체를 주입받기 원하면 해당 함수의 return 값을 주입해줍니다.

말로 표현하니까 좀 이상한 거 같은데..ㅎㅎ;

위의 코드로 예를 들면 provideAddressService() 함수를 보면 Retrofit을 주입받길 원하고 있습니다.
그럼 Hilt는 Retrofit이 누구인지 찾겠죠??
provideRetrofit()의 리턴 값이 Retrofit이므로 해당 함수에서 만든 Retrofit 객체를 provideAddressService()에 주입하게 되는 것입니다.

 

실제로 개발하다 보면 추가적으로 더 공부할 내용은 있겠지만 일단 기본적인 사용법은 위와 같습니다.
본 예제에서도 저거 외 viewModel을 이용한 거 정도밖에 없을 겁니다 ㅎㅎ

다른 예제에서 추가적으로 발생한다고 하면 추가적으로 공부하면서 내용 보충하겠습니다!

 

 

참고 자료
공식 사이트 : https://developer.android.com/training/dependency-injection/hilt-android?hl=ko
블로그 1 : https://developer88.tistory.com/349
블로그 2: https://tecoble.techcourse.co.kr/post/2021-04-27-dependency-injection/

 

728x90
댓글