(2)

[Kotlin] Collections: 시퀀스(Sequences)는 무엇이고 언제 사용해야할까

안녕하세요. 저는 2년차 백엔드 개발자입니다.현재 JS(TS)로 개발하고 있고, Java는 코테용 언어로 사용하고 있습니다.코틀린을 사용하기 위해 현재 제가 알고 있는 지식 대비 필요 부분들만 학습하여 정리하는 Kotlin 시리즈입니다.피드백은 언제나 감사합니다!!     코틀린 기본 문법을 공부하고 있는 와중에, 컬렉션의 Lazy Evaluation을 지원하는 Sequence라는 것이 있다는 것을 알게 되었다. 기본 문법 포스팅에 내용을 남겼다가, 분리해야할 것 같아서 포스팅을 따로 작성했다.  SequenceKotlin 1.0에 추가된 Sequence 인터페이스는 Kotlin의 의 내장 라이브러리로, Collections의 다른 유형을 제공하기 위해 등장했다. 기본적으로 Sequence는 Iterat..

[Kotlin] 기본 문법 - Basic types, Collections, 조건문, 반복문

안녕하세요. 저는 2년차 백엔드 개발자입니다.현재 JS(TS)로 개발하고 있고, Java는 코테용 언어로 사용하고 있습니다.코틀린을 사용하기 위해 현재 제가 알고 있는 지식 대비 필요 부분들만 학습하여 정리하는 Kotlin 시리즈입니다.피드백은 언제나 감사합니다!! Kotlin 등장 배경내가 사용하고 있는 Nest는 nodejs의 구조적인 한계를 해결하기 위해 등장했으며 모듈 기반 아키텍처와 의존성 주입과 같은 개념을 적극 도입함으로써 nodejs의 자유도가 높은 구조를 개선하고 엔터프라이즈급 애플리케이션에서도 일관된 개발 경험을 제공하기 위해 만들어진 프레임워크이다.   코틀린의 공식 문서에는, 간결하고 안전하며 Java와 100% 호환이 되고 (JVM 위에서 동작)  NullSafety하다는 특징을 ..

[Kotlin] Collections: 시퀀스(Sequences)는 무엇이고 언제 사용해야할까

Tech/Kotlin 2025. 2. 13. 13:40
728x90
728x90

 

안녕하세요. 저는 2년차 백엔드 개발자입니다.

현재 JS(TS)로 개발하고 있고, Java는 코테용 언어로 사용하고 있습니다.

코틀린을 사용하기 위해 현재 제가 알고 있는 지식 대비 필요 부분들만 학습하여 정리하는 Kotlin 시리즈입니다.

피드백은 언제나 감사합니다!!

 


 

 

 

 

코틀린 기본 문법을 공부하고 있는 와중에, 컬렉션의 Lazy Evaluation을 지원하는 Sequence라는 것이 있다는 것을 알게 되었다. 기본 문법 포스팅에 내용을 남겼다가, 분리해야할 것 같아서 포스팅을 따로 작성했다.

 

 

Sequence

Kotlin 1.0에 추가된 Sequence 인터페이스는 Kotlin의 의 내장 라이브러리로, Collections의 다른 유형을 제공하기 위해 등장했다. 기본적으로 Sequence는 Iterator을 통해 값을 반환하는 기능을 수행한다. 쉽게 말해 Collections의 처리 기능 중 하나라고 생각하면 되겠다.

(kotlin-stdlib/kotlin.sequences/Sequence)

 

package kotlin.sequences

/**
 * A sequence that returns values through its iterator. The values are evaluated lazily, and the sequence
 * is potentially infinite.
 *
 * Sequences can be iterated multiple times, however some sequence implementations might constrain themselves
 * to be iterated only once. That is mentioned specifically in their documentation (e.g. [generateSequence] overload).
 * The latter sequences throw an exception on an attempt to iterate them the second time.
 *
 * Sequence operations, like [Sequence.map], [Sequence.filter] etc, generally preserve that property of a sequence, and
 * again it's documented for an operation if it doesn't.
 *
 * @param T the type of elements in the sequence.
 */
public interface Sequence<out T> {
    /**
     * Returns an [Iterator] that returns the values from the sequence.
     *
     * Throws an exception if the sequence is constrained to be iterated once and `iterator` is invoked the second time.
     */
    public operator fun iterator(): Iterator<T>
}

 

 

Sequence는 컬렉션과 달리 요소들을 포함하지 않고 반복하면서 생성한다. 순회하며 일련의 연산을 하는 것이 Iterable과 동일한 기능을 제공하지만 여러 연산에서 컬렉션 처리에 대해 다른 방식을 사용한다.

 

Collections의 인터페이스인 Iterable은 여러 연산을 처리할 때, 각 처리 단계를 순차적으로 완료하고, 하나의 연산이 종료될 때 마다 중간 결과를 임시 컬렉션에 저장한다. 다음 단계는, 초기에 선언했던 컬렉션이 아닌 생성한 임시 컬렉션을 통해 진행된다.

val numbers = listOf(1, 2, 3, 4, 5)

val result = numbers.map { 
    println("Mapping $it")
    it * 2
}.filter { 
    println("Filtering $it")
    it > 5
}.find { true }
print("Result $result")

 

 

위 연산에서 가장 처음 생성됐던 numbers 컬렉션에서, map을 수행하면 map의 결과 컬렉션이 생성되고, filter또한 마찬가지이다.

 

반대로 Sequence는  함수를 수행할 때 지연 실행을 지원하는데 공식문서에서는 이를 evaluated lazily라고 표현하고 있다. 즉 Collections을 통한 다양한 연산을 수행할 때 Lazy Evaluation이 가능하게 해주는 인터페이스가 Sequence이다.

 

 

Sequence 생성하기

Sequence는컬렉션과 동일하게 생성하거나, 컬렉션에서 변환할 수 있다.

val sequenceExam = sequenceOf('1', '2', '3')

val numbers = listOf(1, 2, 3, 4, 5)
val numbersSequence = numbers.asSequence()

 

 

컬렉션이나 sequenceOf 외에도 함수를 사용하여 동적으로 생성할 수도 있다. 이 때 기본적으로 무한대의 시퀀스를 생성하기 때문에 반드시 특정 종료 조건을 명시해야한다.

val oddNumbers = generateSequence(1) { it + 2 } // 1, 3, 5, ........
println(oddNumbers.take(5).toList()) //take(5)를 통해 5개만 가져오게끔 설정
// [1, 3, 5, 7, 9]



// ⚠️ 무한 루프
val oddNumbers = generateSequence(1) { it + 2 } // 1, 3, 5, ........
println(oddNumbers.count())



// 종료 조건을 명시
val oddNumbersLessThan10 = generateSequence(1) { if (it < 8) it + 2 else null }

 

 

마지막으로, sequence 블록({ })을 활용하여 시퀀스를 하나 또는 여러개를 동시에 생성할 수도 있다.

val oddNumbers = sequence {
    yield(1) // 개별 요소 반환
    yieldAll(listOf(3, 5)) // 리스트의 모든 요소 반환
    yieldAll(generateSequence(7) { it + 2 }) // 무한 시퀀스 반환
}
println(oddNumbers.take(5).toList()) 
// 출력: [1, 3, 5, 7, 9]

 

위 코드에서 yield(1)은 단일 값을, yieldAll(listOf(3, 5))는 리스트의 모든 값을 반한다. 또한 yieldAll(generateSequence(7) { it + 2 })를 통해 무한 시퀀스를 마지막에 추가할 수도 있다.

 

 

 

Iterable vs Sequence

위에서, 기본 컬렉션의 연산 처리 방식은, 하나의 함수가 끝나면 임시 컬렉션을 생성한다고 했다. 반대로 Sequence의 경우 지연 연산을 통해 데이터를 임시 컬렉션에 저장하지 않고 처리하는 방식이라고 설명했다.

 

말로 와닿지 않아서, 직접 코드로 작성해보았다. 아래는 간단한 Iterable VS Sequence 예제이다.

val numbers = listOf(1, 2, 3, 4, 5)

// 즉시 실행
val result = numbers.map { 
    println("Mapping $it")
    it * 2
}.filter { 
    println("Filtering $it")
    it > 5
}.find { true }
print("Result $result")


// 지연 실행
val result = numbers.asSequence().map { 
    println("Mapping $it")
    it * 2
}.filter { 
    println("Filtering $it")
    it > 5
}.find { true }
print("Result $result")
// 즉시 실행
Mapping 1
Mapping 2
Mapping 3
Mapping 4
Mapping 5
Filtering 2
Filtering 4
Filtering 6
Filtering 8
Filtering 10
Result 6


// 지연 실행
Mapping 1
Filtering 2
Mapping 2
Filtering 4
Mapping 3
Filtering 6
6

 

 

즉시 실행 방식은 numbers 컬렉션에 대해 하나의 메서드가 끝난 후 다른 메서드를 실행하는 방식이다. 위 예제에서는 map의 연산이 끝난 후 filter을, filter 연산이 끝난 후 find가 수행된다. 이 과정에서 numbers 배열에 연산의 결과가 덮어씌워지는 것이 아니라, 임시 컬렉션이 생성되면서 연산 결과를 가지고있게 된다.

 

 

 

하지만 sequence를 사용하면 임시 컬렉션이 생성되지 않고, 최종 연산이 호출되어 실행될 때 까지 연기된다.

(Java의 Stream 객체의 동작과 같다고 하는데 이 부분은 잘 모르겠다.)

 

중간 결과를 따로 저장할 임시 컬렉션이 생성되지 않고, 원소에 연산을 차례대로 적용하다가, 결과가 얻어진 후의 연산은 수행하지 않는다. 그래서 위 결과에서 filtering 6 이후의 연산은 수행되지 않는 것이다.

 

 

 

 

그렇다면 언제 사용해야할까? 사실 잘 와닿지가 않아서 극단적인 상황을 가정해봤다.

class IteratorVsSequenceTest {

    @Test
    fun `즉시 실행과 지연 실행의 성능 차이`() {
        try {
            val numbers = (1..10_000_000).toList()

            val iteratorMemory = measureMemoryUsage {
                val iteratorTime = measureTimeMillis { iteratorFunction(numbers) }
                println("Iterator 실행 시간: ${iteratorTime}ms")
            }

            val sequenceMemory = measureMemoryUsage {
                val sequenceTime = measureTimeMillis { sequenceFunction(numbers) }
                println("Sequence 실행 시간: ${sequenceTime}ms")
            }

            println("Iterator 메모리 사용량: ${iteratorMemory} KB")
            println("Sequence 메모리 사용량: ${sequenceMemory} KB")

        } catch (e: Exception) {
            println("테스트 실패: ${e.message}")
        }
    }

    private fun iteratorFunction(lists: List<Int>): Int? {
        return lists
            .filter { it % 2 == 0 }
            .filter { it % 3 == 0 }
            .map { it * it }
            .map { it / 2 }
            .filter { it > 1_000_000 }
            .firstOrNull { it % 123456 == 0 }
    }

    private fun sequenceFunction(lists: List<Int>): Int? {
        return lists.asSequence()
            .filter { it % 2 == 0 }
            .filter { it % 3 == 0 }
            .map { it * it }
            .map { it / 2 }
            .filter { it > 1_000_000 }
            .firstOrNull { it % 123456 == 0 }
    }

    private fun measureMemoryUsage(block: () -> Unit): Long {
        val runtime = Runtime.getRuntime()
        System.gc()
        val before = runtime.totalMemory() - runtime.freeMemory()
        block()
        val after = runtime.totalMemory() - runtime.freeMemory()
        return (after - before) / 1024
    }
}

 

천 만개의 numbers List를 사용해서, 시퀀스함수에서는 최대한 빨리 조건을 만족해서 빠져나올 수 있도록 구성해봤다. 레퍼런스들을 찾아보고 메모리 사용량을 볼 수 있는 코드도 추가해보았다. 과연 결과는 어떻게 나왔을까?

 

Iterator 실행 시간: 249ms
Sequence 실행 시간: 6ms
Iterator 메모리 사용량: 184529 KB
Sequence 메모리 사용량: 512 KB

 

 

실행 시간 측면에서는 다소 차이가 있었지만 메모리 사용량에서 차이가 엄청나게 많았다. 위에서 설명했듯이 함수 하나하나에 임시 컬렉션을 사용하기 때문에 메모리 사용량에서 엄청난 차이가 있다. 위처럼 극단적인 상황은 없겠지만, 유추해볼 때 파일을 포함한 대용량 데이터를 핸들링할 때 사용해야할 것으로 보인다.

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

[Kotlin] 기본 문법 - Basic types, Collections, 조건문, 반복문

Tech/Kotlin 2025. 2. 12. 17:29
728x90
728x90

 

안녕하세요. 저는 2년차 백엔드 개발자입니다.

현재 JS(TS)로 개발하고 있고, Java는 코테용 언어로 사용하고 있습니다.

코틀린을 사용하기 위해 현재 제가 알고 있는 지식 대비 필요 부분들만 학습하여 정리하는 Kotlin 시리즈입니다.

피드백은 언제나 감사합니다!!


 

Kotlin 등장 배경

내가 사용하고 있는 Nest는 nodejs의 구조적인 한계를 해결하기 위해 등장했으며 모듈 기반 아키텍처와 의존성 주입과 같은 개념을 적극 도입함으로써 nodejs의 자유도가 높은 구조를 개선하고 엔터프라이즈급 애플리케이션에서도 일관된 개발 경험을 제공하기 위해 만들어진 프레임워크이다.

 

 

 

코틀린의 공식 문서에는, 간결하고 안전하며 Java와 100% 호환이 되고 (JVM 위에서 동작)  NullSafety하다는 특징을 Introduce에 서술해두었다. 이 밖에도 코틀린이 등장한 배경이라던가, 해결하고자 하는 문제가 무엇이었는지 찾아보고 싶어 찾아봤고, 기존의 자바에서 어떤 문제가 있었는지는 직접 자바로 서버 구축을 하고 유지 보수를 해보지 않아서, 정리된 글들을 가져왔다.

 

출처: f-lab

 

 

 

나는 이 수많은 이유들 중에 안드로이드 앱을 여러 개 출시할 계획으로 코틀린 공부를 시작한다.

현재 사용중인 RN / Node(Nest)를 통해 개발을 하면 훨씬 생산성이 향상되겠지만, 익숙함을 잠시 내려놓고 개인적인 개발을 할 때는 새로운 환경에서 다시 시작해보려한다.

 


Take Kotlin Tour

공식 문서의 기본 가이드에 간단히 나와있는 언어의 핵심 개념 페이지들을 하나씩 읽어보며 공부한 내용들을 정리해보았다. 내용이 너무 길어져서, Functions (+ Lamda), Classes, Null Safety는 다음 포스팅에 정리해서 총 2회분의 포스팅을 통해 문법을 정리하려고 한다.

 

 

 

 

 

 

 

Variable

JS의 let, const처럼 mutable하고 immutable한 키워드인 var, val을 통해 변수 선언을 할 수 있다.

// Immutable variable ( == const )
val test1 = 10
test1 = 5 // ❌ Val cannot be reassigned


// Mutable variable ( == let )
var test2 = "test" // 타입 추론 (test2: String)
test2  = 1         // ❌ Type Error (정수 리터럴이 String과 호환되지 않음)
tet2 = "TEST2"     // ✅


// 객체의 멤버 변수에서 `val`은 readonly처럼 동작
data class User(var name: String, val age: Int)
val user = User("mag1c", 30)
user.name = "mag2c" // ✅ 변경 가능
user.age = 20      // ❌ 변경 불가능


val list = mutableListOf(1, 2, 3)
list.add(4)                    // ✅ 가능 (리스트 내부 값 변경 가능)
list = mutableListOf(5, 6, 7)  // ❌ 오류 (val 자체는 변경 불가능)

 


Basic Types

TS처럼 자료형을 콜론(:)을 이용해 명시할 수도, 생략할 수도 있다. 마찬가지로 TS처럼 타입 추론을 자동으로 할 수 있기 때문에 굳이 명시하지 않아도 된다. 신기한 부분은, Unsigned Integers를 지원한다는 점이었다.

  Basic Types 예시
Integers Byte, Short, Int, Long val year: Int = 2020
Unsigned integers UByte, UShort, UInt, ULong val score: UInt = 100u
Floating-point numbers Float, Double val currentTemp: Float = 24.5f, val price: Double = 19.99
Booleans Boolean val isEnabled: Boolean = true
Characters Char val separator: Char = ','
Strings String val message: String = "Hello, world!"
Any Any val anything: Any = "HELLO"

 

Any 타입은 모든 타입의 부모 타입(super type)이다. 기본적으로 모든 값이 될 수 있다. TS의 any와 유사하지만 코틀린에서의 Any 타입은 nullable하지 않다. 옵셔널 연산자인 ?을 통해 nullable을 명시할 수 있다.

//TS
let value: any;  // undefined
value = 42;      // ✅
value = null;    // ✅
//Kotlin
var value: Any = "HELLO"
value = 42      // ✅
value = null    // ❌

var value: Any? = "HELLO"
value = 42      // ✅
value = null    // ✅

 

 

Type Check

is를 통한 타입 체크도 가능한데, TS의 typeof와 유사하게 동작한다. 다만 TS의 typeof는 object 타입이 어떤 객체인지까지는 명시해주지 않지만, is를 사용하게되면 instanceof 처럼 동작한다는 점이다.

// is를 통한 타입 체크가 가능
val string1 = "string1"
val isString = if (string1 is String) true else false
print(isString)

 

또한 TS에 있는 as를 통한 명시적 형변환도 코틀린에서 가능하다.

// casting
val obj: Any = "Hello"
val str: String = obj as String  // ✅ 명시적 형 변환
println(str.length)  // 5

// null-safe한 casting
val obj: Any? = null
val str: String? = obj as? String  // ✅ 안전한 형 변환 (null 반환 가능)

String Templates

JS의 Template literals과 유사하지만, 코틀린의 문법이 조금 더 편리하다고 느꼈다. JS에서는 항상 중괄호로 감싸주어야했지만, 코틀린에서는 달러 기호($) 하나면 끝이다.

// JS
let name = "mag1c"
let message = "HELLO"

function sendMessage(name, message) {
	console.log(`[${name}] ${message}`);
}
val name = "mag1c"
val message = "HELLO"

fun sendMessage(name: String, message: String) {
	println("[$name] $message")
}

 

Collections

기본적으로 List, Set, Map을 지원한다. 아래는 코틀린의 기본적인 Collections 구조이다.

 

 

 

기본적으로 JS의 Iterable과 같은 개념으로 보인다. forEach, iterator, hasNext등을 사용할 수 있고, 이런 내장 메서드를 통해 요소를 순회할 수 있다. 반대로 MutableIterable은 Iterable을 상속하지만, mutable한 요소를 구현하여 쓰기 작업을 가능하게 한다.

// Iterable<T>
val list: Iterable<Int> = listOf(1, 2, 3)
for (item in list) {
    println(item)
}



// MutableIterable<T>
val list = mutableListOf(1, 2, 3)
val iterator = list.iterator()
while (iterator.hasNext()) {
    if (iterator.next() == 2) {
        iterator.remove() // ✅ 요소 제거 가능
    }
}
println(list) // [1, 3]



// Collection<T>
val collection: Collection<Int> = listOf(1, 2, 3)
println(collection.size)        // ✅ 크기 확인 가능
println(collection.contains(2)) // ✅ 특정 요소 포함 여부 확인 가능
collection.add(4)               // ❌ (Collection<T>에는 add() 없음)



// MutableCollection<T>
val mutableCollection: MutableCollection<Int> = mutableListOf(1, 2, 3)
mutableCollection.add(4)    // ✅ 가능
mutableCollection.remove(1) // ✅ 가능

 

 

Map은 Iterator의 특성인 순회를 전제로 하지 않기 떄문에 독립적인 Collections을 구현한 것으로 보인다. 여튼 콜렉션 구현을 위한 인터페이스는 동일하다.

// List
val fruits = listOf("apple", "banana", "orange") // immutable
val myDoggies = mutableListOf("guzzi", "coco")   // mutable

// Set
val fruits = setOf("apple", "banana", "orange")  // immutable
val myDoggies = mutableSetOf("guzzi", "coco")    // mutable

// Map
val fruits = mapOf("apple" to 100, "banana" to 200, "orange" to 300)  // immutable
val myDoggies = mutableMapOf("guzzi" to 1, "coco" to 2)               // mutable

 

기본적으로 in 연산자도 지원하는 것으로 보인다.

println("apple" in fruits) //true

 

 

 

고차 함수(?)

JS의 map(), filter() 등의 Array.prototype에 있는 고차 함수들을 List, Set에서도 사용이 가능했다.

// map
val numbers = listOf(1, 2, 3)
val squared = numbers.map { it * it }
println(squared) // [1, 4, 9]



// filter
val numbers = listOf(1, 2, 3, 4, 5)
val evens = numbers.filter { it % 2 == 0 }
println(evens) // [2, 4]


// forEach
val numbers = listOf(1, 2, 3)
numbers.forEach { println(it) } // 1 2 3



// reduce
val numbers = listOf(1, 2, 3, 4)
val sum = numbers.reduce { acc, num -> acc + num }
println(sum) // 10



// flatMap
val arr = listOf(listOf(1, 2), listOf(3, 4))
val flattened = arr.flatMap { it }
println(flattened) // [1, 2, 3, 4]

 


Control Flow

조건문

IF문이나 WHEN문을 통해 조건문을 작성할 수 있으며, 공식 문서에 따르면 둘 중 하나를 선택하여 사용해야한다면 WHEN이 가독성, 유지보수, 확장성 측면에서 낫다고 설명하고 있다.

 

 

val trafficLightState = "Red"
var action: String

// if
if (trafficLightState === "Green") {
    action = "GO"
}
if (trafficLightState === "Yellow") {
    action = "SLOW DOWN"
}
if (trafficLightState === "Red") {
    action = "STOP"
}

// when
val action2 = when (trafficLightState){
    "Green" -> "GO"
    "Yellow" -> "SLOW DOWN"
    "Red" -> "STOP"
    else -> "?"
}

 

 

 

 

반복문

// Range 표현
1..4            // 1, 2, 3, 4
1..<4           // 1, 2, 3
4 downTo 1      // 4, 3, 2, 1
4..1            // ❌
1..5 step 2     // 1, 3, 5
'a'..'d'        // a, b, c, d
'z' downTo 's'  // z, y, x, ..., s
// for
for (number in 1..5) {}

val array = IntArray(10) { 0 }
for (elem in array) {}  // array 내부 요소를 하나씩 선택해서 순회

// while
while(true) { return }

// do while
var num = 0
do {
    println("TEST")
    num++
} while (num < 3)

 

 

 

 

 

 

 

 

References

https://kotlinlang.org/docs/kotlin-tour-welcome.html

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

방명록