494
2
부
기본기 다지기
다른 일반적인 시나리오로는
O
(
1
)의 키를 기반으로 한 쓰기와 읽기가 필요한 경우, 즉 값을 키
를 가지고
immutable
.
Map
(
mutable
.
Map
)에 저장하는 경우가 있다. 마찬가지로
immutable
.
Set
(
mutable
.
Set
)을 사용해서 어떤 값의 존재 여부를 검사할 수 있다.
12.3
컬렉션 라이브러리의 설계 방식
설계상 문제점을 해결하고 재사용을 장려하기 위해 컬렉션 라이브러리가 사용하는 여러 기법이
있다. 그에 대해 설명하고, 그 과정에서 라이브러리에 있는 여러 ‘도우미’ 타입을 살펴보고, 각
각 구현에서 어떻게 사용되는지 알아보자.
12.3.1
Builder
앞에서 변경 가능한 컬렉션을 주의 깊게 사용하면 성능을 위한 좋은 타협안이 될 수 있다고 말
한 적이 있다. 실제로 컬렉션
API
는
map
과 같이 새로운 컬렉션을 만들어내는 연산에서 내부적
으로 변경 가능 컬렉션을 사용한다. 내부적으로는
map
과 같은 연산에서 새로운 컬렉션 인스턴
스를 만들어내기 위해
collection
.
mutable
.
Builder
(
http
://
bit
.
ly
/
1u174xC
) 트레이트를
구현한 것을 활용한다.
빌더는 다음과 같은 시그니처를 가진다.
trait Builder
[
-
Elem
,
+
To
]
{
def
+
=
(
elem
:
Elem
):
Builder
.
this
.
type
def clear
()
def result
():
To
...
//
위
세
추상
메서드로부터
파생된
다른
메서드들
}
Builder
.
this
.
type
이라는 범상치 않은 시그니처는 싱글턴 타입
singleton
type
이다. 이는
+=
메서
드가 오직 그 메서드가 호출된 대상
Builder
, 즉
this
의 인스턴스만 반환하도록 보장한다. 예
를 들어 어떤
Builder
의 구현이
this
대신 새로 만들어낸 인스턴스를 반환하려고 시도하면 타
입 체크를 통과할 수 없다. 싱글턴 타입에 대해서는
15
.
3
.
1
절 ‘싱글턴 타입’에서 다룰 것이다.
495
12
장
스칼라 컬렉션 라이브러리
다음은
List
를 위한 빌더 구현 예제다.
//
src
/
main
/
scala
/
progscala2
/
collections
/
ListBuilder
.
sc
import collection
.
mutable
.
Builder
class ListBuilder
[
T
]
extends Builder
[
T
,
List
[
T
]]
{
private var storage
=
Vector
.
empty
[
T
]
def
+
=
(
elem
:
T
)
=
{
storage
=
storage
:+
elem
this
}
def clear
():
Unit
=
{
storage
=
Vector
.
empty
[
T
]
}
def result
():
List
[
T
]
=
storage
.
toList
}
val lb
=
new ListBuilder
[
Int
]
(
1 to 3
)
foreach
(
i
=
>
lb
+
=
i
)
lb
.
result
//
결과
:
List
(
1
,
2
,
3
)
Vector
보다 더 효율적인 내부 저장 방식을 구현할 수도 있다. 하지만 위 예제는 빌더의 개념을
보여주기 위한 것일 뿐이다.
12.3.2
CanBuildFrom
수의 리스트에 대한 변환을 시도하는 간단한 예제를 보자.
scala
>
List
(
1
,
2
,
3
,
4
,
5
)
map
(
2
*
_
)
res0
:
List
[
Int
]
=
List
(
2
,
4
,
6
,
8
,
10
)
List
(
http
://
bit
.
ly
/
1toub3N
)에 있는 이 메서드의 단순화한 시그니처는 다음과 같다.
496
2
부
기본기 다지기
map[B](f: (A) =
>
B): List[B]
하지만 표준 라이브러리는 가능하면 재사용을 활용한다.
5
.
2
.
3
절 ‘사용 가능한 인스턴스 제한하
기’에서
map
이 실제로는
List
에 혼합된
scala
.
collection
.
TraversableLike
(
http
://
bit
.
ly
/
1yMk4pK
) 트레이트에 정의된 메서드라는 사실을 봤다.
map
의 실제 시그니처는 다음과 같다.
trait TraversableLike
[+
A
,
+
Repr
]
extends
...
{
...
def map
[
B
,
That
](
f
:
A
=
>
B
)(
implicit bf
:
CanBuildFrom
[
Repr
,
B
,
That
]):
That
=
{...}
}
Repr
은 내부적으로 원소를 저장하기 위해 사용하는 컬렉션의 타입이다.
B
는 함수
f
가 만들어내
는 원소의 타입이다. 따라서
That
은 우리가 만들어낼 대상 컬렉션의 타입 매개변수다. 이는 원래
컬렉션의 타입 매개변수와 같을 수도 있지만, 다를 수도 있다.
TraversableLike
는
List
와 같은 서브타입에 대해 아무것도 모른다. 하지만 암시적인
CanBuildFrom
인스턴스가 그런 정보를 모두 담고 있기 때문에 새로운
List
를 만들어서 반환
할 수 있다.
CanBuildFrom
은
Builder
인스턴스를 만들 수 있는 팩터리에 대한 트레이트다. 실제로 새로운
컬렉션을 점진적으로 만드는 일은
Builder
가 담당한다.
CanBuildFrom
기법의 단점은 실제 메서드 시그니처가 너무 복잡해진다는 것이다. 하지만
map
과
같은 연산에서 객체지향적인 재사용을 가능하게 해주는 기능 외에도,
CanBuildFrom
은 생성을
여러 가지 다른 방식으로 일반화하고 모듈화해준다.
예를 들어
CanBuildFrom
인스턴스가 여러 다른 구체적 컬렉션에 대한
Builder
를 인스턴스화
할 수도 있을 것이다. 보통은 같은 타입의 새로운 컬렉션이 반환되지만, 그렇지 않고 주어진 원
소에 따라 좀 더 효율적인 서브타입을 반환할 수도 있다.
예를 들어 원소가 많은
Map
은 키를 해시 테이블에 넣는 방식으로 가장 잘 구현할 수 있다. 그런
방식을 사용하면 분할 상환으로
O
(
1
)이 걸리는 저장과 검색을 수행할 수 있다. 하지만 크기가
497
12
장
스칼라 컬렉션 라이브러리
작은
Map
의 경우 그냥 원소를 배열이나 리스트에 넣는 것이 더 빠를 수도 있다.
n
이 작은 경우
실제로는 이런 구조에 대한
O
(
n
) 검색 연산이 해시 테이블을 사용한
O
(
1
) 연산보다 더 빠르
기 때문이다 (후자에 곱해지는 상수가 상당히 크므로 ).
입력 컬렉션 타입을 출력 컬렉션에 사용할 수 없는 경우도 있다. 다음 예제를 보자.
scala
>
val set
=
collection
.
BitSet
(
1
,
2
,
3
,
4
,
5
)
set
:
scala
.
collection
.
BitSet
=
BitSet
(
1
,
2
,
3
,
4
,
5
)
scala
>
set map
(
_
.
toString
)
res0
:
scala
.
collection
.
SortedSet
[
String
]
=
TreeSet
(
1
,
2
,
3
,
4
,
5
)
BitSet
(
http
://
bit
.
ly
/
1wO3JAt
)은 정수만 저장할 수 있다. 따라서 우리가 그것을 문자열의
집합으로 변환한다면, 암시적인
CanBuildFrom
은 다른 출력 컬렉션을 인스턴스화할 것이다. 여
기서는
SortedSet
(
http
://
bit
.
ly
/
1nWuXEr
)을 사용한다.
마찬가지로 문자열 (문자의 시퀀스 )의 경우 다음과 같은 상황을 볼 수 있다.
scala
>
"
xyz
"
map
(
_
.
toInt
)
res0
:
scala
.
collection
.
immutable
.
IndexedSeq
[
Int
]
=
Vector
(
120
,
121
,
122
)
CanBuildFrom
의 또 다른 장점은 원래 컬렉션이 알지 못하거나, 원래 컬렉션이 들고 다니기엔
적합하지 않은 문맥과 관련한 정보를 담아둘 수 있다는 것이다. 예를 들어 분산 컴퓨팅
API
를
만드는 경우 특별한
CanBuildFrom
인스턴스를 사용해서 원격 프로세스로 직렬화하기 위해 최
적화된 컬렉션 인스턴스를 생성할 수 있을 것이다.
12.3.3
...
Like
트레이트
Builder
와
CanBuildFrom
이 출력 컬렉션을 위한 타입 매개변수를 취한다는 사실을 봤다. 이런
타입 매개변수를 지정할 수 있게 하고, 구현 재사용을 장려하기 위해, 여러분이 아는 컬렉션은
대부분 자신에 대응하는
...
Like
트레이트를 혼합한다. 이런
...
Like
트레이트는 적절한 반환 타
입 매개변수를 추가하고, 공통 메서드에 대한 구현을 제공한다.
Get 프로그래밍 스칼라: 실용적인 스칼라 활용법을 익히는 가장 확실한 실전 바이블 (2.11.x 버전 기반) now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.