Kotlin lateinit vs lazy — 언제 뭘 써야 할까?

hbj0209·2025년 11월 24일

Kotlin

목록 보기
1/2
post-thumbnail

lateinit

개념

lateinit은 나중에 반드시 초기화할 것이라는 보장을 컴파일러에게 알려주는 키워드입니다.

→ 즉, “지금 초기화 못 하지만, 사용되기 전에 꼭 초기화할 거야”라고 선언하는 것.

특징

  • var만 가능 (val 불가능)
  • 기본 타입(Int, Boolean 등 primitive)은 불가능
  • nullable이 아님 (null 허용 X)
  • 초기화되기 전에 접근하면 UninitializedPropertyAccessException 이 발생합니다

언제 사용?

Android ViewBinding, ViewModel, DI 객체 주입 등 '생성 시점에 초기화 불가능한 non-null 객체'를 다룰 때

private lateinit var binding: FragmentHomeBinding

override fun onCreateView(...) {
    binding = FragmentHomeBinding.inflate(inflater, container, false)
    return binding.root
}

사용하는 이유

  • binding: FragmentHomeBinding? 로 nullable로 만들고 binding?.textView 같은 null-safe 호출 쓰고 싶지 않기 때문
  • "나중에 초기화되지만 null은 절대 아니다" → 의도를 컴파일러에 명확히 전달

lazy

개념

lazy는 해당 프로퍼티가 처음 사용되는 시점에 초기화되는 지연 초기화 기법입니다.

특징

  • val만 가능
  • 스레드 안전 모드 설정 가능 (기본: SYNCHRONIZED)
  • 초기화 비용 큰 작업에 적합
// Adapter 지연 생성
private val adapter by lazy { VideoAdapter() }

// 무거운 객체 지연 생성
private val database by lazy {
    Room.databaseBuilder(
        context,
        AppDatabase::class.java,
        "app-database"
    ).build()
}

이 adapter는 실제로 접근되는 순간에만 생성됩니다.

비교

Nullable과의 비교

안드로이드 개발에서는 대부분

  • Activity/Fragment lifecycle 로직: lateinit
  • 무거운 객체 지연 생성: lazy
  • 진짜 null이 가능한 state: nullable

이렇게 구분해서 사용합니다.


LazyThreadSafetyMode 종류 및 성능 차이

Kotlin의 lazy는 스레드 안전성을 제어할 수 있는 3가지 실행 모드를 제공합니다.

LazyThreadSafetyMode.SYNCHRONIZED (기본값)

멀티 스레드 환경에서 완전히 안전한 기본 모드

  • 최초 초기화 시 synchronized 블록을 사용하여 동시 접근을 제어합니다.
  • 여러 스레드가 동시에 접근해도 단 한 번만 초기화됩니다.
  • Android에서는 대부분 UI 스레드에서만 실행되므로, 락(lock) 오버헤드가 불필요한 비용이 될 수 있습니다.

성능: 가장 느림(락 비용)

안전성: 멀티스레드 완전 보장

// 기본값 - 명시하지 않으면 SYNCHRONIZED 사용
private val adapter by lazy {
    UserAdapter()
}

LazyThreadSafetyMode.PUBLICATION

여러 번 초기화될 수 있지만, 최종적으로 하나의 인스턴스만 유지하는 모드

  • 여러 스레드가 동시에 초기화를 시도할 수 있습니다.
  • 하지만 결과적으로 하나의 인스턴스만 유지됩니다.
  • lock을 사용하지 않고 CAS(Compare-And-Swap) 기반으로 동작하여 성능이 빠릅니다.
  • 초기화 side effect가 없는 경우에 적합합니다.

성능: 중간(락 없음)

안전성: 멀티 스레드에서 동일 객체만 보존됨

private val config by lazy(LazyThreadSafetyMode.PUBLICATION) {
    AppConfig() // 초기화 시 부작용이 없는 경우
}

LazyThreadSafetyMode.NONE

스레드 안전성을 보장하지 않는 가장 빠른 모드

  • 스레드 안전성이 전혀 없습니다.
  • 락이나 동기화 메커니즘이 없어 초기화 비용이 가장 낮습니다.
  • Android UI 스레드에서 사용하는 대부분의 케이스에서 이 모드가 가장 효율적입니다.
  • 단일 스레드 환경이 보장되는 경우에만 사용해야 합니다.

성능: 가장 빠름

안전성: UI thread에서 사용 시 문제 없음

// UI 스레드에서만 접근하는 경우
private val adapter by lazy(LazyThreadSafetyMode.NONE) {
    UserAdapter()
}

Fragment에서 lateinit 사용 시 메모리 누수

class MyFragment : Fragment() {
    private lateinit var binding: FragmentMyBinding

    override fun onCreateView(...) : View? {
        binding = FragmentMyBinding.inflate(inflater, container, false)
        return binding.root
    }
}

onDestroyView()가 호출되면 View는 사라지지만 binding 변수는 Fragment가 메모리에서 사라질 때까지 계속 View를 참조하고 있습니다.

결과

  • View가 GC되지 않고 메모리에 계속 남습니다. (특히 RecyclerView + 큰 View 구조일 때 심각함)
  • 파괴된 뷰를 조작하려고 하면 IllegalStateException 발생 가능성이 있습니다.

해결법

private var _binding: FragmentHomeBinding? = null
private val binding get() = _binding!!

override fun onCreateView(...) : View {
    _binding = FragmentHomeBinding.inflate(inflater, container, false)
    return binding.root
}

override fun onDestroyView() {
    _binding = null
}
  • Fragment binding은 반드시 nullable + onDestroyView에서 null 처리
    • View Lifecycle에 맞춰 처리, 메모리 누수가 없습니다.
    • binding 접근 시점도 강제됩니다.

정리

  • lateinit: 나중에 초기화, var 전용, non-null 보장
  • lazy: 처음 사용 시 초기화, val 전용, 지연 생성
  • Android에서: Fragment ViewBinding은 nullable, 무거운 객체는 lazy
  • 메모리 누수 주의: Fragment의 ViewBinding은 onDestroyView에서 null 처리 필수

참고 자료

profile
안녕하세요!

0개의 댓글