안드로이드 연구소

[안드로이드 클린아키텍처] 안드로이드 아키텍처 가이드라인 만드는 법1(Hilt Retrofit 연결하는법/Hilt Room 연결하는법) 본문

안드로이드 연구소/클린아키텍처

[안드로이드 클린아키텍처] 안드로이드 아키텍처 가이드라인 만드는 법1(Hilt Retrofit 연결하는법/Hilt Room 연결하는법)

안드로이드 연구원 2023. 8. 14. 17:53

안녕하세요 안드로이드 개발자입니다.

저는 최근 한주간 강의를 들으면서 클린아키텍처에 대해서 더 배우고

제 간단한 샘플 프로젝트를 만들면서 어떻게 강의를 해야할지 고민하던 와중에...

 

코로나에 또 걸렸습니다.

유행이라고 말만들었는데 예상치도 못하게 걸려버렸네요 ㅎㅎ;;

이틀간 사망한뒤 이렇게 다시 일어나게되었습니다.

 

그렇다면 한주간 정리한 저의 클린 아키텍처

안드로이드 아키텍처 가이드라인을 소개해보도록 하겠습니다.


1. 클린아키텍처에서 가장 중요한 것은?

그전에 들어가기 전에

여러분들은 클린 아키텍처에서 가장 중요한 요소가 무엇이라고 생각하시는가요?

데이터레이어? 도메인레이어? 프레젠테이션 레이어? 아니면 그 4개 원 그림?

 

저는 이번 공부를 하면서 클린 아키텍처를 만들기위해서

필수 오브 필수품은 의존성 주입 라이브러리라고 꺠닫게 되었습니다.

 

클린아키텍처의 창시자 Uncle Bob 선생님께서는 '클린 아키텍처는 "경계"를 만드는 기술이다. '라고 하셨는데요.

안드로이드에서 이 말은 UI레이어(프레젠테이션) -> Domain레이어 -> Data레이어 이 순서가 지켜져야함을 의미합니다.

그렇다면 서로의 경계에 있는 클래스가 의존성에 문제 없이

인스턴스로 하여금 잘 불러와져하는 것이라는 의미라고 생각합니다.

안드로이드 주사 쪽 (출처: 구글 안드로이드 공식 홈)

 

2. 수동 의존성 주입으로 만들면 안되나요? 

저는 의존성 주입 라이브러리를 최대한 사용하지 않고

수동 의존성 주입으로만해서 클린 아키텍처 프로젝트를 만들어서

여러분들에게 소개시켜드리려고 했지만 불가능하다는 것을 알게되었습니다.

 

마지막 단계에서 엑티비티를 연결할 때 엑티비티에서 의존성 주입 라이브러리 없이 

ViewModel를 연결하려면 Factory 클래스를 생성해줘야합니다.

 

이 과정에서 Factory 클래스를 만든다면 ViewModel에서 연결한

UseCase, Repository, DataSource들을 전부 불러오게 되는 대형 참사가 발생합니다.(아래가 예시)

    class ViewModelFactory(private val useCase: UseCase) : ViewModelProvider.Factory {
        override fun <T : ViewModel> create(modelClass: Class<T>): T {
            // useCase 호출 후 repository 호출 후 serviceApi(Data source) 호출;;
            return SearchViewModel(useCase(Repository(ServiceApi()))) as T
        }
    }

 

만약 위처럼 불러오게 된다면

ViewModel(Data레이어)에서 UseCase(Domain레이어), Repository과 DataSource(Data레이어)

모두를 불러오게 되는 건 클린 아키텍처에 위배되는 행위가 되게 됩니다.

 

만약에 이게 이해가 안되신다면 저처럼 의존성 주입 라이브러리 없이 만들어보시면 됩니다^^

(혹시 되는 방법이 있다면 댓글에 알려주세요. 제발)

실패 속에서 꺠달음을 얻은 나. 근데 이 형들 실팬데 왜 신난거여.. (출처: 1박2일 시즌1)

 

 

3. 의존성 주입 라이브러리 어떤것을 썼니?

그렇다면 이전에 Dagger2 vs Hilt, Hilt vs Koin을 비교하는 포스트들을 통해서

Hilt가 현재 가장 우수한 것으로 보인다고 말씀드린 적이 있습니다.

https://android-lab.tistory.com/28

 

[여기가 DI 설명 제일 잘함] Koin vs Hilt

지난 번 의존성 주입 첫번째 포스터에서 의존성 주입(DI)이 뭔지 알아보았고 의존성 주입 두번째 포스터에서 안드로이드 의존성 주입(DI)라이브러리 2가지 Dagger와 Hilt를 알아보았습니다. 그렇다

android-lab.tistory.com

 

하지만 이전 포스터들에서는 의존성 주입 라이브러리에 대한 비교와

ViewModel에서 사용하는 간단 예시들이었기 때문에

이번 클린 아키텍처 안드로이드 아키텍처 가이드라인에서 사용하기에는 내용이 너무 부족하여

클린 아키텍처에서 필요한 Hilt를 최소화해서 정리해보려고합니다.

조셉형 부탁해 (출처:MBC 무한도전)

 

4. Hilt 기본 세팅하는 법을 알려줘

첫번째, build.gradle(app)

plugins {
    id("com.google.dagger.hilt.android") version "2.44" apply false
}

 

두번째, build.gradle(android)

plugins {
    id("dagger.hilt.android.plugin")
}

dependencies {
    implementation 'com.google.dagger:hilt-android:$version'
    kapt 'com.google.dagger:hilt-android-compiler:$version'
}

최신버전: https://developer.android.com/jetpack/androidx/releases/hilt?hl=ko

 

세번쨰, HiltApplication.kt 파일 생성

@HiltAndroidApp으로 주석을 달아 Hilt를 활성화합니다.

import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
class HiltApplication : Application()

 

마지막으로 AndroidManifest에서

아래처럼 이름을 변경해주면 기본 세팅 끝!

    <application
        android:name=".HiltApplication"

 

 

5. 각 클래스에서 Hilt를 사용하기 선언(Hilt 컴포넌트 생성)

@HiltAndroidApp
class HiltApplication : Application()
@HiltViewModel
class MainViewModel: ViewModel()
@AndroidEntryPoint
class MainActivity : AppCompatActivity()

Activity이외에도 Fragment, View, Service, BroadcastReciver 모두 @AndroidEntryPoint를 사용하고 있습니다.

 

 

6. 의존성 주입 사용법 @Inject

위에 5번 과정을 통해서 클래스에 맞는 Hilt컴포넌트를 생성한 후에

클래스에서 @Inject constructor()를 사용해주기만 하면 됩니다.

// (1) Hilt 컴포넌트 생성
@HiltViewModel 
// (2) @Inject constructor로 의존성 주입
class MainViewModel @Inject constructor(private var useCase: UseCase): ViewModel()  {
    // (3)의존성 주입한 인스턴스 사용하기
    useCase.getList()
}

이렇게 쉽게 의존성 주입하고 생성하면 끝!

 

 

7.  일반 클래스들에서 의존성 주입 사용법 @Module

위 처럼만 해서 의존성 주입을 다 사용할 수 있다면 너무 편리하겠지만

클린 아키텍처를 만들다보면 예외의 경우가 발생하게됩니다.

 

Hilt 컴포넌트를 생성하는 클래스들은 Application, Activity, Fragment, ViewModel과 달리

그냥 단순히 저희가 만든 일반 클래스에서 의존성 주입을 해야한다고 하면 어떻게 해야할까요?

 

클린아키텍처 안드로이드 아키텍처 가이드라인을 만들기위해서는

DataSource파일들, Repositroy, UseCase들을 만들게 됩니다.

 

이떄 해당 파일들은 Application(), ViewModel(), AppCompatActivity()등을 상속 받지 않은 일반 클래스들입니다.

이 클래스들을 의존성 주입하고 사용하려면 새로운 Hilt의 개념인 모듈에 대해서 알야합니다 필요합니다.

 

우선 첫번째로 알아야하는 어노테이션은 @Module입니다.

@Module은 해당 클래스가 Hilt에게 인스턴스를 제공하는 방법을 알려줍니다.

간단하게 Hilt 삽입할 클래스임을 선언하는 정도라고 이해하시면 될 것 같습니다.

 

 

8. 모듈의 범위를 지정해줘 @Installin

@Module을 통해서 Hilt모듈임을 설정하였다면

우리가 임의로 @Installin 어노테이션으로 컴포넌트의 범위를 지정해야합니다.

SingletonComponent::class => Application의 생명주기
ActivityComponent::class => Activity의 생명주기
FracgmentComponent::class => Fragment의 생명주기
ViewModeComponent::class => ViewModel의 생명주기
ViewComponent::class => View의 생명주기
ServiceComponent::class => service의 생명주기

앞으로 저희는 가장 넓은 범위에서 사용할 수 있는

SingletonComponent::class를 사용하는 것에 집중하면 됩니다. 

 

 

9. 의존성 주입 대상 여부를 확인하자! @Provides와 @Bind

위에서 모듈과 모듈의 범위를 설정하였다면

의존성을 주입시킬 대상의 반환 여부에 대해서 확인을 해야합니다.

 

인스턴스를 제공하여 반환하는 메서드라면 의존성 주입할 예정이라면 @Proivdes

아니면 인터페이스나 추상클래스여서 반환값이 없는 경우라면 @Binds를 사용합니다.

 

 

10. Retrofit 의존성 주입

그렇다면 위에서 배운 내용들을 총 종합해서 사용해보겠습니다.

먼저 보여드릴 예시는 Repository에서 의존성 주입입니다.

 

Repositoty에서는 아래와 같이 서버와 통신을 하는 ServiceApi와 

내부 데이터베이스를 사용하는 Dao인터페이스를 불러와야합니다.

class Repository @Inject constructor(
    private val serviceApi: ServiceApi, 
    private val dao: Dao) 
{

 

그렇다면 ServiceApi의 인스턴스를 생성하는

ServiceModuleDaoModule 둘 다 필요할 것입니다.

우선 ServiceModule의 구현 부분을 보겠습니다.

@Module // 특정 유형의 인스턴스를 제공하는 방법을 Hilt에 알려줍니다
@InstallIn(SingletonComponent::class) // SingletonComponent은 전체 Application에 삽입할 것 임을 선언
object ServiceModule {

    @Provides // 인스턴스 삽입할 내용 (Provides와 Bind중에 사용)
    @Singleton // SingletonComponent와 세트
    fun provideServiceApi(retrofit: Retrofit): ServiceApi {
        return retrofit.create(ServiceApi::class.java)
    }
}

 

ServiceApi의 인스턴스를 객체를 반환하기 위해 

@Provides를 선언하여 인스턴스 공급자로 표시합니다.

@Provides
@Singleton 
fun provideServiceApi( ): ServiceApi

 

그리고 serviceApi를 호출하기위해서 필요한 Retrofit 인스턴스도

의존성 주입으로 생성해둔 파일에서 불러옵니다.

@Module
@InstallIn(SingletonComponent::class)
object RetrofitModule {

    @Singleton
    @Provides
    fun provideConverterFactory(): GsonConverterFactory {
        return GsonConverterFactory.create(
            GsonBuilder()
                .setLenient() // 규칙 완화 Use JsonReader.setLenient(true) to accept malformed JSON at line 1 column 1 path $ error
                .create()
        )
    }

    @Provides
    @Singleton
    fun provideOkHttpClient(application: Application): OkHttpClient.Builder {
        return OkHttpClient.Builder().apply {
            connectTimeout(5, TimeUnit.SECONDS)
            readTimeout(5, TimeUnit.SECONDS)
            writeTimeout(5, TimeUnit.SECONDS)
            addInterceptor(HttpLoggingInterceptor().apply {
                level = HttpLoggingInterceptor.Level.BODY
            })
        }
    }

    @Provides
    @Singleton
    fun provideRetrofit(client: OkHttpClient.Builder, gsonConverterFactory: GsonConverterFactory): Retrofit {
        return Retrofit.Builder()
            .baseUrl("https://apis/~~")
            .addConverterFactory(gsonConverterFactory)
            .client(client.build())
            .build()
    }
}

provideRetrofit에 필요한 OkHttpClient.Builder 인스턴스와 GsonConverterFactory인스턴스

둘 다 동일한 파일에서 의존성 주입 공급자로 선언하고 의존성 주입하여 사용합니다.

 

 

11. Room Database에서 의존성 주입

위의 Repository에서 서버 통신을 하기위해서는 ServiceApi 인스턴스를 필요로 하였고

내부 데이터베이스와 연결하기위해서는 Dao 인스턴스가 필요합니다.

serviceApi와 똑같이 만들어보면 아래와 같습니다.

@Module
@InstallIn(SingletonComponent::class)
object DaoModule {

    @Singleton
    @Provides
    fun providesDao(appDatabase: AppDatabase) : Dao {
        return appDatabase.dao()
    }
}
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {

    @Singleton
    @Provides
    fun providesDataBase(@ApplicationContext appContext: Context): AppDatabase {
        return Room.databaseBuilder(appContext, AppDatabase::class.java, "databse.db")
            .fallbackToDestructiveMigration()
            .build()
    }
}

 


Hilt를 사용법은 어렵다면 어렵고 쉽다 쉬운 경향인데

몇몇 부분들은 외워줘야하는 부분들이 있다고 생각해서 그렇다고 생각합니다.

 

하지만 의존성 주입 라이브러리 없이는 

클린 아키텍처는 무용지물이기 때문에

가장 쉽게 다룰 수 있는 Hilt에 대해서 공부를 해보시는 것을 추천드립니다.

 

그렇다면 다음 포스트부터 실제로 클린 아키텍처를 사용한 예제를 올려보도록하겠습니다.

감사합니다.

Comments