본문 바로가기

안드로이드

[Android] 10. 구글 코드랩 Dagger를 이용한 리팩토링 - Multiple Activities with the same scope

개요

해당 게시글은 구글 코드랩을 번역한 게시글입니다.

 

 

Multiple Activities with the same scope

세팅 버튼을 클릭하면 앱이 중단된다. Settings 관련된 코드를 리팩토링해보자.

 

현재 SettingsActivity의 필드들은 대거로 주입받아야 한다.

 

1. 대거에게 SettingsActivity가 의존하는 객체들이 어떻게 생성되는지 알려준다. SettingsViewModel.kt이 존재한다.

SettingViewModel.kt

class SettingsViewModel @Inject constructor(
    private val userDataRepository: UserDataRepository,
    private val userManager: UserManager
) { ... }

2. SettingsActivity가 대거로 부터 의존성을 주입받게 하기 위해 AppComponent 인터페이스 안에 SettingsActivity를 매개변수로 갖는 함수를 정의한다.

 

AppComponent.kt

@Singleton
@Component(modules = [StorageModule::class, AppSubcomponents::class])
interface AppComponent {
    ...
    fun inject(activity: SettingsActivity)
}

3. SettingsActivity에서 주입될 필드에 @Inject 처리한다.

4. MyApplication에 존재하는 appComponent 객체에 접근하여 inject(this) 메소드를 호출한다.

5. 기존의 의존성 주입 초기화 라인을 제거한다.

 

SettingsActivity.kt

class SettingsActivity : AppCompatActivity() {

    // 1) 대거로 부터 주입
    @Inject
    lateinit var settingsViewModel: SettingsViewModel

    override fun onCreate(savedInstanceState: Bundle?) {

        // 2) appComponent에 주입
        (application as MyApplication).appComponent.inject(this)

        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_settings)

        // 3) 아래 라인 제거
        val userManager = (application as MyApplication).userManager
        settingsViewModel = SettingsViewModel(userManager.userDataRepository!!, userManager)
        ...
    }
}

 

앱을 실행시켜 보면 Refresh notification이 동작하지 않는 것을 확인할 수 있다. 그 이유는 UserDataRepository가 MainActivity, SettingActivity에 재사용되지 않고 새로운 인스턴스를 생성했기 때문이다.

 

이 상황에서 UserDataRepository를 @Singleton으로 처리해도 괜찮을까? 만약 유저가 로그아웃하거나 등록 해제를 수행하면 UserDataRepository는 메모리에서 더 이상 필요없기 때문에 비효율적이다. 해당 데이터는 오직 로그인한 유저에게만 유효하다.

 

따라서 UserDataRepository가 유저가 로그인 상황에서만 유지되기를 원한다. 사용자가 로그인한 이후에 접근가능한 모든 액티비티는 새로 생성할 UserComponent에 의해 주입된다 (i.g. MainActivity,SettingsActivity).

 

  1. user 패키지에 UserComponent라는 새로운 subcomponent를 만든다.
  2. @Subcomponent 처리된 UserComponent 인터페이스를 정의한다. 

user/UserComponent.kt

// Dagger subcomponent로 정의
@Subcomponent
interface UserComponent {

    // UserComponent 객체 생성 팩토리
    @Subcomponent.Factory
    interface Factory {
        fun create(): UserComponent
    }

    // 이 Component에 의해 주입될 액티비티
    fun inject(activity: MainActivity)
    fun inject(activity: SettingsActivity)
}

 

  3. 새로운 subcomponent를 AppComponent의 subcomponent로 추가한다.

AppSubcomponent.kt

@Module(subcomponents = [RegistrationComponent::class, LoginComponent::class, UserComponent::class])
class AppSubcomponents

 

UserComponent의 생명주기는 무엇이 담당해야 할까? LoginComponent와 RegistrationComponent는 자체 액티비티로 관리되었지만 UserComponent는 두 개 이상의 액티비티를 주입해야한다.

 

유저가 로그인하고 로그아웃할 때를 이 Component가 알아야한다. 이 경우에 그것을 알 수 있는 클래스가 UserManager이다. UserManager는 로그인, 로그아웃, 등록을 처리한다.

 

현재 UserManager는 대거에 의해 주입되는 클래스이므로 UserComponent가 팩토리에 의해 생성되는 과정 또한 주입을 통해 이루어져야한다.

UserManager.kt

@Singleton
class UserManager @Inject constructor(
    private val storage: Storage,
    // UserManager는 UserComponent의 생명주기를 담당,
    // UserComponent 객체의 생성 방법을 알아야 됨
    private val userComponentFactory: UserComponent.Factory
) {
    ...
}

 

UserManager가 UserComponent의 생명주기를 유지할 수 있게 이를 객체화한다. 만약, UserComponent가 Null이 아니면 유저는 로그인될 것이고 UserComponent가 Null이면 유저가 로그아웃될 것이다. 따라서, UserComponent는 특정 유저에 대한 모든 데이터를 가지고 있다가 유저가 로그아웃하면 UserComponent가 파괴되면서 모든 데이터가 메모리에서 제거된다.

 

UserManage가 UserDataRepository를 사용하는 대신 UserComponent를 사용할 수 있도록 리팩토링한다.

 

UserManager.kt

@Singleton
class UserManager @Inject constructor(...) {
    //아래 라인 제거
    var userDataRepository: UserDataRepository? = null

    // 이후에 오는 라인들을 추가하거나 수정
    var userComponent: UserComponent? = null
          private set

	//Null이 아니면 유저는 로그인한 상태
    fun isUserLoggedIn() = userComponent != null

	//logout 시 UserComponent가 Null이 되며 메모리의 데이터가 모두 제거됨
    fun logout() {
        userComponent = null
    }

	//User가 로그인하면 컴포넌트 생성
    private fun userJustLoggedIn() {
        userComponent = userComponentFactory.create()
    }
}

위 코드에서 볼 수 있듯, 유저가 로그인하면 팩토리를 통해 userComponent 객체를 만든다. 그리고 logout() 메소드가 호출되면 해당 객체를 제거한다.

 

UserDataRepository는 UserComponent의 scope로 지정되어 MainActivity와 SettingsAcitivity에 모두 같은 객체를 공유해야 한다.

 

따라서 액티비티가 수명을 관리하는 Component에 @AcitivityScope처리를 하였기 때문에, 전체 앱이 아닌 여러 개의 액티비티를 커버하는 기존과 다른 Scope가 새로 필요하다.

 

따라서 해당 Scope은 유저가 로그인한 이후의 생명주기를 커버하며 이를 LoggedUserScope라고 부른다.

 

 

 

user/LoggedUserScope.kt

@Scope
@MustBeDocumented
@Retention(value = AnnotationRetention.RUNTIME)
annotation class LoggedUserScope

UserComponent와 UserDataRepository에 해당 annotation 처리를 통해 UserComponent는 항상 같은 UserDataRepository를 제공할 수 있게 된다.

 

UserComponent.kt

@LoggedUserScope
@Subcomponent
interface UserComponent { ... }

UserDataRepository.kt

@LoggedUserScope
class UserDataRepository @Inject constructor(private val userManager: UserManager) {
    ...
}

 

UserDataRepository는 기존 리팩토링 전 코드에서 MainActivity가 UserManager에 생성된 UserDataRepository에 접근하여 viewmodel에 주입하기 위해 UserManager 내부에 정의되어 있었다. 하지만, UserDataRepository는 생성자에 @Inject 처리되어 UserComponent의 생명주기 동안 그래프에 존재하게 되어 더 이상 UserManager에서 구현되어 있을 필요가 없다.

리팩토링 이전 코드 참고

 

다시 본론으로, MyApplication 클래스에서 현재 UserManager를 초기화하여 저장하고 있다. 우리의 목적은 오직 대거로만 의존성 주입을 관리하는 것을 목표로 하기에 이를 대거가 주입하도록 리팩토링해야한다.

 

현재 MyApplication의 코드:

MyApplication.kt

open class MyApplication : Application() {

    val appComponent: AppComponent by lazy {
        DaggerAppComponent.factory().create(applicationContext)
    }
}

 

또한 AppComponent도 수정해야 한다

  1. MainActivity와 SettingActivity는 UserComponent에 의해 주입되지, AppComponent에 의해 주입되지 않으므로 관련 메소드를 제거한다.
  2. MainActivity와 SettingActivity가 UserComponet의 특정 객체에 접근하려면 UserManager를 그래프에 추가한다.

AppComponent.kt

@Singleton
@Component(modules = [StorageModule::class, AppSubcomponents::class])
interface AppComponent {
    ... 
    // 2) UserManager를 추가하여 MainActivity와 SettingsActivity가
    // UserComponent의 특정 객체를 접근 가능하게 함
    fun userManager(): UserManager

    // 1) 아래 라인들 제거
    fun inject(activity: MainActivity)
    fun inject(activity: SettingsActivity)
}

SettingActivity에서 @Inject를 통해 settingsViewModel을 주입한다. 유저가 로그인했기 때문에 초기화될 UserComponent를 가져오기 위해 appComponent가 갖고 있는 userManager() 메서드를 호출한다. 이제 userManager 안에 존재하는 userComponent에 접근 가능하여 액티비티를 주입할 수 있게된다.

 

SettingsActivity.kt

class SettingsActivity : AppCompatActivity() {
    // 대거에 의해 주입받음
    @Inject
    lateinit var settingsViewModel: SettingsViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        // application graph에 존재하는 userManager를 가져옴
        // userManager의 userComponet를 통해 해당 액티비티 주입
        val userManager = (application as MyApplication).appComponent.userManager()
        userManager.userComponent!!.inject(this)

        super.onCreate(savedInstanceState)
        ...
    }
    ...
}

MainActivity도 유사하게 진행한다.

  1. UserManager는 appComponent에서 직접 가져오기 때문에 더 이상 주입할 필요가 없다. 기존 userManager 주입 코드를 제거한다.
  2. 사용자의 로그인을 체크하기 위해 userManager 지역 변수를 만든다
  3. UserComponent는 유저가 로그인한 뒤 이용가능하기 때문에 if, else를 통해 분기처리한다.
class MainActivity : AppCompatActivity() {

    // 1) userManager 필드 제거
    @Inject
    lateinit var userManager: UserManager

    @Inject
    lateinit var mainViewModel: MainViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_settings)

        // 2) appComponent에서 userManager를 가져와 유저의 로그인 체크
        val userManager = (application as MyApplication).appComponent.userManager()
        if (!userManager.isUserLoggedIn()) { ... }
        else {
            setContentView(R.layout.activity_main)
            // 3)  MainActivity가 보여야 될 경우, UserComponent를 통해
            // 이 Activity를 주입할 수 있다
            userManager.userComponent!!.inject(this)
            setupViews()
        }
    }
    ...
}

중요한점. 조건부 필드 인젝션은 매우 위험하다. 개발자는 조건을 알고 있어야 하며 삽입된 필드와 상호작용하는 과정에서 NullPointerException이 발생할 수 있다. 이러한 문제를 방지하기 위해 사용자의 상태에 따라 Login,Main,Registration 등으로 라우팅하는 SplashScreen을 생성하여 간접적인 방법을 사용할 수 있다.

 

이제 모든 화면이 정상적으로 동작하게 된다.

 

 

요약

  1. 여러 액티비티에 같은 scope이 필요한 경우, 해당 액티비티가 생성되어있는 동안 존재할 수 있는 클래스를 찾아내기 -> 보통 Application 수준에서 생성되는 객체들 (i.g. UserManager)
  2. 해당 객체 내부에 component를 정의하여 생명 주기를 함께하게 한다. 그러면 여러 액티비티에 주입될 수 있게 됨.(i.e. UserManager 내부의 UserComponent)
  3. 해당 컴포넌트와 생명주기를 같이 해야하는 클래스들에게 해당 컴포넌트에 붙인 새로운 scope로 처리한다. (i.e. LoggedUserScope -> UserDataRepository, UserComponent)