프로그래밍에서 캐스팅(Casting)은 형변환을 의미한다. 데이터 타입을 다른 데이터 타입으로 변환하는 것을 말한다. 캐스팅 용어 중 업캐스팅과 다운캐스팅이라는 용어가 존재하는데 이에 대해 정리하였다.
업캐스팅은 자식 클래스의 객체를 부모 클래스 타입으로 변환하는 것을 말한다. 부모와 자식 관계가 상하관계라고 생각했을 때, 업캐스팅이라는 말이 더 와닿을 것이다. 말 그대로 위로 캐스팅하는 것이다.
abstract class Animal {
fun makeSound() {
println("울음소리")
}
}
class Dog : Animal() {
fun bark() {
println("멍멍")
}
}
fun main() {
val dog = Dog()
dog.makeSound() // "울음소리" 출력
dog.bark() // "멍멍" 출력
val animal: Animal = dog // 업캐스팅
animal.makeSound() // "울음소리" 출력
animal.bark() // 컴파일 오류 발생, Animal 타입이기 때문에 Dog의 메서드 호출 불가
}
예시 코드에서 dog 인스턴스는 Animal 클래스와 Dog 클래스의 메서드를 둘다 호출 가능하다. 하지만 animal 인스턴스는 dog 인스턴스를 Animal 타입으로 업캐스팅하였기 때문에, Animal 클래스의 메서드는 호출 가능하지만 Dog 클래스의 메서드는 호출 불가능하다.
여기서 좀 헷갈렸던 것이 dog 인스턴스 자체가 Animal 타입으로 바뀌는 것은 아니다. dog 인스턴스의 실제 타입은 Dog타입으로 유지되지만, animal 변수가 해당 인스턴스를 Animal 타입으로 참조하게 되는 것이다.
업캐스팅은 자식 클래스의 객체를 부모 클래스 타입으로 변환하는 것이라고 하였다. 그러면 다운캐스팅은 반대로 부모 클래스의 객체를 자식 클래스 타입으로 변환하는 것이라고 생각하면 될까?
abstract class Animal {
fun makeSound() {
println("울음소리")
}
}
class Dog : Animal() {
fun bark() {
println("멍멍")
}
}
class Cat : Animal() {
fun eat() {
println("먹이를 먹는다")
}
}
fun main() {
val animal: Animal = Dog() // 업캐스팅
val cat: Cat = animal as Cat // ClassCastException 발생
cat.eat()
val dog: Dog = animal as Dog // 다운캐스팅
dog.bark()
}
아까의 예시 코드에서 Cat 클래스를 추가하고 main() 블록 내용을 약간 변경했다. 만약 다운캐스팅이 단순히 부모 클래스의 객체를 자식 클래스 타입으로 변환하는 것이 맞다면, val cat: Cat = animal as Cat 이 코드는 원활하게 작동해야 한다. 하지만 막상 코드를 실행해보면 ClassCastException이 발생하면서 Cat 클래스로 캐스트할 수 없다는 메시지가 표시된다.
이를 통해 알 수 있는 것은, 다운캐스팅 개념은 단순히 업캐스팅의 반대가 아니라는 것이다. 제대로 된 개념은 업캐스팅되어 있던 객체를 다시 자식 클래스 타입으로 되돌리는 것을 말한다.
안드로이드에서의 예시를 작성해놓으면 좋을 것 같아서, 한 가지씩만 작성해봤다.
class ViewPagerAdapter(
activity: FragmentActivity
) : FragmentStateAdapter(activity) {
private val fragments: List<Fragment> = listOf(SearchFragment(), FavoriteFragment())
override fun getItemCount(): Int = fragments.size
override fun createFragment(position: Int): Fragment = fragments[position]
}
예시로 ViewPager2를 구현할 때 adapter 클래스를 만들기 위해 FragmentStateAdapter를 상속하는 코드를 가져와봤다. 여기서 fragments는 List<Fragment> 타입으로 선언되었고 SearchFragment 객체와 FavoriteFragment 객체를 리스트에 담았다.
SearchFragment와 FavoriteFragment는 Fragment의 자식 클래스이기 때문에 SearchFragment(), FavoriteFragment() 객체들이 Fragment 타입의 리스트에 저장될 때 자연스럽게 부모 클래스인 Fragment 타입으로 형변환(업캐스팅)이 이루어진 것이다. 즉, 객체들이 Fragment 타입으로 취급되면서 리스트에 저장된 것이다.
그래서 fragments 리스트의 요소를 참조할 때 Fragment 타입으로 참조가 가능하다. 그렇기 때문에 리턴값이 Fragment 타입으로 선언된 createFragment() 메서드의 리턴값으로 fragments[position] 작성이 가능한 것이다.
// MainActivity.kt
binding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) {
tab?.position?.let { position ->
val fragment: Fragment = fragments[position]
if (fragment is SearchFragment) {
// 다운캐스팅 : 사실 as SearchFragment 는 Kotlin가 스마트 캐스트를 지원하기 때문에 작성하지 않아도 된다
(fragment as SearchFragment).setSearchedData()
}
}
}
// ...
다운캐스팅의 예시로는 TabLayout에서 Fragment 객체를 내가 원하는 Fragment로 다운 캐스팅하는 예시를 가져와봤다. TabLayout에서는 여러 프래그먼트가 사용된다. 사용자가 탭한 프래그먼트가 어떤 프래그먼트일지 모르는 상황에서, 프래그먼트가 SearchFragment인 경우 다운캐스팅을 이용하여 해당 프래그먼트만이 가지고 있는 메서드를 호출하였다.