목차
1. 변수(Variable)
Kotlin은 val과 var로 이름을 선언한다. 둘의 차이는 그 이름에 새 값을 다시 할당할 수 있는지다. val은 한 번 할당한 뒤 재할당할 수 없고, var는 다시 할당할 수 있다. 둘 다 객체 내부가 자동으로 불변이 되는 것은 아니다.
fun main() {
val name = "Kim" // String으로 타입 추론
var count: Int = 1 // 타입을 명시
count += 1
println("$name: $count") // Kim: 2
// name = "Lee" // 컴파일 오류: val 재할당
}
val name = "Kim"에서 타입을 생략할 수 있는 것은 초기값으로부터 String을 추론하기 때문이다. 선언만 먼저 하고 나중에 값을 넣으려면 타입이 필요하다. 지역 val도 모든 사용 경로에서 한 번 할당되어야 한다.
val result: Int
result = 30
println(result)
val이 참조하는 컬렉션이 변경 가능한 종류라면 원소는 바뀔 수 있다. 완전히 바뀌지 않는 목록이 필요하다면 공개 API에 변경 메서드가 없는 List를 사용하고, 외부에서 들고 있는 변경 가능한 원본을 공유하는지도 확인한다.
val numbers = mutableListOf(1, 2)
numbers.add(3) // 가능: 객체 내용 변경
// numbers = mutableListOf(4) // 불가능: val 재할당
기본 타입과 문자열 템플릿
val age: Int = 20
val large: Long = 20L
val ratio: Double = 0.5
val small: Float = 0.5f
val letter: Char = '가'
val enabled: Boolean = true
println("나이: $age, 두 배: ${age * 2}")
$age는 단순 변수, ${age * 2}는 식의 값을 문자열에 넣는다. 큰따옴표는 String, 작은따옴표는 Char다. Int 변수에 Long 값을 자동 대입하는 암묵적 형변환은 하지 않으므로 필요한 경우 범위와 오버플로를 확인해 toInt() 등을 사용한다. 여러 타입을 한 변수로 받아야 하면 Any를 쓸 수 있지만 타입별 사용 전에는 스마트 캐스트나 명시적 검사가 필요하다. 함수가 의미 있는 결과를 반환하지 않을 때 쓰는 Unit은 Java의 void와 비슷한 역할을 한다.
초기화를 미룰 때: lateinit과 lazy
클래스가 만들어질 때 값을 준비할 수 없다면 초기화를 미룰 수 있다. lateinit var는 나중에 직접 할당하겠다는 약속이다. 초기화 전에 읽으면 UninitializedPropertyAccessException이 난다. 일반적인 지역 변수·속성에서는 가능한 한 생성자에서 값을 받는 편이 누락을 줄인다.
class Controller {
lateinit var message: String
fun show() {
check(::message.isInitialized) { "message를 먼저 설정하세요" }
println(message)
}
}
val controller = Controller()
controller.message = "Hello"
controller.show()
lateinit은 var 속성에 사용하며 Int, Boolean 같은 비 nullable 기본 타입에는 쓸 수 없다. ::message.isInitialized 검사는 해당 속성에 접근 가능한 범위에서 사용한다. 의존성 주입으로 나중에 값을 제공받는 경우에 볼 수 있지만 초기화 순서를 지키지 못하면 런타임 실패가 된다.
lazy는 처음 읽는 순간 초기화 식을 실행한다. 값을 처음 사용하기 전까지 계산을 미루면서 이후에는 같은 값을 재사용한다.
val configText: String by lazy {
println("설정 계산")
"mode=prod"
}
println(configText) // 계산 메시지를 출력한 뒤 mode=prod
println(configText) // 이미 계산된 mode=prod만 출력
lazy는 보통 val 속성에 위임한다. 기본 모드는 동기화되어 여러 스레드가 동시에 처음 읽어도 한 번만 초기화한다. 동기화 비용이 불필요하다는 근거가 있을 때에만 다른 LazyThreadSafetyMode를 선택한다. lazy 초기화 식이 외부 상태를 읽는다면 최초 접근 시점의 상태가 고정된다는 점도 확인해야 한다.
| 요구 | 선택 | 주의할 점 |
|---|---|---|
| 즉시 준비된 값, 재할당 불필요 | val | 참조 객체가 가변일 수 있음 |
| 계산 중 여러 번 갱신 | var | 변경 범위를 좁힐 것 |
| 외부 생명주기에서 나중에 주입 | lateinit var | 읽기 전 초기화 검사 |
| 첫 사용 때 한 번 계산 | val ... by lazy | 최초 접근 시점과 스레드 안전성 |
표에서 같은 “나중에 값이 생김”이라도 lateinit은 호출자가 값을 설정하고, lazy는 코드에 적은 식이 값을 만든다. 이 차이로 선택한다.
참고: Kotlin 기본 문법, 속성 문서, 위임 속성 문서