타입 검사와 캐스트
타입 검사와 캐스트 (Type checks and casts)
코틀린에서는 런타임에서 타입에 대해 두 가지를 할 수 있어요. 객체가 특정 타입인지 검사하거나, 다른 타입으로 변환하는 것이죠. 타입 검사는 지금 다루고 있는 객체가 어떤 종류인지 확인하는 데 도움을 주고, 타입 캐스트는 객체를 다른 타입으로 변환하려 시도합니다.
본문
제네릭 타입 검사와 캐스트(예: List<T>, Map<K,V>)에 대해 자세히 알아보려면 Generics type checks and casts를 참고하세요.
is와 !is 연산자로 검사하기
객체가 런타임에서 어떤 타입과 일치하는지 확인하려면 is 연산자(부정은 !is)를 사용해요.
fun main() {
val input: Any = "Hello, Kotlin"
if (input is String) {
println("Message length: ${input.length}")
// Message length: 13
}
if (input !is String) { // Same as !(input is String)
println("Input is not a valid message")
} else {
println("Processing message: ${input.length} characters")
// Processing message: 13 characters
}
}
is와 !is 연산자로 객체가 서브타입과 일치하는지도 확인할 수 있어요.
interface Animal {
val name: String
fun speak()
}
class Dog(override val name: String) : Animal {
override fun speak() = println("$name says: Woof!")
}
class Cat(override val name: String) : Animal {
override fun speak() = println("$name says: Meow!")
}
//sampleStart
fun handleAnimal(animal: Animal) {
println("Handling animal: ${animal.name}")
animal.speak()
// Use is operator to check for subtypes
if (animal is Dog) {
println("Special care instructions: This is a dog.")
} else if (animal is Cat) {
println("Special care instructions: This is a cat.")
}
}
//sampleEnd
fun main() {
val pets: List<Animal> = listOf(
Dog("Buddy"),
Cat("Whiskers"),
Dog("Rex")
)
for (pet in pets) {
handleAnimal(pet)
println("---")
}
// Handling animal: Buddy
// Buddy says: Woof!
// Special care instructions: This is a dog.
// ---
// Handling animal: Whiskers
// Whiskers says: Meow!
// Special care instructions: This is a cat.
// ---
// Handling animal: Rex
// Rex says: Woof!
// Special care instructions: This is a dog.
// ---
}
이 예시에서는 is 연산자로 Animal 클래스 인스턴스가 Dog인지 Cat인지 확인해서 관련 관리 지침을 출력하고 있네요.
객체가 선언된 타입의 슈퍼타입인지도 확인할 수 있는데, 답이 항상 참이라 별로 유용하진 않아요. 모든 클래스 인스턴스는 이미 자기 슈퍼타입의 인스턴스이기 때문입니다.
런타임에서 객체의 타입을 식별하려면 Reflection을 참고하세요.
타입 캐스트 (Type casts)
코틀린에서 객체의 타입을 다른 타입으로 변환하는 것을 캐스팅(casting)이라고 해요.
어떤 경우에는 컴파일러가 객체를 자동으로 캐스팅해줍니다. 이것을 스마트 캐스트(smart-casting)라고 부릅니다.
타입을 명시적으로 캐스팅해야 한다면 as? 또는 as 캐스트 연산자를 사용하세요.
스마트 캐스트 (Smart casts)
컴파일러는 불변(immutable) 값에 대한 타입 검사와 명시적 캐스트를 추적하면서 암시적(안전한) 캐스트를 자동으로 삽입합니다.
fun logMessage(data: Any) {
// data is automatically cast to String
if (data is String) {
println("Received text: ${data.length} characters")
}
}
fun main() {
logMessage("Server started")
// Received text: 14 characters
logMessage(404)
}
컴파일러는 부정 검사가 return으로 이어질 때 캐스트가 안전하다는 것까지 알아낼 만큼 똑똑합니다.
fun logMessage(data: Any) {
// data is automatically cast to String
if (data !is String) return
println("Received text: ${data.length} characters")
}
fun main() {
logMessage("User signed in")
// Received text: 14 characters
logMessage(true)
}
제어 흐름 (Control flow)
스마트 캐스트는 if 조건 표현식뿐 아니라 when 표현식에서도 동작해요.
fun processInput(data: Any) {
when (data) {
// data is automatically cast to Int
is Int -> println("Log: Assigned new ID ${data + 1}")
// data is automatically cast to String
is String -> println("Log: Received message \"$data\"")
// data is automatically cast to IntArray
is IntArray -> println("Log: Processed scores, total = ${data.sum()}")
}
}
fun main() {
processInput(1001)
// Log: Assigned new ID 1002
processInput("System rebooted")
// Log: Received message "System rebooted"
processInput(intArrayOf(10, 20, 30))
// Log: Processed scores, total = 60
}
그리고 while 반복문에서도요.
sealed interface Status
data class Ok(val currentRoom: String) : Status
data object Error : Status
class RobotVacuum(val rooms: List<String>) {
var index = 0
fun status(): Status =
if (index < rooms.size) Ok(rooms[index])
else Error
fun clean(): Status {
println("Finished cleaning ${rooms[index]}")
index++
return status()
}
}
fun main() {
//sampleStart
val robo = RobotVacuum(listOf("Living Room", "Kitchen", "Hallway"))
var status: Status = robo.status()
while (status is Ok) {
// The compiler smart casts status to OK type, so the currentRoom
// property is accessible.
println("Cleaning ${status.currentRoom}...")
status = robo.clean()
}
// Cleaning Living Room...
// Finished cleaning Living Room
// Cleaning Kitchen...
// Finished cleaning Kitchen
// Cleaning Hallway...
// Finished cleaning Hallway
//sampleEnd
}
이 예시에서 sealed 인터페이스 Status는 데이터 클래스 Ok와 데이터 객체 Error라는 두 구현을 가져요. currentRoom 속성은 오직 Ok 데이터 클래스에만 있습니다. while 반복문의 조건이 참으로 평가되면 컴파일러는 status 변수를 Ok 타입으로 스마트 캐스트해서, 루프 본문 안에서 currentRoom 속성에 접근할 수 있게 해줍니다.
Boolean 타입 변수를 if, when, while 조건에 사용하기 전에 선언한다면, 컴파일러가 그 변수에 대해 수집한 정보는 스마트 캐스팅을 위해 해당 블록 안에서 접근할 수 있어요.
이것은 불리언 조건을 변수로 추출하고 싶을 때 유용해요. 그러면 변수에 의미 있는 이름을 줄 수 있어서 코드 가독성을 높이고, 나중에 변수를 코드에서 재사용할 수도 있습니다. 예시를 볼게요.
class Cat {
fun purr() {
println("Purr purr")
}
}
//sampleStart
fun petAnimal(animal: Any) {
val isCat = animal is Cat
if (isCat) {
// The compiler can access information about
// isCat, so it knows that animal was smart-cast
// to the type Cat.
// Therefore, the purr() function can be called.
animal.purr()
}
}
fun main(){
val kitty = Cat()
petAnimal(kitty)
// Purr purr
}
//sampleEnd
논리 연산자 (Logical operators)
&& 또는 || 연산자의 왼쪽에 (정규 또는 부정) 타입 검사가 있으면, 컴파일러는 오른쪽에서 스마트 캐스트를 수행할 수 있어요.
// x is automatically cast to String on the right-hand side of `||`
if (x !is String || x.length == 0) return
// x is automatically cast to String on the right-hand side of `&&`
if (x is String && x.length > 0) {
print(x.length) // x is automatically cast to String
}
객체에 대한 타입 검사를 and 연산자(&&)로 결합하면, 컴파일러는 객체를 검사된 모든 타입으로 동시에 스마트 캐스트해요. 자세한 내용은 교차 타입을 참고하세요.
객체에 대한 타입 검사를 or 연산자(||)로 결합하면, 가장 가까운 공통 슈퍼타입으로 스마트 캐스트됩니다.
interface Status {
fun signal() {}
}
interface Ok : Status
interface Postponed : Status
interface Declined : Status
fun signalCheck(signalStatus: Any) {
if (signalStatus is Postponed || signalStatus is Declined) {
// signalStatus is smart-cast to a common supertype Status
signalStatus.signal()
}
}
공통 슈퍼타입은 유니온 타입의 근사치예요. 유니온 타입은 현재 코틀린에서 지원되지 않습니다.
교차 타입 (Intersection types)
컴파일러가 여러 && 검사를 통해 객체를 스마트 캐스트할 때, 교차 타입을 추론합니다. 이것은 검사된 제약을 모두 동시에 충족하는 내부 타입이에요.
interface Bird {
fun fly()
}
interface Fish {
fun swim()
}
fun describe(animal: Any) {
// Infers the Bird and Fish types
if (animal is Bird && animal is Fish) {
// Accesses fly() and swim() without additional checks or casts
animal.fly()
animal.swim()
}
}
교차 타입은 명명할 수 없는(non-denotable) 타입입니다. 타입 검사 중에 타입 정보를 보존하기 위해 컴파일러의 내부 타입 시스템에만 존재해요. 코틀린 코드에 직접 쓸 수는 없죠. 컴파일러 오류 메시지와 IDE 툴팁에서 교차 타입을 만날 수 있는데, 보통 A & B 형태로 표시됩니다. 유일한 예외는 명확히 non-nullable 타입을 선언하는 T & Any예요. 이 문법은 타입 매개변수를 Any와 결합하기 위해 특별히 예약되어 있습니다.
fun <T> T.assertNotNull(): T & Any = this ?: throw IllegalStateException("null value")
인라인 함수 (Inline functions)
컴파일러는 인라인 함수에 전달되는 람다 함수 안에서 캡처된 변수를 스마트 캐스트할 수 있어요.
인라인 함수는 암시적인 callsInPlace 계약을 가진 것으로 취급됩니다. 즉, 인라인 함수에 전달된 모든 람다 함수는 제자리에서 호출된다는 뜻이에요. 람다 함수가 제자리에서 호출되므로, 컴파일러는 람다 함수가 자기 함수 본문 안에 포함된 어떤 변수의 참조도 유출할 수 없다는 것을 알 수 있습니다.
컴파일러는 이 지식을 다른 분석과 함께 사용해서 캡처된 변수 중 어느 것을 안전하게 스마트 캐스트할지 결정합니다. 예시를 볼게요.
interface Processor {
fun process()
}
inline fun inlineAction(f: () -> Unit) = f()
fun nextProcessor(): Processor? = null
fun runProcessor(): Processor? {
var processor: Processor? = null
inlineAction {
// The compiler knows that processor is a local variable and inlineAction()
// is an inline function, so references to processor can't be leaked.
// Therefore, it's safe to smart-cast processor.
// If processor isn't null, processor is smart-cast
if (processor != null) {
// The compiler knows that processor isn't null, so no safe call
// is needed
processor.process()
}
processor = nextProcessor()
}
return processor
}
예외 처리 (Exception handling)
스마트 캐스트 정보는 catch와 finally 블록에도 전달됩니다. 컴파일러가 객체의 nullable 타입 여부를 추적하므로 코드가 더 안전해져요. 예를 들어:
//sampleStart
fun testString() {
var stringInput: String? = null
// stringInput is smart-cast to String type
stringInput = ""
try {
// The compiler knows that stringInput isn't null
println(stringInput.length)
// 0
// The compiler rejects previous smart cast information for
// stringInput. Now stringInput has the String? type.
stringInput = null
// Trigger an exception
if (2 > 1) throw Exception()
stringInput = ""
} catch (exception: Exception) {
// The compiler knows stringInput can be null
// so stringInput stays nullable.
println(stringInput?.length)
// null
}
}
//sampleEnd
fun main() {
testString()
}
스마트 캐스트 전제 조건 (Smart cast prerequisites)
스마트 캐스트는 컴파일러가 검사와 사용 사이에 변수가 바뀌지 않을 것을 보장할 수 있을 때만 동작해요. 다음 조건에서 사용할 수 있습니다.
| 스마트 캐스트 대상 | 조건 |
|---|---|
val 지역 변수 |
항상 가능. 단, 지역 위임 속성(local delegated properties)은 제외. |
val 속성 |
속성이 private 또는 internal이거나, 검사가 속성이 선언된 같은 모듈에서 수행되는 경우 가능. open 속성이나 커스텀 게터가 있는 속성에는 스마트 캐스트를 쓸 수 없음. |
var 지역 변수 |
검사와 사용 사이에 수정되지 않고, 수정하는 람다에 캡처되지 않으며, 지역 위임 속성이 아닌 경우 가능. |
var 속성 |
불가능. 다른 코드가 언제든지 변수를 수정할 수 있기 때문. |
as와 as? 캐스트 연산자
코틀린에는 as와 as? 두 가지 캐스트 연산자가 있어요. 둘 다 캐스팅에 쓸 수 있지만 동작이 다릅니다.
as 연산자로 캐스트가 실패하면 런타임에서 ClassCastException이 던져져요. 그래서 안전하지 않은(unsafe) 연산자라고도 합니다. as는 non-null 타입으로 캐스팅할 때 쓸 수 있어요.
fun main() {
val rawInput: Any = "user-1234"
// Casts to String successfully
val userId = rawInput as String
println("Logging in user with ID: $userId")
// Logging in user with ID: user-1234
// Triggers ClassCastException
val wrongCast = rawInput as Int
println("wrongCast contains: $wrongCast")
// Exception in thread "main" java.lang.ClassCastException
}
대신 as? 연산자를 쓰면 캐스트가 실패할 때 null을 반환해요. 그래서 안전한(safe) 연산자라고도 합니다.
fun main() {
val rawInput: Any = "user-1234"
// Casts to String successfully
val userId = rawInput as? String
println("Logging in user with ID: $userId")
// Logging in user with ID: user-1234
// Assigns a null value to wrongCast
val wrongCast = rawInput as? Int
println("wrongCast contains: $wrongCast")
// wrongCast contains: null
}
nullable 타입을 안전하게 캐스팅하려면 as? 연산자를 써서 캐스트가 실패할 때 ClassCastException이 터지는 것을 막으세요.
as를 nullable 타입과 함께 쓸 수도 있어요. 그러면 결과가 null일 수 있지만, 캐스트가 실패하면 여전히 ClassCastException을 던집니다. 이런 이유로 as?가 더 안전한 선택이에요.
fun main() {
val config: Map<String, Any?> = mapOf(
"username" to "kodee",
"alias" to null,
"loginAttempts" to 3
)
// Unsafely casts to a nullable String
val username: String? = config["username"] as String?
println("Username: $username")
// Username: kodee
// Unsafely casts a null value to a nullable String
val alias: String? = config["alias"] as String?
println("Alias: $alias")
// Alias: null
// Fails to cast to nullable String and throws ClassCastException
// val unsafeAttempts: String? = config["loginAttempts"] as String?
// println("Login attempts (unsafe): $unsafeAttempts")
// Exception in thread "main" java.lang.ClassCastException
// Fails to cast to nullable String and returns null
val safeAttempts: String? = config["loginAttempts"] as? String
println("Login attempts (safe): $safeAttempts")
// Login attempts (safe): null
}
업캐스팅과 다운캐스팅 (Up and downcasting)
코틀린에서는 객체를 슈퍼타입과 서브타입으로 캐스팅할 수 있어요.
객체를 자기 슈퍼클래스의 인스턴스로 캐스팅하는 것을 업캐스팅(upcasting)이라고 합니다. 업캐스팅에는 특별한 문법이나 캐스트 연산자가 필요 없어요. 예를 들어:
interface Animal {
fun makeSound()
}
class Dog : Animal {
// Implements behavior for makeSound()
override fun makeSound() {
println("Dog says woof!")
}
}
fun printAnimalInfo(animal: Animal) {
animal.makeSound()
}
fun main() {
val dog = Dog()
// Upcasts Dog instance to Animal
printAnimalInfo(dog)
// Dog says woof!
}
이 예시에서 printAnimalInfo() 함수가 Dog 인스턴스로 호출되면, 컴파일러는 기대되는 매개변수 타입이므로 이를 Animal로 업캐스팅합니다. 실제 객체는 여전히 Dog 인스턴스이므로, 컴파일러는 Dog 클래스에서 makeSound() 함수를 동적으로 결정해서 "Dog says woof!"를 출력합니다.
코틀린 API에서 동작이 추상 타입에 의존할 때 명시적 업캐스팅을 자주 보게 돼요. Jetpack Compose나 UI 툴킷에서도 흔한데, 보통 모든 UI 요소를 슈퍼타입으로 취급하고 나중에 특정 서브클래스로 작업하기 때문입니다.
val textView = TextView(this)
textView.text = "Hello, View!"
// Upcasts from TextView to View
val view: View = textView
// Use View functions
view.setPadding(20, 20, 20, 20)
// Activity expects a View type
setContentView(view)
객체를 서브클래스의 인스턴스로 캐스팅하는 것을 다운캐스팅(downcasting)이라고 해요. 다운캐스팅은 안전하지 않을 수 있으므로 명시적 캐스트 연산자를 사용해야 합니다. 실패한 캐스트에서 예외가 터지는 것을 피하려면, 실패 시 null을 반환하는 안전한 캐스트 연산자 as?를 권장해요.
interface Animal {
fun makeSound()
}
class Dog : Animal {
override fun makeSound() {
println("Dog says woof!")
}
fun bark() {
println("BARK!")
}
}
fun main() {
// Creates animal as a Dog instance with Animal
// type
val animal: Animal = Dog()
// Safely downcasts animal to Dog type
val dog: Dog? = animal as? Dog
// Uses a safe call to call bark() if dog isn't null
dog?.bark()
// "BARK!"
}
이 예시에서 animal은 Animal 타입으로 선언됐지만 Dog 인스턴스를 담고 있어요. 코드는 animal을 Dog 타입으로 안전하게 캐스팅하고 안전 호출(?.)을 사용해 bark() 함수에 접근합니다.
다운캐스팅은 직렬화에서 베이스 클래스를 특정 서브타입으로 역직렬화할 때 쓰게 돼요. 슈퍼타입 객체를 반환하는 Java 라이브러리를 다룰 때도 흔한데, 그런 객체를 코틀린에서 다운캐스트해야 할 수 있기 때문입니다.