null을 허용하지 않으면서 초기화를 미루는 두 가지 방법.
lateinit — 나중에 내가 넣는다
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater) // 여기서 넣는다
setContentView(binding.root)
}
}
제약
- var 에만 (val 불가)
- 원시 타입 불가 (Int · Boolean 등)
- nullable 타입 불가
- 초기화 전에 접근하면 UninitializedPropertyAccessException
if (::binding.isInitialized) { ... } // 초기화 여부를 확인할 수 있다
lazy — 처음 쓸 때 알아서
private val adapter: UserAdapter by lazy {
UserAdapter { user -> onUserClick(user) } // 최초 접근 시 한 번만 실행된다
}
특징
- val 이다 (한 번 만들어지면 안 바뀐다)
- 기본이 스레드 안전 (LazyThreadSafetyMode.SYNCHRONIZED)
- 초기화 로직이 선언부에 붙어 있어 읽기 좋다
// 단일 스레드가 확실하면 동기화 비용을 없앨 수 있다
private val adapter by lazy(LazyThreadSafetyMode.NONE) { UserAdapter() }
무엇을 언제 쓰나
-
lateinit — 초기화 시점과 값을 '내가' 정해야 한다
- 예: onCreate 에서 주입받는 것 · 프레임워크가 나중에 넣어 주는 것
-
lazy — 값을 만드는 방법이 선언 시점에 이미 정해져 있다
- 예: 비싼 객체를 처음 쓸 때 만들고 싶다
-
판단: "만드는 방법을 지금 적을 수 있나?" → 있으면 lazy, 없으면 lateinit
남용하면
lateinit 을 남발하면 nullable 을 숨긴 것과 다르지 않다
- 초기화 전 접근이 런타임 예외로 터진다
- 생성자 주입으로 해결할 수 있으면 그게 가장 안전하다
Android 에서 Activity/Fragment 는 생성자를 쓸 수 없어 lateinit 이 관용구가 됐을 뿐, 일반 클래스에서는 다시 생각해 본다
Fragment의 뷰 바인딩 함정
// Fragment 는 뷰가 파괴돼도 인스턴스가 살아남는다
private var _binding: FragmentHomeBinding? = null
private val binding get() = _binding!!
override fun onDestroyView() {
super.onDestroyView()
_binding = null // 반드시 해제 — 안 하면 뷰 계층이 통째로 누수된다
}
여기서 lateinit을 쓰면 메모리 누수가 된다 — 뷰가 파괴돼도 참조가 남기 때문이다.
면접 함정
- ❌ "lazy는 lateinit보다 항상 낫다" → 값을 밖에서 받아야 하면
lazy로는 못 한다. - ❌ "lateinit은 null 검사가 필요 없다" → 초기화 전 접근은 예외다. 안전해진 게 아니라 시점을 옮긴 것이다.