🍎 Kotlin List 사용 중 내부 코드에서 List가 MutableList를 사용하는 것을 발견해 어떤 이유로 MutableList를 사용하는지 정리합니다.
+ 또한 이에 적용된 설계 Practice를 정리하고 어떤 방식으로 설계에 녹여낼 수 있을지 정리합니다.
🍏 List 생성 내부 Code
✓ Immutable(불변)인 List를 호출하는 것을 기대했지만 MutableList를 호출하는 모습을 볼 수 있습니다.
🍏 MutableList 생성 내부 Code
✓ List 함수 바로 아래 위치해 있는 MutableList 함수는 ArrayList를 내부 변수로 선언한 후 반복을 통해 Element를 집어 넣습니다.
❓ 왜 List는 MutableList를 호출하는 것일까요??
✓ List 또한 MutableList 함수를 호출하여 내부에서 ArrayList를 호출하여 list를 반환하는데 말이죠
✓ 이유를 알기 위해선 MutableList Interface와 List Interface를 살펴봐야 합니다.
🍏 List Interface, MutableList Interface
✓ List Interface는 Collection Interface를 상속하고 List에 read-only를 허용한다고 설명되어 있습니다.
✓ MutableList Interface는 List Interface와 MutableCollection을 상속하여 Collection의 Element를 추가 삭제할 수 있다고 설명되어 있습니다.
(Plus, MutableCollection Interface는 Collection과 MutableIterable Interface를 상속합니다.)
✓ 상속 관계를 통해 알 수 있는 것은 MutableList Interface가 List Interface를 부모로 두고 상속을 진행하고 있다는 점입니다.
✓ 다시 List, MutableList 함수로 돌아가 함수 호출부가 아닌 반환값에 주목해야 합니다.
✓ List 함수는 MutableList를 호출하고 있지만 반환값은 List <T>이고, MutableList 함수의 반환값은 MutableList <T>입니다.
✓ Upcasting을 통해 MutableList Instance를 List로 Upcasting 하여 List를 반환하는 과정을 볼 수 있습니다.
❓ Upcasting은 무엇이며 위 코드에서 이유에서 사용한 것인가요?
✓ Upcasting이란 OOP에서 자식 클래스의 객체를 부모 클래스 타입으로 참조하는 것을 말합니다.
// 실제 객체는 여전히 ArrayList(MutableList 구현체)입니다
val mutableList = mutableListOf(1, 2, 3) // ArrayList 타입
val readOnlyList: List<Int> = mutableList // 업캐스팅
// 데이터는 그대로 유지됩니다
println(readOnlyList) // [1, 2, 3] 출력
// 원본을 변경하면 readOnlyList에도 반영됩니다 (같은 객체를 참조하기 때문)
mutableList.add(4)
println(readOnlyList) // [1, 2, 3, 4] 출력
✓ 코드에서 볼 수 있듯 List <Int>로 Upcasting 한 readOnlyList는 Element의 추가적인 조작이 제한되어 read_only만을 제공합니다.
✓ 리스트 안의 구성 요소를 변경하는 작업은 오직 원본인 mutableList를 통해서만 가능합니다.
✓ 이를 통해 볼 수 있는 Upcasting의 장점은 "접근 가능한 기능 제한"입니다.
❗️ List 함수에선 MutableList를 호출하지만 반환값을 List로 지정함으로 Upcasting 되어 요소들의 추가/제거 기능을 제한한 것입니다.
❓ Upcasting이 JVM Memroy Level에선 어떻게 구성되는지 궁금해졌습니다. 🐍
✓ 동물(부모) - 강아지(자식) 관계의 예제 코드를 통해 살펴봅니다.
open class Animal {
open fun makeSound() {
println("Some animal sound")
}
fun eat() {
println("Animal is eating")
}
}
// 자식 클래스
class Dog : Animal() {
// 부모 클래스의 메서드 오버라이드
override fun makeSound() {
println("Woof! Woof!")
}
// 자식 클래스만의 추가 메서드
fun fetch() {
println("Dog is fetching the ball")
}
}
fun main() {
// 자식 클래스 객체 생성
val myDog = Dog()
// 기본 상태 - Dog의 모든 메서드 사용 가능
myDog.makeSound() // 출력: Woof! Woof!
myDog.eat() // 출력: Animal is eating
myDog.fetch() // 출력: Dog is fetching the ball
// 업캐스팅: Dog 객체를 Animal 타입으로 참조
val animalRef: Animal = myDog
// 업캐스팅 후 접근 가능한 메서드
animalRef.makeSound() // 출력: Woof! Woof! (다형성에 의해 여전히 Dog의 메서드 호출)
animalRef.eat() // 출력: Animal is eating
// animalRef.fetch() // 컴파일 오류: Animal 타입에는 fetch() 메서드가 없음
println(myDog.toString()) // 출력: Dog@372f7a8d
println(animalRef.toString()) // 출력: Dog@372f7a8d
}
✓ 코드 블록은 런타임 때 인스턴스 생성 시 아래 도표와 같이 구성됩니다.
✓ 컴파일 단계에서 검증된 바이트 코드(. class)는 JVM 클래스 로딩 시점에서 클래스 내부 파일에 존재하는 정보 바탕으로 상속 관계, 메서드 등을 동적으로 Method 영역에 구성합니다.
✓ 다음으로 Instance 생성 시 myDog, animalRef 참조 변수들이 Stack 영역에 저장됩니다.
✓ 생성된 myDog을 Animal 부모 클래스로 Upcasting 되어 생성된 animalRef는 myDog와 같은 주소값을 갖게 됩니다.
❓ 동일한 주소 값을 갖게 되었는데 왜 animalRef는 fetch() 함수를 사용하지 못하는 것일까요?
✓ 이유는 컴파일링 시 Animal.class에 fetch() 함수가 존재하지 않기 때문입니다.
✓ 지금까지 ClassFile의 MetaData에서 상속 관계가 존재하니 해당 상속 관계에서 어떠한 필터링 과정을 통해 부모 인스턴스가 자식의 자식의 함수를 사용하지 못한 것으로 짐작하고 있었는데 그게 아닌 컴파일 시 부모 클래스의 정의를 검사하고 해당 메서드가 없음을 확인한 후 컴파일 파일 오류가 발생되는 것을 확인할 수 있었습니다.
❓ 타입 캐스팅이 되면 부모 인스턴스가 자식 인스턴스의 함수를 사용할 수 있는데 이 때는 어떤 과정을 거치는 건가요?
// 객체 자체는 여전히 Dog 타입임을 증명
if (animalRef is Dog) {
println("Reference is actually a Dog")
// 스마트 캐스트: Kotlin이 자동으로 Animal을 Dog로 캐스팅
animalRef.fetch() // 출력: Dog is fetching the ball
}
✓ 위 코드에선 명시적으로 타입이 주어집니다. 런타임 단계에서 타입이 지정되는 것이 아닌 컴파일러가 타입 체크 후 블록 내에서 animalRef를 Dog 타입으로 취급합니다.
✓ 컴파일 시간에 명시적인 캐스팅을 포함하는 바이트 코드를 생성하는 것이죠
✓ 결과적으로 런타임에서는 이미 객체가 Dog 타입이므로 캐스팅이 성공하고 fetch() 메서드가 정상적으로 호출됩니다.
🍎 결과
✓ List와 MutableList의 생성 과정을 파헤치면서 Upcasting과 Upcasting 시 JVM 메모리 레벨에서 어떤 일들이 발생하는지 알아봤습니다.
✓ 이번 글을 통해 지레짐작하고 있던 잘못된 지식을 바로 잡았고 무엇보다 큰 수확은 Upcasting을 사용했을 때 "접근 가능한 기능 제한"을 할 수 있다는 것을 알았다는 점입니다.
✓ 앞으로 모델을 설계할 때 모델의 성격에 따라 제한된 접근이 필요한 상황이라면 이번에 알게된 Practice를 활용해야겠습니다.
📚 Reference
✓ JVM Class 구조
Chapter 4. The class File Format
Attributes are used in the ClassFile, field_info, method_info, Code_attribute, and record_component_info structures of the class file format (§4.1, §4.5, §4.6, §4.7.3, §4.7.30). For all attributes, the attribute_name_index item must be a valid unsigne
docs.oracle.com
'TIL' 카테고리의 다른 글
Nginx Basic (0) | 2025.03.06 |
---|---|
원시 데이터가 DB에 존재하는 모습 (0) | 2025.03.05 |
[Tip]비즈니스 로직은 무엇인가요? (1) | 2024.09.05 |
Redis Container vs Embedded Redis 어떤 것을 선택해야 할까요? (0) | 2023.06.07 |
[Python] Multi Processing과 [Java] Multi Processing 차이 (0) | 2023.04.24 |