방학 기간 동안 그냥 놀려고 하니 좀이 쑤시고 노는 게 재미없어졌다… 그래서 리뷰어에게 부탁해서 방학 숙제를 얻어냈다!!! 문제 풀이와 문제에서 얻어낼 수 있는 중요 키워드에 대해 글을 작성한다.
문제
아래 코드를 실행하면 어떤 일이 일어날까?
x / y 중 하나가 먼저 초기화되고 다른 쪽이 그 값을 읽는다? 혹은 Deadlock??
class LazyLoop {
val x: Int by lazy { println("init x"); y + 1 }
val y: Int by lazy { println("init y"); x + 1 }
}
fun runQ1() {
println("=== Q1: by lazy 순환 ===")
val loop = LazyLoop()
val result = runCatching { loop.x }
println("Q1 result = ${result.exceptionOrNull()?.javaClass?.simpleName ?: loop.x}")
}
힌트
힌트 보기
by lazy의 기본 모드는LazyThreadSafetyMode.SYNCHRONIZED이다. 같은 스레드에서 재진입하면 어떻게 될까?
예측
runQ1에서 LazyLoop() 선언 → LazyLoop에서, x는 lazy인 y + 1을 하고, y는 lazy인 x + 1을 하고 있다.
그렇다면, x와 y가 호출될 때에 실제로 lazy{…}가 실행되는 게 아닐까?
runCatching에서 loop.x를 호출하니, println을 하고 y+1을 하겠지, 오류 나지 않을까? y가 호출되기 전인데 y를 호출하고 있으니 아무 값도 존재하지 않기에, 오류가 난다고 예상함.
실제 관측
…
init x
init y
init x
init y
Q1 result = StackOverflowError
init x와 init y가 무한하게 반복하다가 StackOverflowError가 발생하고 프로그램이 종료됨.
왜 그럴까?
loop.x 를 부르는 순간, 내부에서는 y + 1을 수행하기에 그 즉시 y lazy {…}를 수행함, 똑같이 내부에서 x + 1을 수행하기에 x lazy {…} 실행 무한 반복.
by lazy?
by lazy는 Kotlin의 프로퍼티 위임(property delegation) 문법 중 하나로, 값이 처음 사용되는 시점에 초기화되는 읽기 전용 프로퍼티를 만들 때 사용한다.
val expensive: String by lazy {
println("초기화 중...")
"결과값"
}
fun main() {
println("시작")
println(expensive) // 여기서 "초기화 중..."과 "결과값"이 출력됨
println(expensive) // "결과값"만 출력됨 (재계산 X)
}
lazy { } 는 lazy로 설정된 프로퍼티가 처음 호출될 때 lazy { } 가 실행되어 블록안에서 마지막으로 반환하는 값으로 한번만 설정한다.
왜 쓰는가?
주된 목적은 초기화 비용이 큰 객체를 꼭 필요할 때까지 미루는 것이다. 아래 코드 처럼, Android에서 Context가 필요한 객체나, 파일/네트워크에서 값을 읽어와야 하는 프로퍼티를 클래스 생성 시점이 아닌 실제 사용 시점에 초기화하여 최적화할 수 있다.
class MyFragment : Fragment() {
private val viewModel: MyViewModel by lazy {
ViewModelProvider(this)[MyViewModel::class.java]
}
}
val 전용
by lazy 는 한 번 초기화 되면 변하지 않는 값에만 사용할 수 있다. 실제로, 여러번 호출한다고 하여 재계산 되지도 않기에, val이 잘 어울린다. 값이 바뀌어야 한다면, lateinit var을 활용할 수 있겠다.
lateinit과의 차이
둘 모두, “나중에 초기화”하지만, 성격이 다르다. by lazy 는 val 만 사용 가능하고, 자동 초기화, 초기화 로직이 미리 정의된다. 반면에, lateinit var 는 var 사용, 수동으로 초기화, 원시 타입 사용 불가, nullable 불가라는 차이점이 있다.
스레드 안정성 모드
lazy() 는 따로 정의하지 않으면 기본적으로LazyThreadSafetyMode.SYNCHRONIZED 로 작동한다. 처음의 퀴즈는 단일 스레드에서 동작하는 코드이기에, 세 모드 모두 결국 재귀가 발생해 StackOverflowError로 귀결된다. 이 문제는 스레드 안정성 모드와는 무관하다. 모드와 상관없이 순환 참조 자체가 원인이다. 스레드 안정성 모드를 공부하기에 좋은 코드는 아니긴 하다 ㅎㅎ;
SYNCHRONIZED(기본): 스레드 안전, 락 사용PUBLICATION: 여러 스레드가 동시에 초기화 블록을 실행할 수 있지만, 먼저 완료된 값이 채택됨NONE: 락 없음, 단일 스레드 전용 (가장 빠름)
'안드로이드' 카테고리의 다른 글
| 안드로이드 원정대 - 왜 Intent가 필요할까?, Activity, IPC, Bundle (0) | 2026.05.06 |
|---|---|
| Q2. inline + non-local return vs labeled return (0) | 2026.04.26 |
| 리컴포지션 원정대 - Restart Scope, inline Composable (0) | 2026.04.19 |
| 리컴포지션 원정대 - Compose 리컴포지션 스코프와 remember의 관계 (0) | 2026.04.13 |
| 안드로이드 컴포즈 dp, sp 단위 (1) | 2026.03.30 |