본문 바로가기

개발/FRONT

Android Error : NullPointerException 완전 정리

728x90

 

이 글에서는 안드로이드 개발에서 가장 흔하게 만나는 오류인 java.lang.NullPointerException을 정리한다. 실제 액티비티/프래그먼트 코드 예제와 함께 1) 언제/왜 발생하는지, 2) 어떻게 해결하는지, 3) 처음부터 어떻게 예방할 수 있는지를 중심으로 설명한다.

 

 

 


1. NullPointerException은 언제, 왜 발생할까?

NullPointerException(이하 NPE)은 참조 타입 변수가 null 인데, 그 변수로 메서드 호출이나 프로퍼티 접근을 할 때 발생하는 런타임 오류다. 안드로이드에서는 특히 액티비티/프래그먼트 생명주기, 뷰 바인딩, Intent/Bundle 값, 네트워크 비동기 콜백에서 자주 등장한다.

1.1 프래그먼트에서 뷰 바인딩이 null 인 경우

프래그먼트는 onCreateView()에서 뷰를 만들고, onDestroyView()에서 뷰를 파괴한다. 이 사이가 아닐 때 binding으로 뷰에 접근하면 null이어서 NPE가 난다.

// 잘못된 예시
class ProfileFragment : Fragment() {

    private var binding: FragmentProfileBinding? = null

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        binding = FragmentProfileBinding.inflate(inflater, container, false)
        return binding!!.root
    }

    override fun onDestroyView() {
        super.onDestroyView()
        binding = null
    }

    fun updateName(name: String) {
        // 화면이 이미 사라진 뒤에 호출되면 binding == null 이라서 NPE 발생
        binding!!.textName.text = name
    }
}
  • 비동기 콜백에서 프래그먼트가 이미 종료된 뒤에 updateName()을 호출하는 경우
  • Navigation으로 다른 화면으로 이동한 뒤 이전 프래그먼트의 뷰를 건드리는 경우
핵심: 프래그먼트에서는 onCreateView()~onDestroyView() 구간에서만 뷰에 접근해야 한다. 그 외에는 언제든 null 일 수 있다.

1.2 Intent / Bundle / Arguments 값이 없을 때

다른 화면에서 putExtra/arguments로 값을 넘긴다고 “믿고” 코드를 짰지만, 실제로는 키 오타나 로직 변경으로 값이 안 넘어와서 null 인 경우에도 NPE가 터진다.

// 잘못된 예시
class DetailActivity : AppCompatActivity() {

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

        val title = intent.getStringExtra("title")   // null 일 수 있음
        // title.toString() 은 "null" 이 되지만,
        // 아래처럼 바로 length 를 접근하면 NPE
        val length = title!!.length
        findViewById<TextView>(R.id.textTitle).text = title
    }
}
  • 테스트용 진입 경로에서 putExtra("title", ...)를 안 넣은 경우
  • 딥링크/푸시 알림 등 외부 인텐트에서는 해당 키가 아예 없는 경우

1.3 비동기 네트워크 응답에서 화면이 이미 사라진 경우

네트워크 요청을 보낸 뒤, 사용자가 뒤로가기나 화면 전환을 해서 액티비티/프래그먼트가 사라졌는데 콜백 안에서 뷰에 접근하면 뷰가 null 이라서 NPE가 발생한다.

// 잘못된 예시
api.getUserProfile { result ->
    // 이 시점에 Activity 가 finish() 된 상태라면?
    findViewById<TextView>(R.id.textName).text = result.name  // NPE 가능
}
주의: 비동기 콜백에서는 항상 “이 화면/뷰가 아직 살아있나?”를 먼저 생각해야 한다. 화면이 파괴된 뒤 오는 콜백은 UI 를 건드리지 않도록 방어 코드가 필요하다.

2. NullPointerException 해결 방법

이미 NPE가 발생했다면, 먼저 Logcat 에 찍힌 stack trace 로 정확한 라인 번호를 찾는 것부터 시작해야 한다. 그 줄에서 어떤 변수가 null 인지 파악한 뒤, 생명주기·데이터 흐름을 따라가면서 “왜 null 인지”를 역추적한다.

2.1 프래그먼트 ViewBinding 패턴으로 수정

프래그먼트에서는 아래 패턴이 사실상 표준이다. _binding은 nullable, 외부에서는 binding 게터로만 접근하도록 해서 생명주기 밖 접근을 빠르게 발견할 수 있다.

// 수정된 예시
class ProfileFragment : Fragment() {

    private var _binding: FragmentProfileBinding? = null
    private val binding get() = _binding
        ?: throw IllegalStateException("View binding is only valid between onCreateView and onDestroyView")

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentProfileBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }

    private fun updateName(name: String) {
        binding.textName.text = name
    }
}
  • 뷰가 파괴된 상태에서 접근하면 NPE 대신 IllegalStateException으로 바로 문제를 인지
  • 디버깅 시 “생명주기 밖에서 접근했다”는 사실이 명확해져 원인 파악이 쉬워진다

2.2 Intent / Bundle 값 읽을 때 안전하게 처리

외부에서 넘어올 수 있는 값(Intent, arguments 등)은 항상 null 일 수 있다고 가정하고 방어 코드를 넣어야 한다.

// 안전한 예시 1: 기본값 제공
val title: String = intent.getStringExtra("title") ?: "제목 없음"
textTitle.text = title

// 안전한 예시 2: 필수 값이면 없을 때 명확히 종료
val userId = intent.getStringExtra("userId")
if (userId == null) {
    Toast.makeText(this, "잘못된 접근입니다.", Toast.LENGTH_SHORT).show()
    finish()
    return
}
// 여기부터는 userId 가 null 아님이 보장
loadUser(userId)
TIP: Navigation Component 의 SafeArgs 를 사용하면 필수 인자를 컴파일 타임에 강제할 수 있어, “필수 인자인데 null 이다” 류의 NPE 를 크게 줄일 수 있다.

2.3 Kotlin null-safety 연산자 적극 활용

Kotlin 에서는 safe call(?.), Elvis(?:), let 을 적절히 쓰면 NPE 를 대부분 회피할 수 있다. 반대로 !! 는 정말 확신할 때만 써야 하는 “비상용” 연산자다.

val user: User? = repository.getUserOrNull()

// 1) safe call
val nameLength = user?.name?.length

// 2) elvis 연산자
val displayName = user?.name ?: "손님"

// 3) let 사용
user?.let { u ->
    textName.text = u.name
}

2.4 비동기 콜백에서 화면 생존 여부 체크

네트워크 응답/코루틴/리스너 콜백에서 UI 를 만지기 전에는, 해당 액티비티나 프래그먼트가 아직 살아있는지 체크하는 습관을 들이면 NPE 를 크게 줄일 수 있다.

// 프래그먼트 예시
api.getUserProfile { user ->
    if (!isAdded) return@getUserProfile               // Activity 에 붙어있지 않으면 리턴
    if (view == null) return@getUserProfile           // 뷰가 이미 파괴된 경우

    _binding?.let { binding ->
        binding.textName.text = user.name
    }
}

3. NullPointerException 예방 방법

NPE 는 “발생한 뒤에 잡는 것”보다, 애초에 “발생하지 않게 설계하는 것”이 훨씬 효율적이다. 아래 습관들을 프로젝트 초반부터 적용해두면, 크래시 리포트에서 NPE 를 거의 보지 않게 된다.

3.1 Kotlin nullability 적극 활용 (Non-null 지향)

  • 기본은 non-null 타입으로 선언하고, 어쩔 수 없이 null 이 될 수 있는 경우에만 ? 붙이기
  • nullable 타입을 반환하는 함수는 이름/코멘트로 “null 이 나올 수 있다”는 것을 명확히 표현
// Bad
fun findUser(id: String): User? { ... }

// Better - null 가능성을 이름에 드러냄
fun findUserOrNull(id: String): User? { ... }

3.2 !! 연산자는 “마지막 수단”으로만 사용

!! 는 “여기서 null 이 나오면 앱 터져도 상관없다”는 의미와 같다. 테스트 코드나 정말 확실한 부분(예: DI 프레임워크가 주입해주는 필드) 외에는 되도록 사용하지 않는 것이 좋다.

// Bad
val nameLength = user!!.name!!.length

// Good
val nameLength = user?.name?.length ?: 0

3.3 프래그먼트 ViewBinding 템플릿 통일

프로젝트에서 프래그먼트를 새로 만들 때마다, 아래 패턴을 템플릿으로 복붙해서 쓰면 실수를 크게 줄일 수 있다.

class SampleFragment : Fragment() {

    private var _binding: FragmentSampleBinding? = null
    private val binding get() = _binding
        ?: throw IllegalStateException("View binding is only valid between onCreateView and onDestroyView")

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentSampleBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

3.4 크래시 리포트 도구로 NPE 모니터링

실 서비스에서는 Firebase Crashlytics 같은 크래시 리포트 도구를 반드시 붙여두고, 릴리즈 후에 실제 사용자 환경에서 어떤 NPE 가 발생하는지 모니터링하면서 하나씩 제거해 나가는 것이 좋다.

  • 스택 트레이스를 보고 “어떤 경우에 null 이 될 수 있는지”를 케이스 별로 정리
  • 재현이 안 되면, 로그/분기별 토스트/이벤트 트래킹을 넣어서 원인 좁히기

마무리

NullPointerException 은 안드로이드에서 가장 흔하게 보는 오류지만, Kotlin 의 null-safety, 올바른 ViewBinding 패턴, Intent/Bundle 방어 코드만 지켜도 대부분 예방할 수 있다.

이 글의 예제 코드를 프로젝트 템플릿 수준으로 가져다 쓰면, 실서비스에서 갑자기 터지는 NPE 크래시를 크게 줄일 수 있다. 앞으로 새로운 화면을 만들 때마다 “이 변수가 null 이 될 수 있을까?”를 한 번만 더 의심해보면, 훨씬 안정적인 앱을 만들 수 있다.

✅ 핵심 요약:
  • 프래그먼트에서는 ViewBinding 생명주기 패턴(_binding + binding 게터) 을 통일해서 사용한다.
  • Intent/Bundle/Arguments 값은 항상 null 가능성을 염두에 두고 기본값/에러 처리를 넣는다.
  • Kotlin 의 ?., ?:, let 을 적극 활용하고, !! 는 되도록 쓰지 않는다.
  • 비동기 콜백에서 UI 를 건드리기 전에 화면/뷰가 살아있는지 먼저 확인한다.
  • Firebase Crashlytics 등의 크래시 리포트 도구로 실제 서비스에서 발생하는 NPE 를 꾸준히 모니터링한다.

 

 아래 카카오톡 오픈 채팅 링크

https://open.kakao.com/o/seCteX7h

 

 

 

 

 

728x90