본문 바로가기
Study OR Book/Book

[코틀린 코루틴] 2장 시퀀스 빌더

by Baest 2025. 5. 20.

 

 

파이썬이나 자바스크립트 같은 언어에서는 아래와 같이 제한된 형태의 코루틴을 사용하고 있다.

1) 비동기 함수 (async/await)

2) 제너레이터 함수 (값을 순차적으로 반환)

 

코틀린에서는 제너레이터 대신 시퀀스를 생성할 때 사용하는 시퀀스 빌더를 제공한다.

코틀린의 시퀀스는 List나 Set 같은 컬렉션과 비슷한 개념인데, 필요할 때마다 값을 하나씩 계산하는 지연 처리를 한다.

 

시퀀스의 특징

- 요구되는 연산을 최소한으로 수행

- 무한정이 될 수 있음

- 메모리 사용이 효율적

   -> Q: 무한정. 메모리 효율?

 

시퀀스는 sequence라는 함수를 이용해 정의된다.

시퀀스의 람다 표현식 내부에서는 yield 함수를 호출하여 다음 값을 생겅한다.

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

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

 

 

시퀀스의 작동 방식: 중단이 가능하여, 사용되는 함수와 시퀀스 제너레이터가 번갈아가면서 실행되는 방식

 

 

[실제 사용 예제]

- 데이터베이스에서 대량의 데이터를 처리하거나, 원격 브라우저에서 수집한 데이터를 스트리밍 방식으로 처리

fun processLargeDataSet(): Sequence<ProcessedData> = sequence {
    // 데이터 소스에서 청크 단위로 데이터 가져오기
    var chunk = dataRepository.getNextChunk(chunkSize)
    
    while (chunk.isNotEmpty()) {
        // 각 아이템 처리 및 yield
        for (item in chunk) {
            val processedItem = processItem(item)
            yield(processedItem)
        }
        
        // 다음 청크 가져오기
        chunk = dataRepository.getNextChunk(chunkSize)
    }
}

// 사용 예
processLargeDataSet()
    .filter { it.isValid }
    .map { it.transform() }
    .take(100)
    .forEach { kafkaProducer.send(it) }

 

 

대량의 데이터 및 실시간 처리에 적합한데, 메모리 효율성이 높은 이유?

 

필요한 시점에만 계산 (Lazy Evaluation)

- 전체 데이터셋을 한 번에 메모리에 로드하지 않고, 필요한 부분만 로드

- 예시: 1백만 개의 레코드가 있을 때 List를 사용하면 모든 레코드를 메모리에 로드해야 하지만,
          Sequence를 사용하면 처리 중인 청크만 메모리에 유지

 

스트림 처리 방식

- 데이터를 청크 단위로 처리하므로 메모리 사용량이 청크 크기에 비례

- 예: 1GB 데이터를 10MB 청크로 나누어 처리하면 한 번에 10MB만 메모리에 로드

 

중간 컬렉션 생성 방지

- filter, map 등의 연산이 즉시 실행되지 않고 최종 연산(forEach, toList 등)이 호출될 때까지 지연

- 이로 인해 중간 결과를 저장하는 임시 컬렉션이 생성되지 않음

 

단일 패스 처리

- 시퀀스는 모든 연산을 요소별로 한 번에 처리

- List를 사용: list.filter {...}.map {...} 는 두 개의 별도 리스트를 생성

- Sequence 사용: sequence.filter {...}.map {...} 는 각 요소에 대해 filter와 map을 순차적으로 적용하여 추가 컬렉션을 만들지 않음