원문: Effective Scala - Collections
Scala는 매우 범용적이고 풍부하며 강력하면서 조합 가능한 컬렉션 라이브러리를 제공합니다. 컬렉션은 고수준의 기능을 제공하면서 수많은 연산을 노출합니다. 많은 컬렉션 조작 및 변환 작업이 간결하고 가독성 있게 표현될 수 있지만, 이러한 기능을 부주의하게 적용하면 그 반대의 결과를 초래할 수 있습니다. 모든 Scala 프로그래머는 컬렉션 디자인 문서를 읽어보아야 합니다. 이 문서는 Scala collection library에 대한 동기와 통찰을 제공합니다.
항상 당신의 요구사항과 부합하는 가장 단순한 컬렉션을 사용하세요.
Hierarchy (계층 구조)
Scala의 컬렉션 라이브러리는 매우 거대합니다. 이 복잡한 계층 구조를 간단히 표현하자면 최상위에는 Traversable[T]가 존재하고, 그 아래에는 불변(immutable)과 가변(mutable)이라는 변형이 존재합니다. 아래의 그림은 불변 및 가변 계층의 중요한 차이점을 설명합니다.

Iterable[T]는 순회 가능한 모든 컬렉션을 의미하며, iterator 메소드와 foreach 메소드를 제공합니다.
Seq[T]는 순서가 있는 컬렉션을 의미합니다.
Set[T]는 수학적 집합으로, 중복되지 않은 항목들의 순서가 없는 컬렉션을 의미합니다.
Map[T]는 연관 배열이며, 역시 순서가 없는 컬렉션입니다.
Use (사용 지침)
불변 컬렉션을 사용하려고 노력하세요. 대부분의 경우 불변 컬렉션이 적합한 선택이며, 참조 투명성을 제공하므로 프로그램을 더욱 쉽게 이해할 수 있고 기본적으로 스레드로부터도 안전합니다.
가변 컬렉션을 사용하고 싶다면 mutable namespace를 명시하세요. import scala.collection.mutable._를 이용해 모든 불변 컬렉션을 불러오지 마세요. 가변 컬렉션이 사용되었다는 것을 명확히 알려주세요.
// 이렇게 사용하세요
import scala.collection.mutable
val set = mutable.Set()
// 이렇게 사용하지 마세요
import scala.collection.mutable._
val set = Set()
컬렉션 타입의 기본 생성자를 사용하세요. 언제든 순서가 있는 sequence가 필요하다면 Seq() 생성자를 사용하는 것이 좋습니다. Seq 이외 다른 컬렉션도 동일합니다.
// 이렇게 쓰세요
val seq = Seq(1, 2, 3)
val set = Set(1, 2, 3)
val map = Map(1 -> "One", 2 -> "Two", 3 -> "Three")
// 이렇게 쓰지 마세요
val seq = List(1, 2, 3)
이렇게 적으면 컬렉션의 의미를 구현에서 분리할 수 있으며, 컬렉션 라이브러리가 가장 적절한 선택을 할 수 있게 해줍니다. 당신은 Map이 필요한 거지, Red-Black Tree가 필요한 것이 아닙니다. 심지어, 이러한 기본 생성자는 특수한 용도에 맞는 특별한 타입을 생성해줍니다. 예를들어, Map()이 3개의 키를 가진 맵이라면 3개의 키를 가질 수 있게 특화된 맵의 구현을 사용하게 해줍니다 (collection 구현을 보면 갯수가 적으면 특수하게 구현된 collection 구현체를 사용하는 걸 확인할 수 있습니다).
위의 글을 통해서 추론하자면, 당신이 구현한 메소드나 생성자가 받는 컬렉션 타입은 가장 generic한 컬렉션 타입을 받을 수 있게 하세요. 이는 보통 Iterable, Seq, Set, Map 중 하나로 귀결됩니다. 만약 당신의 메소드가 sequence를 필요로 한다면, Seq[T]를 사용하고 List[T]는 사용하지 마세요.
한 가지 주의할 점은 scala.package에 정의된 Traversable, Iterable, Seq 타입은 scala.collection 버전입니다. 이와 대비되게 Predef.scala에 정의된 Map, Set은 scala.collection.immutable 버전입니다. 이를 통해 알 수 있는건, 기본적으로 사용하는 Seq 타입은 immutable과 mutable 모두 될 수 있다는 것입니다. 만약 Traversable, Iterable, Seq를 사용하고 불변성을 보장해야 한다면, 반드시 immutable 패키지를 import 해서 사용해야합니다. 그렇지 않을경우 다른 사용자는 mutable 버전의 타입을 넘길 수 있습니다.
Style (스타일)
함수형 프로그래밍 스타일은 불변 컬렉션의 변환을 파이프라인 방식으로 표현하는 경우가 많습니다. 이러한 방식은 매우 간결하게 표현되지만, 때때로 작성자의 의도를 파악하기 어렵게 하고, 암시만 되어있는 중간 결과를 추적하는 것을 힘들게 합니다. 한가지 예시로, 우리는 각 언어의 투표 결과를 하나로 모아 가장 투표수가 많은 값부터 적은 값으로 배열하려고 합니다.
val votes = Seq(("scala", 1), ("java", 4), ("scala", 10), ("scala", 1), ("python", 10))
val orderedVotes = votes
.groupBy(_._1)
.map { case (which, counts) =>
(which, counts.foldLeft(0)(_ + _._2))
}.toSeq
.sortBy(_._2)
.reverse
이 코드는 간결하고 올바르지만, 대부분의 독자는 작성자의 의도를 파악하기 어려울 것입니다. 이러한 의도를 명확히 하기 위한 하나의 방법은 중간 결과와 매개변수에 이름을 지정하는 것입니다.
// 개인적으로는 여기서 타입까지 명시하는게 더 이해하기 쉽다고 느낍니다
val votesByLang = votes groupBy { case (lang, _) => lang }
val sumByLang = votesByLang map { case (lang, counts) =>
val countsOnly = counts map { case (_, count) => count }
(lang, countsOnly.sum)
}
val orderedVotes = sumByLang.toSeq
.sortBy { case (_, count) => count }
.reverse
이 코드는 첫번째 방법처럼 간결하지만, 중간값에 이름을 지어주어 변환이 더욱 명확히 드러나고, 파라미터에도 이름을 지어주어 중간에 변형된 값이 더욱 명확합니다. 만약 이러한 스타일이 namespace에 영향을 끼친다고 느낀다면, 표현식을 중괄호({})로 감싸서 사용하세요.
val orderedVotes = {
val votesByLang = ...
// ...
}
Performance (성능)
고수준으로 추상화된 컬렉션 라이브러리는 성능을 예측하기 어렵게 만듭니다. 컴퓨터를 직접적으로 다루는 것에서 벗어날수록 (명령형으로 작성하는걸 말합니다) 특정 코드의 성능을 정확히 예측하기 더 어려워집니다. 그러나 정확성에 대한 추론은 일반적으로 더 쉽고, 가독성도 더 개선됩니다. Scala를 통해 성능을 바라보면 Java runtime에 비해 더욱 복잡합니다. Scala에서는 박싱/언박싱 작업이 숨겨져 있어 성능이나 메모리 사용에 상당한 영향을 미칠 수 있습니다.
저수준의 디테일을 챙기기 전에, 당신의 현재 상황에 맞는 적절한 컬렉션을 사용했는지 확인해보세요. 사용하는 데이터 구조가 예기치 않은 점근적인(asymptotic) 복잡도를 가지고 있는지 알아보세요. Scala 컬렉션의 복잡도는 여기에서 확인할 수 있습니다.
성능 최적화의 첫 번째 규칙은 왜 당신의 어플리케이션이 느린지 확인하는 것입니다. 데이터 없이 작업에 착수하지 마세요! 프로파일링을 통해 어플리케이션을 분석하세요. hot loops와 거대한 데이터 구조에 초점을 맞추세요. 과도한 최적화는 시간 낭비일 수 있습니다. Knuth의 격언을 기억하세요: "섣부른 최적화는 만악의 근원입니다".
좋은 성능과 공간 복잡도를 위해 저수준의 컬렉션을 사용하는게 유리할 때도 있습니다. 많은 데이터를 가지는 list를 사용하기보다 array를 사용하세요(불변 Vector 컬렉션은 array에 대해 참조 투명한 interface를 제공해줍니다). 만약 성능에 문제가 있다면 직접 sequence를 생성하기보다 buffer를 사용해보세요.
Java collections (자바 컬렉션과의 상호 운용)
scala.collection.JavaConverters를 사용해 자바 컬렉션과 상호 운용하세요. asJava와 asScala 변환 메서드를 제공하는 일련의 implicit 변환이 포함되어 있습니다. 이러한 변환은 명시적으로 이루어지므로 독자에게 도움이 됩니다.
import scala.collection.JavaConverters._
// asJava와 asScala로 상호변환이 가능합니다
val list: java.util.List[Int] = Seq(1, 2, 3, 4).asJava
val buffer: scala.collection.mutable.Buffer[Int] = list.asScala
원문: Effective Scala - Collections
Scala는 매우 범용적이고 풍부하며 강력하면서 조합 가능한 컬렉션 라이브러리를 제공합니다. 컬렉션은 고수준의 기능을 제공하면서 수많은 연산을 노출합니다. 많은 컬렉션 조작 및 변환 작업이 간결하고 가독성 있게 표현될 수 있지만, 이러한 기능을 부주의하게 적용하면 그 반대의 결과를 초래할 수 있습니다. 모든 Scala 프로그래머는 컬렉션 디자인 문서를 읽어보아야 합니다. 이 문서는 Scala collection library에 대한 동기와 통찰을 제공합니다.
항상 당신의 요구사항과 부합하는 가장 단순한 컬렉션을 사용하세요.
Hierarchy (계층 구조)
Scala의 컬렉션 라이브러리는 매우 거대합니다. 이 복잡한 계층 구조를 간단히 표현하자면 최상위에는 Traversable[T]가 존재하고, 그 아래에는 불변(immutable)과 가변(mutable)이라는 변형이 존재합니다. 아래의 그림은 불변 및 가변 계층의 중요한 차이점을 설명합니다.

Iterable[T]는 순회 가능한 모든 컬렉션을 의미하며, iterator 메소드와 foreach 메소드를 제공합니다.
Seq[T]는 순서가 있는 컬렉션을 의미합니다.
Set[T]는 수학적 집합으로, 중복되지 않은 항목들의 순서가 없는 컬렉션을 의미합니다.
Map[T]는 연관 배열이며, 역시 순서가 없는 컬렉션입니다.
Use (사용 지침)
불변 컬렉션을 사용하려고 노력하세요. 대부분의 경우 불변 컬렉션이 적합한 선택이며, 참조 투명성을 제공하므로 프로그램을 더욱 쉽게 이해할 수 있고 기본적으로 스레드로부터도 안전합니다.
가변 컬렉션을 사용하고 싶다면 mutable namespace를 명시하세요. import scala.collection.mutable._를 이용해 모든 불변 컬렉션을 불러오지 마세요. 가변 컬렉션이 사용되었다는 것을 명확히 알려주세요.
// 이렇게 사용하세요 import scala.collection.mutable val set = mutable.Set() // 이렇게 사용하지 마세요 import scala.collection.mutable._ val set = Set()
컬렉션 타입의 기본 생성자를 사용하세요. 언제든 순서가 있는 sequence가 필요하다면 Seq() 생성자를 사용하는 것이 좋습니다. Seq 이외 다른 컬렉션도 동일합니다.
// 이렇게 쓰세요 val seq = Seq(1, 2, 3) val set = Set(1, 2, 3) val map = Map(1 -> "One", 2 -> "Two", 3 -> "Three") // 이렇게 쓰지 마세요 val seq = List(1, 2, 3)
이렇게 적으면 컬렉션의 의미를 구현에서 분리할 수 있으며, 컬렉션 라이브러리가 가장 적절한 선택을 할 수 있게 해줍니다. 당신은 Map이 필요한 거지, Red-Black Tree가 필요한 것이 아닙니다. 심지어, 이러한 기본 생성자는 특수한 용도에 맞는 특별한 타입을 생성해줍니다. 예를들어, Map()이 3개의 키를 가진 맵이라면 3개의 키를 가질 수 있게 특화된 맵의 구현을 사용하게 해줍니다 (collection 구현을 보면 갯수가 적으면 특수하게 구현된 collection 구현체를 사용하는 걸 확인할 수 있습니다).
위의 글을 통해서 추론하자면, 당신이 구현한 메소드나 생성자가 받는 컬렉션 타입은 가장 generic한 컬렉션 타입을 받을 수 있게 하세요. 이는 보통 Iterable, Seq, Set, Map 중 하나로 귀결됩니다. 만약 당신의 메소드가 sequence를 필요로 한다면, Seq[T]를 사용하고 List[T]는 사용하지 마세요.
한 가지 주의할 점은 scala.package에 정의된 Traversable, Iterable, Seq 타입은 scala.collection 버전입니다. 이와 대비되게 Predef.scala에 정의된 Map, Set은 scala.collection.immutable 버전입니다. 이를 통해 알 수 있는건, 기본적으로 사용하는 Seq 타입은 immutable과 mutable 모두 될 수 있다는 것입니다. 만약 Traversable, Iterable, Seq를 사용하고 불변성을 보장해야 한다면, 반드시 immutable 패키지를 import 해서 사용해야합니다. 그렇지 않을경우 다른 사용자는 mutable 버전의 타입을 넘길 수 있습니다.
Style (스타일)
함수형 프로그래밍 스타일은 불변 컬렉션의 변환을 파이프라인 방식으로 표현하는 경우가 많습니다. 이러한 방식은 매우 간결하게 표현되지만, 때때로 작성자의 의도를 파악하기 어렵게 하고, 암시만 되어있는 중간 결과를 추적하는 것을 힘들게 합니다. 한가지 예시로, 우리는 각 언어의 투표 결과를 하나로 모아 가장 투표수가 많은 값부터 적은 값으로 배열하려고 합니다.
val votes = Seq(("scala", 1), ("java", 4), ("scala", 10), ("scala", 1), ("python", 10)) val orderedVotes = votes .groupBy(_._1) .map { case (which, counts) => (which, counts.foldLeft(0)(_ + _._2)) }.toSeq .sortBy(_._2) .reverse
이 코드는 간결하고 올바르지만, 대부분의 독자는 작성자의 의도를 파악하기 어려울 것입니다. 이러한 의도를 명확히 하기 위한 하나의 방법은 중간 결과와 매개변수에 이름을 지정하는 것입니다.
// 개인적으로는 여기서 타입까지 명시하는게 더 이해하기 쉽다고 느낍니다 val votesByLang = votes groupBy { case (lang, _) => lang } val sumByLang = votesByLang map { case (lang, counts) => val countsOnly = counts map { case (_, count) => count } (lang, countsOnly.sum) } val orderedVotes = sumByLang.toSeq .sortBy { case (_, count) => count } .reverse
이 코드는 첫번째 방법처럼 간결하지만, 중간값에 이름을 지어주어 변환이 더욱 명확히 드러나고, 파라미터에도 이름을 지어주어 중간에 변형된 값이 더욱 명확합니다. 만약 이러한 스타일이 namespace에 영향을 끼친다고 느낀다면, 표현식을 중괄호({})로 감싸서 사용하세요.
val orderedVotes = { val votesByLang = ... // ... }
Performance (성능)
고수준으로 추상화된 컬렉션 라이브러리는 성능을 예측하기 어렵게 만듭니다. 컴퓨터를 직접적으로 다루는 것에서 벗어날수록 (명령형으로 작성하는걸 말합니다) 특정 코드의 성능을 정확히 예측하기 더 어려워집니다. 그러나 정확성에 대한 추론은 일반적으로 더 쉽고, 가독성도 더 개선됩니다. Scala를 통해 성능을 바라보면 Java runtime에 비해 더욱 복잡합니다. Scala에서는 박싱/언박싱 작업이 숨겨져 있어 성능이나 메모리 사용에 상당한 영향을 미칠 수 있습니다.
저수준의 디테일을 챙기기 전에, 당신의 현재 상황에 맞는 적절한 컬렉션을 사용했는지 확인해보세요. 사용하는 데이터 구조가 예기치 않은 점근적인(asymptotic) 복잡도를 가지고 있는지 알아보세요. Scala 컬렉션의 복잡도는 여기에서 확인할 수 있습니다.
성능 최적화의 첫 번째 규칙은 왜 당신의 어플리케이션이 느린지 확인하는 것입니다. 데이터 없이 작업에 착수하지 마세요! 프로파일링을 통해 어플리케이션을 분석하세요. hot loops와 거대한 데이터 구조에 초점을 맞추세요. 과도한 최적화는 시간 낭비일 수 있습니다. Knuth의 격언을 기억하세요: "섣부른 최적화는 만악의 근원입니다".
좋은 성능과 공간 복잡도를 위해 저수준의 컬렉션을 사용하는게 유리할 때도 있습니다. 많은 데이터를 가지는 list를 사용하기보다 array를 사용하세요(불변 Vector 컬렉션은 array에 대해 참조 투명한 interface를 제공해줍니다). 만약 성능에 문제가 있다면 직접 sequence를 생성하기보다 buffer를 사용해보세요.
Java collections (자바 컬렉션과의 상호 운용)
scala.collection.JavaConverters를 사용해 자바 컬렉션과 상호 운용하세요. asJava와 asScala 변환 메서드를 제공하는 일련의 implicit 변환이 포함되어 있습니다. 이러한 변환은 명시적으로 이루어지므로 독자에게 도움이 됩니다.
import scala.collection.JavaConverters._ // asJava와 asScala로 상호변환이 가능합니다 val list: java.util.List[Int] = Seq(1, 2, 3, 4).asJava val buffer: scala.collection.mutable.Buffer[Int] = list.asScala