컬렉션 함수를 사용하면 간결하고, 효율적이고, 이해하기 쉬운 코드를 만들 수 있다.

또한, 컬렉션 함수는 아래처럼 연쇄 호출(method chaining)을 할 수 있다.

people.filter { it.name.startsWith("A") }.map { it.name }

하지만 단점이 없는 건 아니다. 연쇄 호출을 하면 매번 중간(임시) 컬렉션을 생성한다.
위 예시에서 filter, map의 결과로 컬렉션 객체가 2개 생성된다. 이것은 원소의 수가 수십만 개 이상일 때 효율을 떨어뜨린다.

이러한 상황을 효율적으로 바꾸고 싶으면 컬렉션을 직접 사용하는 대신 시퀀스(Sequence)를 사용해야 한다.

시퀀스 (Sequence)

people.asSequence()     // 원본 컬렉션을 시퀀스로 변환
    .map(Person::name)    // 시퀀스도 컬렉션과 같은 API를 제공한다.
    .filter { it.startsWith("A") }
    .toList() // 결과 시퀀스를 리스트로 되돌린다

위 코드는 이전 예시의 시퀀스 사용 버전이다.

시퀀스를 사용하면 중간 컬렉션이 생성되지 않아서 원소가 많을 때 성능이 매우 좋아진다.

시퀀스란?

시퀀스(Sequence)는 기본적으로 인터페이스이다.** 시퀀스 자체는 자신이 갖고 있는 데이터를 순서대로 반환할 수 있는 데이터 타입**임을 의미한다.

public interface Sequence<out T> {
    /**
     * Returns an [Iterator] that returns the values from the sequence.
     */
    public operator fun iterator(): Iterator<T>
}

인터페이스에는 iterator 메서드 하나만 정의되어 있는데, 반환되는 Iterator를 통해 원소를 하나씩 리턴받을 수 있다.

시퀀스가 중간 컬렉션을 생성하지 않는 이유는 이 인터페이스를 구현하고 있는 클래스의 연산 수행 방식에 있다.

시퀀스의 연쇄 연산 순서

시퀀스는 원소 단위로 연산을 수행한다.

listOf(1,2,3,4).asSequence() // 컬렉션을 시퀀스로 변경
        .map { print("map($it) "); it * it }
        .filter { print("filter($it) "); it % 2 == 0 }
        .toList() // 결과 시퀀스를 리스트로 되돌린다

컬렉션으로 위 연산을 진행한다면 모든 원소에 대해 map 함수를 수행하고, 그 결과 컬렉션에 filter 함수를 수행한다. 따라서, map에 의한 중간 컬렉션이 생성된다.


반면 시퀀스는 첫 번째 원소를 map 연산하고, 그 결과 값에 filter를 수행한다. 그리고 두 번째 원소에 대해 map -> filter 연산을 수행한다.

위 코드의 결과는 아래와 같다.

map(1) filter(1) map(2) filter(4) map(3) filter(9) map(4) filter(16)

이렇게 원소 단위로 연산을 연쇄 수행하기 때문에 중간 컬렉션이 생기지 않는다.

꼭 시퀀스를 컬렉션으로 되돌려야 하나

예시를 보면 마지막에 시퀀스를 리스트로 되돌린다. 시퀀스가 효율적이면 시퀀스를 계속 쓰는 게 좋을텐데 말이다.

시퀀스의 결과를 차례로 이터레이션 하기만 할거면 시퀀스를 써도 된다. 하지만 인덱스 접근과 같이 컬렉션에서 제공하는 기능을 사용해야 한다면 컬렉션으로 되돌려야 한다.

지연(lazy) 연산

시퀀스의 또 하나의 특징은 지연 연산을 한다는 것이다.

val sequence = List(1, 2, 3, 4).asSequence()
        .map { print("map($it) "); it * it }
        .filter { print("filter($it) "); it % 2 == 0 }

위 코드는 이전 예시에서 마지막 toList() 호출부를 제외한 것이다. 이 코드를 수행하면 아무런 내용도 출력하지 않는다. 시퀀스는 최종 시퀀스를 이터레이션하거나 리스트로 변환해야 비로소 연산이 수행되기 때문이다.

위 코드는 그저 어떤 연산을 수행해서 원소를 반환하는 시퀀스인지를 알려줄 뿐이다.

정리

원소의 개수가 많은 거대한 컬렉션에 연쇄적인 연산을 적용해야 할 경우 시퀀스를 사용하자.

시퀀스의 특징

  • 중간 컬렉션을 사용하지 않아서 연속적인 계산을 효율적으로 할 수 있다.
  • 지연 연산을 한다.

참조

책: Kotlin In Action

+ Recent posts