Kotlin Coroutine (2) : 시퀀스 빌더

Giyun Kim·2026년 3월 1일

Kotlin Coroutine

목록 보기
2/8

0. 들어가며

저번 글에서는 Kotlin Coroutine이 무언인지, 왜 필요한지를 살펴보았다. 이번 글에서는 마르친 모스카와의 'Kotlin Coroutine' 2장, 시퀀스 빌더를 읽어보도록 한다.

1. 시퀀스 빌더

1. 시퀀스 빌더란?

꽤 낯선 용어일 수도, 그렇지 않을 수도 있다.
이게 무엇인지, 어디에 사용하는 지 지금부터 알아보도록 한다.

개발자라면 모를 수 없는 Pyhton과 JS에서는 제한된 형태의 Coroutine을 사용한다.

  • 비동기 함수 async, await와 같은 호출 방식
  • 제너레이터 함수

한 번 쯤 오다가다 보았을 상기 두 가지가 바로 그 것이다.

저번 글의 예시 중 async, await는 이미 보았다.
Kotlin에서는 시퀀스를 생성할 때, 제너레이터 대신 시퀀스 빌더라는 것을 사용한다.

Kotlin에서 시퀀스란, Listset같은 컬렉션과 비슷한 개념이다. 다만, 필요할 때 값을 하나 씩 계산하는 지연처리 개념을 포함하고 있다.

  • 요구되는 연산을 최소한으로 처리
  • 무한정 연산도 가능
  • 메모리를 효율적으로 활용

상기 세 가지는 Kotlin 시퀀스의 특징이다.

위 특징으로 인해, 값을 순차 계산하고 필요 시 반환하는 빌더, 라는 것을 정의할 필요성이 있다. 이 때 시퀀스는 sequence라는 함수로 정의한다.

그럼, 반복문 쓰지, 뭐하러 sequence를 쓸까? 궁금증은 아래에서 해결하기로 한다.

1-2. 시퀀스 빌더 예시

시퀀스의 람다 표현식 내부에선 yield 함수를 호출해서 값을 생성한다.

val seq = sequence {
	yield(1)
    yield(2)
    yield(3)
}

fun main() {
	for (num in seq) {
    	print(num) /* 123 */
    }
}

여기서 사용된 sequence 함수는 짧은 도메인 전용 언어(DSL)다.

- 인자 : 수신 객체 지정 람다 함수 `suspend SequenceScope<T>.() -> Unit`
- 람다 내부 수신 객체 : `this`(Sequence Scope<T>를 가리킴.)

수신 객체 thisyield함수를 가지고 있는데, 해당 수신 객체가 암시적으로 사용되므로 yield(1)를 호출한다는 건 this.yield(1) 호출과 같다.

여기서 중요한 건? yield의 숫자가 미리 생기는 것이 아니라는 것이다.
즉, 각 숫자가 미리 생기는 것이 아니라, 필요할 때마다 생긴다.

아까 들었던 의문이 해소되지 않는가?
시퀀스의 작동 로직을 더 자세히 보도록 하자.

val seq = sequence {
	println("generating 1")
	yield(1)
    println("generating 2")
    yield(2)
    println("generating 2")
    yield(3)
}

fun main() {
	for (num in seq) {
    	print(num) 
        /**
          * generating 1
          * 1
          * generating 2
          * 2
          * generating 3
          * 3
         */
    }
}

도서 내용 중 일부를 가져왔다. 반복문과의 결정적인 차이는, 다른 수를 찾기 위해 멈췄던 부분부터 다시 시작한다는 것이다. 중단 체제가 없는 반복문에서 이런 행위는 불가능하다. 중단이 가능하기에 main 함수와 시퀀스 제너레이터가 번갈아가면서 수행될 수 있다.

sequenceiteratior를 제공한다.

var iter = seq.iterator
val first = iter.next()
...

이렇게 어떤 지점이든 상관없이 원할 때 이터레이터를 호출하면 빌더 함수에서 진행되었던 지점으로 되돌아간다!

Kotlin Coroutine 없이도 가능할까?
스레드로도 가능하긴 하지만, 처리할 때 막대한 비용이 든다.
Coroutine의 이터레이터는 자원을 거의 사용하지 않기에 부담도 없다. 어떻게 구현되었는지는 다음 게시글에서 다루도록 한다.

1-3. 시퀀스 빌더의 사용

그럼 좋다는 건 알겠다. 어디에 쓸까?
아주 간단한 예시로, 피보나치 수열을 들 수 있다.

val fibonacci : Sequence<BigInteger> = sequence {
	val first = 0.toBigInteger()
    val second = 1.toBigInteger()
    
    while(true) {
    	yield(first)
        val tmp = first
        first += second
        second = tmp
    }
}

fun main() {
	print(fibonacci.take(10).toList())
    /* [0, 1, 2, 3, 5, 8, 13, 21, 34] */
}

정확히 필요한 10개만 출력할 수 있게 된다.

2. 마치며

시퀀스 빌더를 아무렇게나 사용해도 될까?
시퀀스 빌더는 반환 역할을 하는 yield가 아닌 중단 함수는 사용할 수 없다. 따라서, 중단이 필요하다면 데이터를 가져오기 위해 후에 소개할 Flow를 사용하는 것이 낫다.

이번 게시물에서 우린 시퀀스 빌더와, 시퀀스가 작동하기 위해 중단이 필요한 이유를 알아보았다.
중단이 무엇인지는 알았으니, 어떻게 구현되는지는 다음 게시물에서 확인하도록 한다.

profile
Android 개발자가 되기까지

0개의 댓글