본문 바로가기

내일배움캠프 안드로이드 3기

[TIL] 24.05.21 Service Locator 패턴 적용하기

Service Locator 패턴 적용하기

 

 팀 프로젝트 진행 중에, 튜터님께 프로젝트의 아키텍처에 대한 피드백을 듣고 domain 레이어를 추가하기로 했다. 원래는 간단한 구조의 앱이라고 생각하기도 했고, 팀원들에게 최대한 쉬운 구조로 접근하려고 optional한 domain 레이어를 제외하고 presentation-data 2 레이어만 사용하기로 했었다. 하지만 그러다 보니 presentation 레이어가 domain 레이어의 역할을 어느 정도 겸하게 되었는데, 튜터님께서는 다중모듈로 구성하게 되었을 때 이 레이어들이 온전하게 분리가 될 수 없을 것 같다고 하셨다.

 그런 의존성 측면 부분에 대한 문제는 인지하고 있었지만, 레이어를 2개만 쓰면 감수해야 하는 부분이라고 생각했다. 하지만 튜터님과 앱 아키텍처에 관해 이야기를 많이 나누었고, 아무래도 견고한 구조를 지향하는 것이 맞는 것 같아 결국 domain 레이어를 도입하기로 했다. 대신 usecase까진 만들지 않아도 될 것 같아서, 없이 구성해보기로 했다.

 

 사실 레이어가 나뉘어있지 않았다뿐이지, 의존성이나 로직 분리는 충분히 고려해서 repository pattern, 인터페이스를 이용한 의존성 역전 등을 구현해 둔 상태였다. 그래서 domain 영역을 분리해내는 것 자체는 큰 비용이 들지는 않았다. 대신에 ViewModel에 주입되어야 할 repository들의 구현체가 data 레이어에 있었으므로, 의존성 문제가 생겼다.

 Hilt까지 도입하면 프로젝트에 혼란이 가중될 것 같아서, 튜터님께 찾아가 DI 없이 의존성 문제를 해결할 방법을 여쭤보았다. 튜터님께서는 Service Locator 패턴에 대해 알려주셨고, 이를 적용해보라고 하셨다.

 Android developers의 앱 아키텍처 문서에서 manual dependency injection을 다루는 페이지에 소개가 잘 되어 있었는데, 분명 예전에 DI를 공부한다고 읽었던 페이지였는데 내용이 낯설었다😂

 

출처 Android Developers, 분명 예전에 읽었던 페이지인데..

 

 

 골자는 application 차원에서 주입할 객체들을 관리하도록 하고, 이를 필요한 곳에서 호출해 사용하도록 하는 내용이었다. 작업 중이던 프로젝트에 적용하기 위해, app 패키지를 생성해 사용할 Application 클래스를 정의하고 AppContainer도 정의했다.

 

class AppContainer(context: Context) {
    private val youtubeService = RetrofitClient.youtubeService

    val searchRepository = SearchRepositoryImpl(youtubeService)
    val videoRepository = VideoRepositoryImpl(context)

    var myVideoContainer: MyVideoContainer? = null
    ...

}

class MyVideoContainer(
    private val videoRepository: VideoRepository
) {
    val myVideoViewModelFactory = MyVideoViewModelFactory(videoRepository)
    val user = DummyAuth.getUser()
}

...

 

class YMediaApplication: Application() {

    val appContainer = AppContainer(this)
}

 

 대략 이런식으로 컨테이너를 미리 정의해두고 Application 내에 생성해 사용하는 방식이다. 

 

class MyVideoFragment : Fragment() {
	...

    private lateinit var appContainer: AppContainer
    private lateinit var myVideoViewModel: MyVideoViewModel
    private lateinit var user: User

	...

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        appContainer = (requireActivity().application as YMediaApplication).appContainer
        appContainer.myVideoContainer = MyVideoContainer(appContainer.videoRepository)
    }

  	...

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        appContainer.myVideoContainer?.let {
            myVideoViewModel = ViewModelProvider(this, it.myVideoViewModelFactory)[MyVideoViewModel::class.java]
            user = it.user
        }
    	...
    }
    
    ...
    
	override fun onDestroy() {
        appContainer.myVideoContainer = null
        super.onDestroy()
    }
    
    ...

 

 

 그리고 실제로 이용할 때는 주입받을 컴포넌트의 생명주기에 맞춰 각 화면에 필요한 컨테이너를 생성해 사용하고 소멸시킨다. 생각보다 간단하지만 강력한 패턴이었고, 의존성 문제를 깔끔하게 해결할 수 있었다.

 

 하지만 팀원분께서 DB 초기화와 관련해서 에러가 생긴다고 하셨고, 확인해보니 DB를 이용하는 Repository 구현체에서 초기화 시점에 DB도 연결하게 되는데 이 때 Context가 null로 전달되는 상황이었다. AppContainer에 생성자를 달아 Application 내에서 this를 주입하도록 했었는데, 디버깅 해서 보니까 Application이 onCreate() 되는 시점보다 AppContainer의 초기화가 빨랐고 그러다보니 AppContainer 내의 Repository들도 Application의 생성보다 먼저 생성되고 있어서 발생하는 문제였다.

 

class AppContainer(context: Context) {
    private val youtubeService = RetrofitClient.youtubeService

    val searchRepository: SearchRepository by lazy { SearchRepositoryImpl(youtubeService) }
    val videoRepository: VideoRepository by lazy { VideoRepositoryImpl(context) }

    var myVideoContainer: MyVideoContainer? = null
    ...

}

 

 그래서 다양한 방법을 고려하다가 심플하게 by lazy를 통해서 Repository의 생성 시점을 지연시켰다. 관련해서 튜터님과 이야기 나누었는데 큰 문제가 없는 것 같아서, 이대로 사용하게 될 것 같고 테스트 했을 때는 모든 기능들이 정상 작동했다. 

 그리고 좀 더 안전하게 AppContainer를 아래처럼 생성해볼까 생각 중인데 아직 적용은 안 했고, 내일 팀원분들 및 튜터님과 고민해볼 것 같다.

 

class YMediaApplication: Application() {

    lateinit var appContainer: AppContainer

    override fun onCreate() {
        appContainer = AppContainer(this)
        super.onCreate()
    }
}

 

 DI 라이브러리를 사용하지 않고, Injector도 따로 구현히지 않은 채로, 이렇게 의존성 주입을 구현해보니 좋은 경험이 된 것 같다.