fix_crash988

이 로그를 보면 앱이 크래시 난 원인이 명확히 보입니다.

핵심 크래시 원인 (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에서 bindingnull인데 접근하려고 해서 터졌습니다.

왜 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 터짐.

실제 발생 시나리오 재현 (로그로 유추)

  1. 대화 이력 탭(ConversationHistoryFragment)에 들어감
  2. 백그라운드로 나감 → onDestroyView() 호출 → _binding = null
  3. 다시 앱을 foreground로 올림 → Fragment는 재사용되지만 onCreateView()는 아직 호출되지 않음 (ViewState 복원 중)
  4. 그 사이에 Room이나 ViewModel에서 새로운 대화 데이터가 emit 됨
  5. observeConversations() 코루틴이 여전히 살아 있어서 updateUI() 호출 → binding이 null → 크래시

해결 방법 (추천 순위 순)

  1. 가장 안전한 방법 (권장) – 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
    }
}
  1. 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 쓰면 자동 처리됨
}
  1. 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% 해결됩니다.

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다