세상에.. 마지막 글이 4월이다 🙁
회사 일이 바빠서 라는 핑계는 댈 수가 없다.. 충분히 여기저기 놀러다니고 기타 치고 놀았으면서
과연 블로그 글 쓸 시간이 없었을까?? 이건 100번 반성해야 한다.. 😞
제발 정신줄 꽉 잡고 블로그 활동 좀 하자!! 초심을 잃지 말자 ㅠㅠ
🔥 해당 포스팅은 Dagger 에 대한 기본적인 이해를 전제로 합니다
아래 자료들에 의거하여 작성된 포스팅입니다.
https://developer.android.com/training/dependency-injection/dagger-android?hl=ko
https://dagger.dev/dev-guide/android.html
Dagger 에서는 여러 DI 기법 중 생성자 주입(Constructor Injection
) 을 가장 권장하고 있다. 생성자 주입을 사용한다면, 컴파일러 차원에서 의존성 주입 이전에 객체가 참조되는 등의 상황을 방지해주기 때문이다. (즉, NullPointerException
이 발생하는 상황을 막아준다)
Dagger 문서 中 (링크)
Constructor injection is preferred whenever possible because javac will ensure that no field is referenced before it has been set, which helps avoid NullPointerExceptions.
그런데 일부 상황에선 어쩔 수 없이 Constructor Injection
을 사용하지 못할 때가 있다. Android 컴포넌트(Activity, Fragment 등) 를 사용하는 경우가 그 예다. Fragment
의 생성자는 비워둬야 하는 것이 원칙이고, Activity 의 생성자는 건드릴 방법이 전혀 없기 때문이다.
Dagger 에서는 이러한 상황에서 필드 주입 (Field Injection
) 을 사용할 수 있도록 inject()
메소드를 제공해준다. 예를 들어 아래와 같은 Component
와 Activity
, ViewModel
이 있다고 하자. 필드 주입을 위한 준비 과정은 주제에 벗어나므로 생략한다.
class MyApplication: Application() {
val appComponent = DaggerApplicationComponent.create()
}
@Component
interface ApplicationComponent {
fun inject(activity: LoginActivity)
}
class LoginActivity: Activity() {
@Inject lateinit var loginViewModel: LoginViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
}
class LoginViewModel @Inject constructor(
private val userRepository: UserRepository
) { ... }
이 경우 Activity
는 Dagger 에서 제공해주는 다음과 같은 코드를 추가함으로써 필드 주입을 받을 수 있다.
(applicationContext as MyApplication).appComponent.inject(this)
필드 주입은 클래스가 인스턴스화되고 난 뒤 최대한 빠른 시점에 일어나야 한다. 우선 Activity
의 상황에서는, 우리가 흔히 알고 있는 Lifecycle Callback 인 onCreate()
시점에서 필드 주입이 일어나야 맞다.
따라서 아래와 같이 필드 주입을 실행할 수 있다.
class LoginActivity: Activity() {
@Inject lateinit var loginViewModel: LoginViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
(applicationContext as MyApplication).appComponent.inject(this)
}
}
그러나 위와 같이 코드를 작성할 경우 문제가 발생한다. 얼핏 보기에는 잘못된 부분이 없지만, super.onCreate()
에서 일어나는 일에 의거하면 문제가 발생하는 것이 맞다.
Activity
에서 Configuration Change 등이 일어날 때 화면을 구성 및 복구하기 위해 super.onCreate(savedInstanceState)
가 호출되면, 해당 액티비티에 속해있는 Fragment
들을 차례대로 Attach 하게 된다. 그런데 해당 Fragment
들은 자신이 속한 Activity
에 접근할 수 있기 때문에, Fragment
가 Attach 되기 전에 상위 Activity
가 먼저 완성되어 있어야 한다. 즉, DI 가 모두 이루어지고 난 뒤 super.onCreate()
가 호출되어야 한다. 따라서 코드는 다음과 같이 작성되어야 한다.
class LoginActivity: Activity() {
@Inject lateinit var loginViewModel: LoginViewModel
override fun onCreate(savedInstanceState: Bundle?) {
// 반드시 super.onCreate() 보다 먼저 불려야 한다!
(applicationContext as MyApplication).appComponent.inject(this)
super.onCreate(savedInstanceState)
}
}
Fragment
에선 어느 시점에 필드 주입이 일어나야 할까?우리가 알고 있는 Fragment
의 생명주기에 따르면, onAttach()
시점에 필드 주입이 일어나야 맞다. 인스턴스화 이후 최대한 빠르게 필드 주입이 일어나야하기 때문이다. 동작 시간 상 onAttach()
와 그리 차이가 나지 않는 onCreate()
도 고려해볼 수 있겠지만, Fragment
의 onCreate()
는 Fragment
가 Attach 되고 난 뒤에 두 번 다시 호출되지 않는다.
즉, Fragment
가 Re-attach 되는 상황에서 DI 가 정상동작 안 할 수 있기 때문에 onAttach()
시점에 필드 주입이 일어나야 한다.
아래 사진을 보면, Re-attach 상황에서 onCreate() 를 스킵하는 것을 확인할 수 있다.
참고로 Fragment
에서는 Activity
상황과 다르게 화면 복원과 관련하여 더이상 문제될 것이 없기 때문에 super.onAttach()
전후 상관없이 필드 주입을 할 수 있다.
override fun onAttach(context: Context) {
super.onAttach(context)
(applicationContext as MyApplication).appComponent.inject(this)
}
오히려 그 초심을 망실하지 않았기에, 헤매지 않고 원점으로 돌아올 수 있었다 생각합니다.
awesome devblog 에 오랜만에 글이 올라와 댓글 달아봅니다.
좋은 글 잘 읽었습니다.