이 로그를 보면 앱이 크래시 난 원인이 명확히 보입니다.
핵심 크래시 원인 (FATAL EXCEPTION)
java.lang.NullPointerException
at how.naver.translator1.ui.history.ConversationHistoryFragment.getBinding(ConversationHistoryFragment.kt:29)
at how.naver.translator1.ui.history.ConversationHistoryFragment.updateUI(ConversationHistoryFragment.kt:129)
→ ConversationHistoryFragment에서 binding이 null인데 접근하려고 해서 터졌습니다.
왜 binding이 null이 되었을까?
Kotlin + ViewBinding을 사용할 때 흔히 발생하는 패턴입니다. 대부분 이렇게 코드를 짰을 가능성이 99%입니다:
private var _binding: FragmentConversationHistoryBinding? = null
private val binding get() = _binding!! // 여기서 !!로 강제 언랩
그리고 onDestroyView()에서 이렇게 null로 만들어 줬을 겁니다:
override fun onDestroyView() {
super.onDestroyView()
_binding = null // 메모리 릭 방지
}
문제는 Fragment가 백그라운드에 있다가 다시 foreground로 돌아올 때
또는 Configuration change (화면 회전 등) 후에onCreateView() → onViewCreated()는 다시 호출되지만,
그 사이에 코루틴이 살아있어서 observe 중이었고,
데이터가 바뀌는 순간 updateUI()가 호출되는데
이때 binding이 아직 다시 inflate 되기 전이거나, 이미 onDestroyView()로 null이 된 상태였습니다.
결과적으로 binding이 null인데 getBinding()에서 !!로 강제 언랩 → NPE 터짐.
실제 발생 시나리오 재현 (로그로 유추)
- 대화 이력 탭(ConversationHistoryFragment)에 들어감
- 백그라운드로 나감 →
onDestroyView()호출 →_binding = null - 다시 앱을 foreground로 올림 → Fragment는 재사용되지만
onCreateView()는 아직 호출되지 않음 (ViewState 복원 중) - 그 사이에 Room이나 ViewModel에서 새로운 대화 데이터가 emit 됨
observeConversations()코루틴이 여전히 살아 있어서updateUI()호출 → binding이 null → 크래시
해결 방법 (추천 순위 순)
- 가장 안전한 방법 (권장) – binding을 안전하게 접근하도록 변경
private var _binding: FragmentConversationHistoryBinding? = null
private val binding: FragmentConversationHistoryBinding
get() = _binding ?: throw IllegalStateException("Binding is null!")
// 또는 더 간단히
private fun requireBinding() = _binding ?: return // early return용
그리고 updateUI() 안에서 이렇게:
private fun updateUI() {
_binding?.let { binding ->
// UI 업데이트 코드
} ?: run {
// binding이 없을 때는 아무것도 안 함 (또는 로그)
return
}
}
- observe는 onViewCreated에서, cancel은 onDestroyView에서 확실히
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
observeConversations()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
// 코루틴 job cancel은 ViewModelScope 쓰면 자동 처리됨
}
- repeatOnLifecycle 사용 (가장 깔끔함 – Android 추천 방식)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.conversations.collectLatest { conversations ->
updateUI(conversations)
}
}
}
}
→ 이 방법은 Fragment의 View가 살아 있을 때만 collect하고,onDestroyView() 되면 자동으로 코루틴이 suspend → NPE 원천 차단.
결론
크래시는 “Fragment의 binding을 null로 만든 후에도 백그라운드 코루틴이 UI 업데이트를 시도해서 생긴 전형적인 NPE”입니다.
repeatOnLifecycle 쓰거나, updateUI()에서 _binding?.let { }으로 안전하게 감싸면 100% 해결됩니다.
답글 남기기