목차
서론
코틀린의 컬렉션은 기본적으로 자바의 그것을 그대로 사용하지만 불변 컬렉션과 가변 컬렉션으로 구분된다는 차이가 있다. 물론 자바에서도 불변 컬렉션을 만드는 것이 불가능하진 않지만 불변이라는 것을 컴파일 타임에 검증할 수 없고, 만드는 것도 조금 번거롭기 때문에 완전하다고 보기는 어렵다. 이 글에서는 코틀린의 불변 컬렉션과 가변 컬렉션에 대해 다룬다.
컬렉션(Collection)?
컬렉션은 수집이라는 단어 의미 그대로 다수의 객체를 수집해놓은 객체라고 볼 수 있다. 컬렉션은 크게 리스트(List), 집합(Set), 맵(Map)으로 나눌 수 있다.
- List : 인덱스를 통해 요소에 접근할 수 있는, 순서가 보장된 컬렉션. 중복 요소가 허용된다. 스택과 큐, 덱 등의 자료구조도 큰 의미에서 리스트에 포함된다고 볼 수 있다.
- Set : 순서를 보장하지 않고 각 요소가 고유하다는 것을 보장하는 컬렉션. 순서를 보장하지 않기 때문에 인덱스를 통해 요소에 접근할 수 없다.
- Map : key - value 쌍을 모아놓은 컬렉션. 모든 키는 중복되지 않으며 하나의 값에만 매핑된다. 값은 중복이 허용된다.
코틀린에서의 컬렉션
코틀린에서는 위의 세 종류의 컬렉션에서 나뉘어 가변 컬렉션(Mutable Collection)이라는 것이 존재한다.
위 그림에서 볼 수 있듯이 Map은 Collection 인터페이스의 구현체는 아니지만 컬렉션의 한 종류로 분류되기 때문에 함께 다룬다.
불변 컬렉션(Immutable Collections)
불변 컬렉션(Immutable Collections)는 컬렉션에 새로운 요소 삽입, 기존 요소 삭제 및 수정과 같은 변경이 허용되지 않고 오직 저장된 요소를 조회하는 것만 가능한 컬렉션이다. 위의 그림에서 Mutable-이 붙지 않은 Iterable, Collection, List, Set, Map은 모두 불변이 보장된다.
따로 조치를 취하지 않으면 기본적으로 가변 컬렉션인 자바와 달리 코틀린의 컬렉션은 불변을 지향하는 코틀린의 특성에 따라 기본적으로 불변 컬렉션 객체를 만들게 된다. 예를 들어, 자바의 경우 스트림을 통해 컬렉션 객체를 다루고 이를 리스트로 반환할 때 아래와 같은 코드를 사용할 것이다.
List<T> newList = list.stream()
.map( ... )
.collect(Collectors.toList());
여기서 collect(Collectors.toList())
를 통해 반환되는 리스트는 가변 리스트이기 때문에 값의 삽입과 삭제, 수정 등, 리스트의 상태를 마음껏 변경할 수 있다. 해당 결과를 불변 리스트로 받기 위해서는 collect(Collectors.toUnmodifiableList())
라는 별도의 방법을 사용해야 한다.
하지만 코틀린에서는 이렇게 컬렉션 객체를 다루는 메소드의 반환 타입이 모두 기본적으로 불변 컬렉션이다. 위의 자바 코드와 같은 기능을 하는 코틀린의 코드는 아래와 같다.
val newList = list.map { ... }.toMutableList()
코틀린에서는 map()
과 같이 컬렉션 객체의 요소들과 관련된 연산을 수행하고 컬렉션 객체를 반환하는 메소드들이 기본적으로 불변 리스트를 반환하기 때문에 위의 자바 코드와 같이 가변 리스트를 얻기 위해선 toMutableList()
를 추가적으로 호출해야 한다.
자바와 코틀린의 불변 컬렉션 객체 생성
자바에서 불변 컬렉션을 생성하는 방법으로는 위에서 기존의 가변 컬렉션이 존재하는 경우라면 Collections 클래스의 unmodifiableList()
, unmodifiableSet()
, unmodifiableMap()
의 파라미터로 기존 컬렉션을 전달하여 호출하는 것으로 기존 컬렉션의 요소가 복사된 불변 컬렉션을 생성할 수 있다.
각 컬렉션 타입에 따라 List.of()
, Set.of()
, Map.of()
를 사용하여 직접 값을 추가하여 생성하는 방법도 있는데 Set을 생성할 때 중복된 값을 넣거나 Map을 생성할 때 중복된 키를 넣으면 IllegalArgumentException
을 반환하게 된다.
코틀린에서는 컬렉션을 다루면서 컬렉션을 반환하는 메소드들이 불변 컬렉션을 반환하기 때문에 기존 컬렉션을 불변 컬렉션으로 변경할 일은 없다고 볼 수 있으나, 만약 기존의 가변 컬렉션을 불변 컬렉션으로 만들고 싶다면 해당 컬렉션에서 toList()
, toSet()
, toMap()
를 호출하는 것으로 손쉽게 변경할 수 있다.
그 외에도 자바와 같은 방식으로 직접 값을 추가하면서 컬렉션 객체를 생성하기 위해 listOf()
, setOf()
, mapOf()
를 사용할 수 있다. 이 때 자바와는 달리 Set을 생성할 때 중복된 값을 넣으면 중복된 값은 하나만 남게 되고, Map을 생성할 때 중복된 키를 넣으면 나중에 설정한 값이 저장된다. 또한, mapOf()
의 경우에는 키와 값을 묶은 Pair 객체를 파라미터로 전달해야 한다.
자바의 불변 컬렉션과 코틀린의 불변 컬렉션의 차이
자바의 불변 컬렉션과 코틀린의 불변 컬렉션은 구현 방식의 차이가 있다.
자바의 불변 컬렉션은 ImmutableCollections라는 별도의 클래스가 존재해서 컬렉션의 요소에 변경을 주는 모든 메소드가 UnsupportedOperationException를 발생시키도록 오버라이딩한 방식으로 구현되어 있다.
이러한 구현 방식으로 인해 자바의 불변 컬렉션은 서론에서 이야기한 것과 같이 컴파일 시점에 컬렉션의 변경을 감지할 수 없고 런타임에 컬렉션을 변경하려는 시도가 있어야만 예외를 발생시킨다는 문제가 있다.
반면에 코틀린의 불변 컬렉션은 구현 방식이 다르다. 상단의 이미지와 같이 코틀린의 컬렉션 인터페이스는 불변인 Collection과 가변인 MutableCollection으로 나뉘는데, List, Set, Map 인터페이스에는 add()
, remove()
, clear()
, set()
등의 컬렉션을 변경하는 메소드가 아예 존재하지 않는다.
코틀린의 불변 컬렉션은 변경 메소드가 존재하지 않는다는 특징으로 인해 변경을 시도하면 아예 컴파일 시점에 에러를 발생시킨다.
자바와 코틀린을 함께 사용할 때 주의할 점
자바에서 코틀린 코드로 작성된 컬렉션을 가져다 쓸 때는 해당 컬렉션 객체가 불변인지 가변인지 구분할 수 없다. 따라서 아래와 같이 코틀린의 불변 컬렉션을 자바에서 조회하여 새로운 요소를 추가하는 코드를 작성해도 어떠한 경고도 표시되지 않고 아무런 문제도 없이 실행되지만, UnsupportedOperationException이 발생하게 된다.
코틀린에서 자바 코드로 작성된 컬렉션을 가져다 쓸 때도 마찬가지의 상황이 발생한다. 이때도 동일하게 어떠한 경고도 표시되지 않고 아무런 문제도 없이 실행되지만, UnsupportedOperationException이 발생한다.
따라서 자바와 코틀린을 함께 사용할 땐 참조하는 컬렉션 객체가 불변인지 가변인지 확실하게 구분해서 사용하는 것이 좋을 것이다.
가변 컬렉션(Mutable Collections)
가변 컬렉션은 지금까지 이야기한 불변 컬렉션과는 달리 자바에서 일반적으로 사용하는 ArrayList, HashSet, HashMap 등과 같이 마음껏 변경이 가능한 컬렉션을 의미한다. 코틀린에서는 MutableCollection이라는 별개의 인터페이스가 존재하고, MutableCollection은 Collection을 상속하여 추가적으로 컬렉션을 변경하는 메소드들이 작성되어 있다.
코틀린의 가변 컬렉션으로는 MutableList, MutableSet, MutableMap이 존재한다고 되어 있는데 이들은 각각 구현체로 자바의 ArrayList, LinkedHashSet, LinkedHashMap을 사용한다. 다시 말해, 가변 컬렉션이 필요한 상황이라면 mutableListOf()
, mutableSetOf()
, mutableMapOf()
메소드를 사용하여 컬렉션 객체를 생성해도 되고, 그냥 자바와 동일하게 ArrayList, LinkedList, LinkedHashSet, HashSet, LinkedHashMap, HashMap 등의 컬렉션 클래스들의 생성자를 통해 컬렉션 객체를 생성해도 된다.
마치며
코틀린의 컬렉션은 모두 기본적으로 불변이기 때문에 일관성이 있고 컴파일 시점에 변경을 감지할 수 있다는 장점이 있다. 하지만 자바 코드와 혼용해서 사용하게 되면 이러한 장점이 희석되는 감이 있기 때문에 주의해서 사용하는 것이 좋을 것이다.
참조 링크
'Programming Language > Kotlin & Java' 카테고리의 다른 글
[Java/Kotlin] 필드(Field)와 프로퍼티(Property)는 무슨 차이가 있을까? (1) | 2023.05.02 |
---|---|
[Kotlin] Java와 Kotlin, 그리고 Lombok (0) | 2023.04.21 |
[Java] HashMap vs Hashtable(feat. ConcurrentHashMap) (2) | 2023.03.27 |
[Effective Java] 상속보다는 컴포지션을 사용하라(Feat. Stack) (0) | 2023.02.27 |
[Kotlin] 빌더 패턴과 코틀린, 그리고 Default Parameter와 Named Argument를 이용하여 코틀린에서 빌더 패턴의 효과를 내보기 (0) | 2023.02.13 |
댓글