Life Logs && *Timeline

  • 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% 해결됩니다.