[Kotlin] Sequence에 대해 알아 보자!

환노·2025년 3월 16일

코드를 짜다 보면 List, Map 등, 컬렉션을 사용해서 데이터를 저장하는 경우가 많죠?

저장한 데이터 중 우리가 원하는 값을 가져오기 위해 컬렉션 함수를 쓰곤 합니다 🤔

우리가 흔히 쓰는 컬렉션 함수들은 매 단계마다 중간 계산 결과를 새로운 컬렉션으로 저장한다는 사실을 알고 계셨나요?

우선 간단한 코드로 컬렉션 함수가 어떻게 동작하는지 살펴 봅시다 ! 💪

fun main() {
    val list = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

    val result = list
        .map { it * 2 }
        .first { it > 10 }
}

위와 같이 1부터 10까지의 정수를 저장한 리스트가 있습니다
이 리스트에서 각 요소들을 2배로 증가시키고, 그 값들 중 10이 넘는 첫번째 값을 원한다고 가정해 봅시다 🙃

컬렉션 함수들은 순차적으로 호출될 때마다 실행되기 때문에 map부터 호출될 것입니다
map 이 실행되고 중간에 저장된 리스트는 아래와 같은 형태가 되겠죠

List(2, 4, 6, 8, 10, 12, 14, 16, 18, 20)

이후에 first 함수가 실행되고 결과는 다음과 같이 나올 겁니다 !

12

우리가 궁극적으로 원하는 값은 12 하나인데, 모든 요소를 돌며 map 로직이 적용되는 것이 비효율적이다! 라는 생각이 들지 않나요?

또한 연쇄적으로 호출하는 함수가 많아질 때, 매번 중간 결과를 새로운 컬렉션으로 저장하는 것이 비효율적이다! 라는 생각도 듭니다 🤔

우리는 이런 비효율적인 작업을 Sequence를 통해 해결할 수 있습니다 🔥

📌 Sequence를 사용하면 효율적인 이유

Sequence를 사용하면 중간 임시 컬렉션을 사용하지 않고, 컬렉션 연산을 연쇄할 수 있습니다

fun main() {
    val list = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

    val result = list.asSequence()
        .map { it * 2 }
        .first { it > 10 }
}

사용 방법은 간단히 .asSequence() 함수를 붙여 주면 됩니다! 🥸

sequence 연산에서 map중간 연산(intermediate operation)으로, 시퀀스 별로 수행하는 작업에 추가됩니다

first최종 연산(terminal operation)으로, 마지막에 실행되는 연산입니다

모든 중간 연산은 최종 연산을 만날 때까지 지연됩니다 ( Lazy Evaluation )
즉, map은 호출 시 바로 실행되지 않고 객체에 함수로 저장됩니다

이후 first가 호출되고 Sequence의 첫번째 원소부터 조건을 확인하겠죠!
이 과정에서 각 원소에 map이 실행됩니다 🔥

글로만 보면 이해가 안 되니 그림으로 알아 볼까요? 😵‍💫


Collection

도형들의 색깔을 바꾸고, 모양이 삼각형인 첫번째 원소를 가져오는 로직입니다

위 로직은 아래 플로우로 진행됩니다

전체 원소에 대해 색깔을 바꾸는 map이 모두 실행된 후 임시 컬렉션에 저장합니다
임시 저장된 컬렉션을 대상으로 first 가 실행되죠

Sequence

Sequencemap이 먼저 실행되지 않고, 지연됩니다

map 을 지나쳐 최종 연산인 first 연산을 만났으니 시퀀스의 앞 원소들부터 실행될 겁니다

  1. 동그라미를 대상으로 map, first를 실행한다
  2. first 조건에 맞지 않으니 다음 원소로 넘어간다
  3. 네모를 대상으로 map, first를 실행한다
  4. first 조건에 맞지 않으니 다음 원소로 넘어간다
  5. 세모를 대상으로 map, first를 실행한다
  6. 조건을 충족하므로 세모를 반환하며 sequence 연산이 끝난다

플로우를 그림으로 보면 위처럼 나타낼 수 있겠네요 🤔

여기서 Sequence의 두 가지 장점을 알 수 있습니다

Sequence의 두 가지 장점

1. 중간 연산이 모든 원소에 대해 적용되지 않는다

➡️ first 조건을 만족시키는 원소를 찾고 종료되었으므로, 별과 하트에 대해서는 map이 실행되지 않습니다

현재는 원소가 5개밖에 없어 크게 와닿지 않을 수 있지만, 원소가 100만개, 1000만개라면 어떨까요?
전체 원소를 순회하지 않는 것만으로도 오버헤드를 방지할 수 있겠죠 ! 💪

2. 중간 결과를 저장하지 않는다

// Collection.map()
public inline fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R> {
    return mapTo(ArrayList<R>(collectionSizeOrDefault(10)), transform)
}

// Sequence.map()
public fun <T, R> Sequence<T>.map(transform: (T) -> R): Sequence<R> {
    return TransformingSequence(this, transform)
}

이는 map 의 내부 로직을 보면 알 수 있습니다

Collection에서 호출되는 map은 inline function으로, 호출되는 즉시 실행되며 컬렉션을 반환합니다

Sequence에서 호출되는 map은, 최종 연산을 만날 때까지 객체에 저장되어야 하므로 inline function으로 구현될 수 없습니다
이는 최종 연산이 수행될 때, 각 요소들에 대해 next()를 통해 하나씩 변환됩니다

즉, 지연 실행을 활용하여 성능을 최적화하고, 필요할 때만 변환을 수행하는 방식인 거죠 ! 🙃


원소가 많지 않고, 연쇄 함수의 개수가 많지 않은 경우에는 컬렉션을 사용하는 것이 큰 오버헤드를 발생시키진 않을 겁니다

하지만 원소의 개수가 많아지고 연쇄 함수가 길어질수록 매번 변환하고 필터링 걸고 데이터를 뽑아 주는 작업은 비효율적일 수 있습니다 😵‍💫

이럴 때 Sequence를 사용해 오버헤드를 줄여 보는 건 어떨까요? ☺️

[참고 자료]
https://youtu.be/77hfjIYwouw?si=5xZPAN7LuK3NDQLu
https://kotlinlang.org/docs/sequences.html

3개의 댓글

comment-user-thumbnail
2025년 3월 16일

좋은 글 읽고 갑니다 🫡

이때까지 진행한 미션에서 Sequence를 어디에 적용해보면 좋을까요❓

답글 달기
comment-user-thumbnail
2025년 3월 20일

Sequence의 단점은 없나요?

1개의 답글