최근 회사에서 주로 자바를 사용하고 있지만, 개인적으로 코틀린을 즐겨 사용하며 새로운 것을 배우게 되어 그 경험을 공유하고자 한다.

이 글에서는 코틀린의 컬렉션 객체와 시퀀스 객체를 살펴보려 한다. 먼저 아래 코드를 통해 간단한 컬렉션 처리를 확인해보자.

val list = listOf(1, 2, 3, 4, 5)
val result = list.map { it * 2 }.toList()

이 코드에서 map(), filter(), flatMap()과 같은 중간 작업 함수를 사용하면 즉시 새로운 컬렉션 객체를 반환한다. 이는 자바의 Stream API와 유사한 개념이다.

그런데 코틀린에는 시퀀스라는 객체도 있다. 아래 코드를 살펴보자.

val list = listOf(1, 2, 3, 4, 5)
val result = list.asSequence().map { it * 2 }.toList()

asSequence()는 중간 연산 함수로써, 컬렉션 객체를 시퀀스 객체로 변환한다. 이렇게 하면 map() 함수를 적용할 때 즉시 계산하지 않고, 지연 계산이 적용된다.

이 원리를 이해하려면 Sequence의 로직을 살펴보면 된다. map() 중간 연산 함수를 호출했을 때, TransformingSequence라는 Sequence의 구현체가 반환된다. 이 객체는 일단 연산을 래핑하고, toList()와 같은 최종 연산 함수가 호출될 때 실행된다. 이 객체는 일종의 프록시 객체이다.

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

internal class TransformingSequence<T, R>
constructor(private val sequence: Sequence<T>, private val transformer: (T) -> R) : Sequence<R> {
    override fun iterator(): Iterator<R> = object : Iterator<R> {
        val iterator = sequence.iterator()
        override fun next(): R {
            return transformer(iterator.next())
        }

        override fun hasNext(): Boolean {
            return iterator.hasNext()
        }
    }

    internal fun <E> flatten(iterator: (R) -> Iterator<E>): Sequence<E> {
        return FlatteningSequence<T, R, E>(sequence, transformer, iterator)
    }
}

//toList() 내부적으로 이 함수를 사용하기 있기 때문에 예제 코드로 사용함.
public fun <T, C : MutableCollection<in T>> Sequence<T>.toCollection(destination: C): C {
    for (item in this) { // 이 때 계산
        destination.add(item)
    }
    return destination
}

결론

결국, 코틀린의 시퀀스를 사용하면 컬렉션 처리를 더 효율적으로 할 수 있다. 중간 연산이 즉시 계산되지 않고, 최종 연산 함수가 호출될 때까지 연산을 지연했다가 한 번에 계산되기 때문에 성능 향상과 자원 절약이 가능하다.

그러나 시퀀스를 사용하는 것이 항상 최선은 아닐 수 있는데, 예를 들어 작은 크기의 컬렉션에서는 시퀀스의 지연 연산이 큰 이점을 가져오지 않을 수 있다. 반면에 큰 데이터셋의 경우 시퀀스를 사용하면 성능 향상을 기대할 수 있기 때문에 상황에 따라 적절한 방법을 선택하는 것이 중요하다.

반성

코틀린의 Stream API로만 간주하고, asSequence()를 생략해도 동작하네?와 같은 안이한 생각이었던 것 같다. 그러나 최근에 이런 태도로 인해 코드 분석과 원리 이해에 필요한 노력이 미흡했음을 깨달으며, 이에 대한 성찰을 진행하게 되었다.