Grusie 안드로이드 개발 기술 블로그

[Android] 클린 아키텍처 회사 프로젝트에 적용하기 본문

안드로이드 개발/클린아키텍처

[Android] 클린 아키텍처 회사 프로젝트에 적용하기

grusie 2024. 3. 26. 17:05
728x90
반응형
SMALL

현재 근무중인 회사의 안드로이드 프로젝트는 기존 MVC 패턴으로 구현되어 있으며, Retrofit 같이 여러 곳에서 사용 되어야 하는 객체도 Singleton으로 의존성 주입을 활용하여 되어있지 않다.

현재 새로 개발하는 프로젝트들은 필자가 MVVM패턴을 적용하고 있으나, DI는 아직 적용하지 못했던 상태이다.

클린 아키텍처를 공부하며 프로젝트까지 진행한 이번 기회에 클린 아키텍처 + MVVM + Hilt를 적용해 볼 예정이다.

 

 

우선 클린 아키텍처를 위해, 각 계층들을 추가한다.

- data모듈(실질적인 통신)

- domain모듈(중개자)

- presentation모듈(UI)

- DI(DI 및 앱 설정)

 

기존에 사용했던 프로젝트들을 분리하려고 했으나, 얽혀있는 것들이 너무 많아 모듈로 분리하지 않고, 디렉토리로 분류하게 되었다.

 

Hilt사용

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

프로젝트 수준의 build.gradle 파일에 힐트를 활용하기 위한 의존성을 주입해준다.

apply plugin: 'dagger.hilt.android.plugin'
...

dependencies{
	...	
	implementation "com.google.dagger:hilt-android:2.44"
	kapt "com.google.dagger:hilt-android-compiler:2.44"
    ...
}

사용할 모듈에도 마찬가지로 힐트와 컴파일러에 관련한 어노테이션을 위한 의존성을 주입해준다.

 

@HiltAndroidApp
public class GlobalApplication extends MultiDexApplication {

기존에 사용중이던 어플리케이션을 상속받는 안드로이드 앱 클래스에 HiltAndroidApp 어노테이션을 추가해주고

 

<application
    android:name=".GlobalApplication"

매니페스트 application 속성에 name태그를 등록해주면 힐트를 사용하기 위한 준비가 끝이 난다.

 

ApiModule

@Module
@InstallIn(SingletonComponent::class)
internal object ApiModule {
    @Provides
    @Singleton
    fun provideHttpClient(): OkHttpClient {
        val interceptor = HttpLoggingInterceptor()

        if (BuildConfig.DEBUG) {
            interceptor.level = HttpLoggingInterceptor.Level.BODY
        } else {
            interceptor.level = HttpLoggingInterceptor.Level.NONE
        }

        return OkHttpClient.Builder().connectTimeout(5, TimeUnit.SECONDS)
            .addInterceptor(interceptor).build()
    }

    @Provides
    @Singleton
    @Named("tomcatRetrofit")
    fun provideTomcatRetrofit(client: OkHttpClient): Retrofit {
        return Retrofit.Builder()
            .baseUrl(HttpConstant.TOMCAT_SERVER_URL + "/")
            .addConverterFactory(
                GsonConverterFactory.create())
            .client(client)
            .build()
    }
    @Provides
    @Singleton
    @Named("iisRetrofit")
    fun provideIISRetrofit(client: OkHttpClient): Retrofit {
        return Retrofit.Builder()
            .baseUrl(HttpConstant.TOMCAT_SERVER_URL + "/")
            .addConverterFactory(
                GsonConverterFactory.create())
            .client(client)
            .build()
    }


    @Provides
    @Singleton
    fun provideTomcatService(@Named("tomcatRetrofit")retrofit: Retrofit): TomcatRequest {
        return retrofit.create(TomcatRequest::class.java)
    }

    @Provides
    @Singleton
    fun provideIISService(@Named("iisRetrofit")retrofit: Retrofit): IISRequestApi {
        return retrofit.create(IISRequestApi::class.java)
    }

    @Provides
    @Singleton
    fun provideGsonBuilder(): Gson {
        return GsonBuilder().create()
    }
}

서버통신을 담당할 Retrofit 서비스를 제공해줄 ApiModule을 만들어준다.

Singleton으로 생성하였기에, 가져다가 사용하면 된다.

 

각 계층 패키지 구분

다른 코드들은 너무 많아 구조만 간단하게 보고 넘어가도록 하자.

vrog라는 마이페이지의 개편을 하려고 하는데, 구조는 클린아키텍처 구조를 활용하기에

View -> ViewModel -> UseCase -> Repository -> Source -> 서버

이런 구조로 되어있으며, data, di, domain, presentation 영역으로 구분하여 코드를 구성하였다.

 

클린아키텍처에서 주의깊게 봐야 할 di에서는, 각각에 해당하는 계층에 대한 의존성 주입을 담당하고 있다.

 

각 계층별 주요 코드들만 보고 넘어가자.

presentation

@HiltViewModel
class VrogViewModel @Inject constructor(private val userInfoUseCase: UserInfoUseCases) :

UseCase를 주입 받아 사용하는 HiltViewModel이다, 물론 여러 가지 useCase를 주입 받아야한다.

 

@AndroidEntryPoint
class NewVrogFragment : BaseFragment() {
    private var binding: FragmentNewVrogBinding? = null
    private val viewModel: VrogViewModel by viewModels()

뷰모델을 주입받아 사용할 브로그 프래그먼트이다. @AndroidEntryPoint를 활용한다.

물론, 프래그먼트를 사용하는 액티비티에서도 @AndroidEntryPoint를 선언해주어야 한다.

 

UseCase


class GetUserInfoUseCase(private val repository: UserInfoRepository) {
    suspend operator fun invoke(memId: String): ResponseResult<UserItemVo> {
        return repository.getUserInfo(memId = memId)
    }
}

repository에서 유저정보를 받아서 넘겨준다. ResponseResult를 따로 구현하여, 처리하였다.

 

Repository

class UserInfoRepositoryImpl @Inject constructor(
    private val userInfoSource: UserInfoSource,
    private val gson: Gson
) :
    UserInfoRepository {
    override suspend fun getUserInfo(memId: String): ResponseResult<UserItemVo> {
        return try {
        	...
        }catch(e:Exception)
    }
    ...
}

Repository에서는 서버통신을 위한 Source와 파싱을 위해 사용할 Gson을 주입받아서 사용한다.

 

Source

class UserInfoSourceImpl @Inject constructor(private val tomcatService: TomcatRequest) :
    UserInfoSource {
    override suspend fun getUserInfo(memId: String): Call<RequestResult> {
        return tomcatService.requestMemInfo(memId)
    }

Source에서는 Retrofit을 활용하여 생성해 둔 서비스를 주입받아, 실질적인 서버통신을 실행한다.

 

 

후기

기존에 너무 얽혀있던 부분들이 많아서, 따로 모듈로 빼는데에 실패하였다. 물론 다시 사용할 부분들을 새로 만들면서 하면 가능하겠으나, 너무 많은 것들을 새로 만들었어야 했다. 예를 들어 Utils라는 클래스에는 유틸에 관련한 함수들이 있을 것이라고 예상하였으나, 유틸에 관한 것들도 있었지만, 뷰를 의존하고 있는 경우가 다수 존재했다. 브로그로 이동하는 인텐트를 담은 함수라던지, Alert, Progress등 전부, 뷰를 의존하고 있었기에 분리하는 게 힘들 것이라 판단했다.

또한 데이터클래스도 사용하는 곳이 너무 많아 이동하는 데에 두려움이 컸던 것 같다.

그래서 이번 클린아키텍처 적용은, 그냥 각 계층별로 역할을 분리하는 것만으로 만족해야겠다.

반응형
LIST