본문 바로가기

코틀린

코틀린: 시퀀스(Sequence)와 컬렉션(Collection)은 뭐가 다를까요?

시퀀스(Sequence)에 관한 보충 설명입니다. 자세한 내용은 쉽게 다가가는 최신 프로그래밍: 코틀린 - 5.7 iterator와 sequence를 참고하기 바랍니다. 시퀀스와 컬렉션의 공통점은 "여러 개 원소를 다룰 수 있다" 입니다. 그러나, 원소를 저장하고 처리하는 방식은 전혀 다릅니다. 간단히 정리하면, 컬렉션은 열성적 처리(eager evaluation) 방식이며 시퀀스는 지연 처리(lazy evaluation) 방식입니다. eager는 "열성적인, 간절히 갈망하는" 뜻입니다. 컬렉션은 보자마자 달려드는 스타일인 반면, 시퀀스는 느긋하게 기다리는 스타일인 셈입니다.

여러 개 메소드를 연결해서 처리하는 메소드 체이닝(method chaining)으로 구현했을 때 유의해야 할 내용이 있습니다. 컬렉션은 매번 메소드를 실행하고 중간 처리 결과를 메모리에 저장합니다. 그러나, 시퀀스는 마지막 메소드를 호출하는 시점에 앞에서 처리를 지연시켰던 여러 개 메소드를 합쳐서 함께 처리합니다.

■ 컬렉션은 독립 연산을 처리할 때, 시퀀스는 연관된 연산들을 적용할 때 유리
아래 예에서 시퀀스 객체 numbers는 초기에 [1, 3, 9, 27, 81, 243] 등 6개의 원소를 갖지만, 마지막 출력 결과는 빈 리스트입니다. 시퀀스는 toList()를 호출한 시점에 map()과 filter() 연산을 함께 연결해 처리하며, 조건에 맞는 원소가 없기 때문에 빈 리스트를 즉시 출력합니다. 만약 아래 코드를 리스트로 구현했다면, map()을 적용한 처리 결과를 메모리에 저장합니다. 특히 filter() 또는 take()처럼 조건에 맞는 원소를 선택하는 메소드가 포함되었다면, 시퀀스가 성능 측면(처리 속도 및 메모리 사)에서 훨씬 효율적입니다.

fun main() {
    val numbers = generateSequence (1) { if (it > 50) null else it * 2 }
    val even = numbers
        .map { it }        
        .filter { it % 2 == 0 } 
        .toList()               
    println(even) // []
}

 

■ 시퀀스 객체 생성 방법
컬렉션 객체를 생성하는 방법은 쉽게 다가가는 최신 프로그래밍: 코틀린 5.4 리스트~ 5.6 Map에서 자세히 설명하고 있어 생략합니다. 물론 Sequence 객체를 생성하는 방법도 5.7 iterator와 sequence에서 일부 설명하고 있지만, 여기서 체계적으로 다뤄보겠습니다.

방법 1: 가장 대표적인 방법은 sequenceOf()를 사용해 시퀀스 객체를 생성하는 것입니다. 

val numbers: Sequence<Int> = sequenceOf(1, 2, 3, 4, 5)

방법 2:  asSequence()를 사용해 컬렉션 객체를 시퀀스 객체로 변환할 수 있습니다.

val numbers: List<Int> = listOf(1, 2, 3, 4, 5)
val numberSeq: Sequence<Int> = numbers.asSequence()

숫자 범위를 지정한 다음 asSequence()를 사용해 시퀀스 객체로 변환할 수 있습니다.

fun main() {
    val numbers = (1..5).asSequence()
    println(numbers.toList())  // [1, 2, 3, 4, 5]
}

방법 3:  generateSequence()는 초깃값과 함께 다음 원소를 정의하는 람다 식을 정의해 시퀀스 객체를 생성할 수 있습니다.  아래 예는 1부터 시작해서 무한대로 생성된 홀수 숫자 중 5개만 선택해 리스트 객체 [1, 3, 5, 7, 9]를 출력합니다.

fun main() {
    val numbers = generateSequence(1) { it + 2 }
    val subNumbers = numbers.take(5).toList()
    println(subNumbers)  // [1, 3, 5, 7, 9]
}

무한 시퀀스를 생성하기 보다는 중단 조건을 설정하여 제한된 시퀀스를 생성하는 방식을 더 선호합니다.

fun main() {
    val numbers = generateSequence(1) { if (it < 9) it + 2 else null }
    println(numbers.toList())  // [1, 3, 5, 7, 9]
}

 

방법 4:   마지막 방법은 sequence() 빌더를 사용해 좀 더 복잡한 형태의 시퀀스를 생성할 수 있습니다.

fun main() {
    val numbers = sequence {
        for (i in 1..5) {
            yield(i)  
        }
    }
    println(numbers.toList())
}