Kotlin vs Java: Collection의 핵심 차이점과 다운캐스팅의 위험성

Murjune·2024년 1월 3일
0

kotlin/Java

목록 보기
1/5
post-thumbnail

0) Intro

이번 포스팅은 Effective kotlin의 한 예제로부터 시작되었다.

나는 listOf() 함수 가 생성한 List 객체가 is MutableList 검사에서 항상 true 값을 반환한다는 것에 이해가 가지않아 골머리를 앓았다..😥

listOf() 함수는 List의 인스턴스를 만드는 producer 함수라고 생각했었기에, 절대 MutableList 타입으로 변환할 수 없다고 생각했다.

(지금 글을 작성하면서 드는 생각인데, List는 interface인데 어떻게 List의 인스턴스를 만드는 녀석이라고 생각했을까.. 난 바보다)

아무튼, 이에 관련하여 알아보다가 머리속에서 정리가 되지 않아 글로 정리하고자 한다. :D

먼저, Java의 Collection과 kotlin Collection의 차이에 대해 알아보고 kotlin Collection 사용시 유의할 점에 대해 알아보자

kotlin Collection 사용법과 같은 자세한 내용은 생략하겠다.

1) Java의 Collection

Java의 Collection 중 List를 예시로 설명을 하겠다.

Java의 List 인터페이스는 자체적으로 불변성(Immutable)을 보장하지 않는다. Java의 Collection은 set(), add()과 같은 메서드를 지원하며, List가 이를 구현한다.

따라서, List 인터페이스 자체로는 해당 List 구현체가 immutable한지, mutable한지 구별할 수 없다.

  • 자바에서는 자주 사용하는 List들을 살펴보자
// 1) List.of : 완벽한 Immutable, null도 못들어감
List<String> immutableList = List.of("a", "b", "c"); 
immutableList.add("d"); // java.lang.UnsupportedOperationException
immutableList.set(0, "d"); // java.lang.UnsupportedOperationException

// 2) Arrays.asList : Array와 비슷, set 가능
List<String> sizeFixedList = Arrays.asList("a", "b", "c");
sizeFixedList.add("d"); // java.lang.UnsupportedOperationException
sizeFixedList.set(0, "d"); // ok

// 3) ArrayList : Mutable한 List
List<String> mutableList = new ArrayList<>(List.of("a", "b", "c"));
mutableList.add("d"); // ok
mutableList.set(0, "e"); // ok

위 예시처럼, Java에서 List 의 구현체들은 set(), add() 메서드를 지원하는 방식이 서로 다르다. 따라서 List의 구현체가 어떤 종류인지 모르면, List가 mutable한지 immutable한지 알 수가 없다.

public void cal(List<String> list) {
        list.add("d"); // // immutable? mutableList?
}

따라서, 따로 예외 처리를 해주거나 추가적인 로직 처리를 해주어야 한다.

이러한 상황은 Liskov 치환 원칙(LSP과 인터페이스 분리 법칙을 위배하는 대표적인 예시 중 하나다.

LSP 원칙 : 서브 클래스는 슈퍼 클래스/인터페이스에서 가능한 행위를 수행할 수 있어야 한다.
인터페이스 분리 법칙 : 객체는 자신이 호출하지 않는 메소드에 의존하지 않아야한다는 원칙

뭐 LSP 원칙을 위배하는건 흔히 있는 일이지만 표준 라이브러리(stdlib)에서 제공되는 Collection에서 이런 일이 발생하는 것은 다소 아쉽다..

2) Kotlin - MutableList, List

Kotlin은 java의 Collection와 달리 컬렉션을 변경 가능한 MutableCollection과 읽기 전용인 Collection으로 분리하였다.

  • MutableCollection은 add()와 set() 메서드를 포함해 변경 가능한 여러 작업을 지원
  • Collection은 add(), set()과 같은 변경 메서드를 제공 x -> 읽기 전용
  • MutableCollectionCollection을 상속

따라서, MutableCollection을 통해 변경 가능한 로직을 처리한 후, 인스턴스를 읽기 전용 Collection 타입으로 업캐스팅하여 안전하게 데이터를 보존하고 사용할 수 있다.

  • java와 다르게 kotlin의 List는 add()와 set()함수가 없음

따라서, 개발자는 필요에 따라 MutableCollection을 사용하여 데이터를 유연하게 처리할 수 있고, 필요한 시점에 읽기 전용 List로 전환하여 외부에서의 변경을 방지할 수 있다.

  • 예시)
fun List<Int>.plusOneAll(): List<Int> {
    val list = mutableListOf<Int>()
    for (i in this) {
        list.add(i + 1)
    }
    return list // ok (O)
    // return list.toList()  no (X)
}

val li: List<Int> = listOf(1, 2, 3).plusOneAll()

모든 요소에 1을 더하는 함수 plusOneAll() 함수이다.
내부 구현을 보면 구현 과정에서는 MutableList를 사용하고, 사용한 Collection을 바로 return 해주고 있다.
list.toList()와 같이 방어적 복사를 한 후 return하는 경우를 종종 보는데 그럴 필요 없다!

반환 값의 타입을 List로 지정함으로써, 결과가 변경 불가능하다는 것을 보장하기 때문이다.

3) kotlin Collection의 이점

이와 같이 Kotlin에서는 컬렉션의 가변성을 명확하게 구분함으로써 개발자에게 높은 수준의 자유도를 제공한다.

API를 구현하는 개발자는 내부적으로 가변성이 필요할 때 MutableCollection을 사용해 유연하게 데이터를 처리하고, 이를 읽기 전용인 Collection으로 업캐스팅함으로써 외부에서의 불필요한 변경을 방지할 수 있다.
API를 사용하는 개발자는 반환된 Collection이 읽기 전용임을 알고 있기 때문에, 이를 신뢰하고 사용할 수 있다.

4) 주의점

반환 받은 Collection을 사용할 때는 MutableCollection 타입으로 다운캐스팅해서는 안된다!!

Collection 인터페이스가 내부의 값까지 불변(Immutable)하다고 보장하지는 않는다는 것은 어찌 보면 당연하다. 위에서 본 plusOneAll() 예시에서도 실제로는 Mutable한 List를 반환하고 있지 않았는가?

보통 개발자가 내부값을 신경 쓸 일은 없으나, 반환받은 값을 조작해야할 때 DownCasting을 는 실수로 인해 문제가 발생할 수 있다.

이에 대해 2가지 예시를 통해 설명하겠다.

  • 예시 1)
class MenuRepository {
    fun fetchMenus(): List<String> {
        return listOf("menu1", "menu2", "menu3")
    }
}

class Restaurant {
    private var menus: List<String> = MenuRepository().fetchMenus()

    fun addMenu(menu: String) {
        require(menus.contains(menu).not()) { "이미 등록된 메뉴입니다." }

        // MutableList 일 경우
        if (menus is MutableList) {
            menus = (menus as MutableList<String>).apply { add(menu) } 
            // UnsupportedOperationException 발생
            return
        }
        
        // List 일 경우
        menus = menus + menu
        println(menus)
    }
}

Restaurant().addMenu("menu4")

새로운 메뉴가 레스토랑에 추가하려한다.
개발자는 극한의 효율을 추구하기 때문에 menus 인스턴스가 MutableList일 경우에는 add()를 사용하고, Mutable하지 않을 경우에는 plus() 연산자를 통해 메뉴를 업데이트하려 한다.

하지만, 메뉴를 추가할 경우 아래와 같은 예외가 발생한다.

이는 menus is MutableList 값이 true이기 때문이다.

간단히 설명해보겠다.

  • listOf() 를 통해 생성된 리스트는 Arrays$ArrayList 객체이다.
    (java.util.ArrayList가 아님을 유의!)
  • Arrays$ArrayList는 내부적으로 List의 서브 클래스이다.
  • Kotlin의 MutableList와 List는 모두 Java의 List와 대응된다.

정리를 하자면, Arrrays$ArrayList는 내부적으로 List의 서브 클래스이고, kotlin의 MutableList와 List는 둘다 java의 List와 대응되기 때문에 menus(ArrayList) is MutableList(List) 는 항상 true를 반환하게 된다. 🥵

그리고 Arrrays$ArrayList 는 add() 연산자를 지원하지 않기 때문에 UnsupportedOperationException 가 발생하는 것이다.

예시 2)

class MenuRepository {
    fun fetchMenuNames(): List<String> {
        return List(3) { "menu$it" }
    }
}

Restaurant().addMenu("menu4") // Ok

listOf()가 아닌 List()를 통해 리스트를 반환하면 또 다른 결과가 나온다.

이는 List() 를 통해 생성된 리스트는 java.util.ArrayList 이고 add() 연산자를 지원하기 때문이다.

이와 같이 반환 받은 Collection의 내부 값이 무엇인지 사용자 입장에서는 예측할 수 없기 때문에 MutableCollection 타입으로 다운캐스팅해서는 안된다.

만약, MutableCollection으로 바꿔 사용하고 싶다면 toMutableList() 함수를 통해 copy 후 새로운 mutable 컬렉션을 만든 후 사용하도록 하자!

✚ 참고

fun main() {
    val li = List(3) { it + 1 }
    val li2 = listOf(1, 2, 3)
    println(li.javaClass)

    if (li is MutableList) {
        println(li.javaClass) // class java.util.ArrayList
        li.add(1)
        println(li)
    }

    if (li2 is MutableList) {
        println(li2.javaClass) // class java.util.Arrrays$ArrayList
        li2.add(1) //  java.lang.UnsupportedOperationException
        println(li2)
    }
}

위의 예제를 통해 List()와 listOf()의 반환값의 타입이 다른 것을 알 수 있다.

6) 정리

이번 글에서 kotlin Collection이 java의 Collection과 다른 점에 대해 정리해봤고, MutableCollection을 Collection으로 다운캐스팅하면 안되는 이유에 대해서도 알아봤다.

사실 예시와 같이 예상치 못한 버그가 발생할 수 있어 다운캐스팅하면 안된다고 했지만, 설계적인 측면에서도 문제가 있다. 다운캐스팅은 추상화를 깨고 특정 구현에 대한 과도한 의존성을 생성한다. 이는 유연성과 확장성을 어렵게하여 유지보수를 어렵게 만들기 때문에 별 이유 없이 함부로 다운캐스팅하지말자!

참고

Effective Kotlin

https://discuss.kotlinlang.org/t/smart-cast-problem/6739/7

https://stackoverflow.com/questions/64853452/in-kotlin-why-does-mutablelistof-return-an-instance-of-java-util-arraylist-whi

https://kotlinlang.org/docs/collections-overview.html#collection-types
https://kim-jong-hyun.tistory.com/31

profile
열심히 하겠슴니다:D

0개의 댓글