컬렉션 — 불변과 가변
컬렉션 — 불변과 가변 (Mutable and Immutable Collections)
스칼라 컬렉션은 불변(mutable과 immutable)을 체계적으로 구분해요. 가변 컬렉션은 그 자리에서 갱신·축소·확장할 수 있고, 불변 컬렉션은 만들어진 뒤로 절대 바뀌지 않죠. 이 페이지에서는 두 계열이 어디에 살고, 기본적으로 어떤 걸 선택하게 되는지, 그리고 모든 컬렉션이 공유하는 공통 API를 살펴볼게요.
컬렉션의 세 패키지
모든 컬렉션 클래스는 scala.collection 패키지 또는 그 하위 패키지 mutable·immutable에 있어요. 클라이언트 코드에서 필요한 대부분의 컬렉션 클래스는 세 가지 변형으로 존재하는데, 각각 scala.collection, scala.collection.immutable, scala.collection.mutable 패키지에 위치해요. 각 변형은 가변성에 대해 서로 다른 특성을 가져요.
scala.collection.immutable 패키지의 컬렉션은 누구에게나 불변임이 보장돼요. 만들어진 뒤에는 절대 바뀌지 않으니까, 같은 컬렉션 값을 다른 시점에 여러 번 읽어도 항상 같은 원소를 가진 컬렉션을 얻는다는 사실을 믿을 수 있어요.
scala.collection.mutable 패키지의 컬렉션은 컬렉션을 그 자리에서 바꾸는 연산을 가진 것으로 알려져 있어요. 그래서 가변 컬렉션을 다룰 때는 어떤 코드가 어떤 컬렉션을 언제 바꾸는지 이해해야 해요.
scala.collection 패키지의 컬렉션은 가변일 수도, 불변이 될 수도 있어요. 예를 들어 collection.IndexedSeq[T]는 collection.immutable.IndexedSeq[T]와 collection.mutable.IndexedSeq[T]의 수퍼클래스예요. 일반적으로 scala.collection의 루트 컬렉션들은 컬렉션 전체에 영향을 주는 변환 연산을 지원하고, scala.collection.immutable의 컬렉션은 보통 단일 값을 추가·제거하는 연산을 더하고, scala.collection.mutable의 컬렉션은 루트 인터페이스에 부수 효과가 있는 수정 연산을 더해요.
루트 컬렉션과 불변 컬렉션 사이엔 또 하나의 차이가 있어요. 불변 컬렉션의 클라이언트는 아무도 그 컬렉션을 바꿀 수 없다는 보장을 받지만, 루트 컬렉션의 클라이언트는 스스로만 바꾸지 않겠다고 약속할 뿐이에요. 정적 타입이 수정 연산을 제공하지 않더라도, 런타임 타입은 가변 컬렉션일 수 있고 다른 클라이언트가 그걸 바꿀 수도 있거든요.
기본은 항상 불변
기본적으로 스칼라는 항상 불변 컬렉션을 골라요. 예를 들어 접두사 없이 그냥 Set이라고 쓰거나 다른 곳에서 Set을 import 하지 않은 채 쓰면 불변 집합을 얻고, Iterable이라고 쓰면 불변 iterable 컬렉션을 얻어요. 이들은 scala 패키지에서 기본으로 import 되는 기본 바인딩이기 때문이죠. 가변 기본 버전을 쓰려면 명시적으로 collection.mutable.Set이나 collection.mutable.Iterable이라고 써야 해요.
가변·불변 버전을 함께 쓰고 싶을 때 유용한 관례는 collection.mutable 패키지만 import 하는 거예요:
import scala.collection.mutable
그러면 접두사 없는 Set은 여전히 불변 컬렉션을 가리키고, mutable.Set은 가변 짝을 가리켜요.
컬렉션 계층의 마지막 패키지는 scala.collection.generic이에요. 이 패키지에는 구체적인 컬렉션들을 추상화하기 위한 building block들이 들어 있어요.
편의와 하위 호환을 위해 몇몇 중요한 타입은 scala 패키지에 별칭(alias)이 있어서, import 없이 간단한 이름으로 쓸 수 있어요. 예를 들어 List 타입은 다음과 같이 접근할 수 있어요:
scala.collection.immutable.List // that's where it is defined
scala.List // via the alias in the scala package
List // because scala._
// is always automatically imported
별칭을 가진 다른 타입으로는 Iterable, Seq, IndexedSeq, Iterator, LazyList, Vector, StringBuilder, Range가 있어요.
컬렉션 API 개요
가장 중요한 컬렉션 클래스들은 위 그림처럼 여러 계층에 걸쳐 있고, 이 클래스들 사이에는 공통점이 꽤 많아요. 예를 들어 모든 종류의 컬렉션은 컬렉션 클래스 이름 뒤에 원소를 나열하는 동일한 문법으로 만들 수 있어요:
Iterable("x", "y", "z")
Map("x" -> 24, "y" -> 25, "z" -> 26)
Set(Color.red, Color.green, Color.blue)
SortedSet("hello", "world")
Buffer(x, y, z)
IndexedSeq(1.0, 2.0)
LinearSeq(a, b, c)
같은 원리는 특정 컬렉션 구현에도 적용돼요:
List(1, 2, 3)
HashMap("x" -> 24, "y" -> 25, "z" -> 26)
이 모든 컬렉션은 toString으로 위에서 쓴 것과 같은 방식으로 표시돼요.
모든 컬렉션은 Iterable이 제공하는 API를 지원하되, 의미가 있는 곳에서는 타입을 특수화해요. 예를 들어 Iterable 클래스의 map 메서드는 결과로 또 다른 Iterable을 반환해요. 그런데 이 결과 타입은 서브클래스에서 오버라이드돼요. List에 map을 호출하면 다시 List가, Set에 호출하면 다시 Set이 나오는 식이죠:
scala> List(1, 2, 3) map (_ + 1)
res0: List[Int] = List(2, 3, 4)
scala> Set(1, 2, 3) map (_ * 2)
res0: Set[Int] = Set(2, 4, 6)
컬렉션 라이브러리 전체에서 구현되는 이 동작을 _균일 반환 타입 원리(uniform return type principle)_라고 불러요.
컬렉션 계층의 대부분 클래스는 루트·불변·가변 세 가지 변형으로 존재하지만, 유일한 예외로 Buffer 트레이트는 가변 컬렉션으로만 존재해요.
더 알아보기
- 컬렉션 소개 (Introduction) — 컬렉션 프레임워크의 개요
- Iterable 트레이트 — 모든 컬렉션의 공통 조상