예제로 정리하는 코틀린의 코루틴(Kotlin Coroutine)

코루틴은 스레드와 기능적으로 같지만, 스레드에 비교하면 좀더 가볍고 유연하며 한단계 더 진화된 병렬 프로그래밍을 위한 기술입니다. 하나의 스레드 내에서 여러개의 코루틴이 실행되는 개념인데, 아래의 코드는 동일한 기능을 스레드와 코루틴으로 각각 구현한 코드의 예시입니다.

Thread(Runnable {
    for(i in 1..10) {
        Thread.sleep(1000L)
        print("I'm working in Thread.")
    }
}).start()

GlobalScope.launch() {
    repeat(10) {
        delay(1000L)
        print("I'm working in Coroutine.")
    }
}

아래는 코루틴에 대해서 초점을 맞춰서 가장 간단한 코루틴의 예제입니다.

print("Start Main Thread")

GlobalScope.launch {
    delay(3000)
    print("in Coroutine ...")
}

print("End Main Thread")
}

코루틴은 GlobalScope.launch로 정의되며 { .. } 으로 묶은 코드가 비동기적으로 실행됩니다. 실행 결과는 다음과 같습니다.

V: Start Main Thread
V: End Main Thread
V: in Coroutine ...

다음은 비동기적으로 실행된 코루틴이 완료되어 그 결과를 반환받는 예제입니다.

GlobalScope.launch {
    launch {
        print("At Here!")
    }

    val value: Int = async {
        var total = 0
        for (i in 1..10) total += i
        total
    }.await()

    print("$value")
}

결과는 다음과 같습니다.

V: At Here!
V: 55

다음 코드 역시 비동기적으로 실행된 코루틴의 완료를 기다리고 그 결과를 반환받아 출력하는 예제입니다.

GlobalScope.launch {
    val x = doSomething()
    print("done something, $x")
}

private suspend fun doSomething():Int {
    val value: Int = GlobalScope.async(Dispatchers.IO) {
        var total = 0
        for (i in 1..10) total += i
        print("do something in a suspend method: $total")
        total
    }.await()

    return value
}

비동기적으로 실행되는 코루틴을 별도의 함수로 분리했는데, 코루틴 내부에서 실행되는 함수는 suspend로 지정해야 합니다. 위의 코드의 결과는 다음과 같습니다.

V: do something in a suspend method: 55
V: done something, 55

이번에는 2개의 코루틴을 실행하고 이 2개의 결과를 받아 출력하는 예제입니다.

print("Start...")
GlobalScope.launch(Dispatchers.Main) {
    val job1 = async(Dispatchers.IO) {
        var total = 0
        for (i in 1..10) {
            total += i
            delay(100)
        }
        print("job1")
        total
    }

    val job2 = async(Dispatchers.Main) {
        var total = 0
        for (i in 1..10) {
            delay(100)
            total += i
        }
        print("job2")
        total
    }

    val result1 = job1.await()
    val result2 = job2.await()

    print("results are $result1 and $result2")
}
print("End.")

위의 코드에서 볼 수 있는 Dispatchers.Main, Dispatchers.IO는 각각 UI 변경 등을 처리하는 메인 스레드 그리고 입출력 연산을 처리하기에 적합한 IO 스레드를 의미하며, 코루틴들은 이처럼 지정된 스레드 내에서 실행됩니다. 위 코드의 결과는 다음과 같습니다.

V: Start...
V: End.
V: job1
V: job2
V: results are 55 and 55

다음은 코루틴이 완료를 기다리기 위한 await 호출을 사용하지 않는 또다른 방법입니다.

GlobalScope.launch(Dispatchers.IO) {
    val v = withContext(Dispatchers.Main) {
        var total = 0
        for (i in 1..10) {
            delay(100)
            total += i
        }

        total
    }

    print("result: $v")
    print("Do something in IO thread")
}

withContext를 써서 새로운 코루틴을 다른 스레드에서 동기적으로 실행하도록 하는 코드입니다. 결과는 다음과 같습니다.

V: result: 55
V: Do something in IO thread

launch는 Job 객체를 반환하는데, 이를 통해 다음 예제처럼 코루틴을 중간에 중단시킬 수 있습니다.

print("start..")

val job = GlobalScope.launch() {
    repeat(10) {
        delay(1000L)
        print("I'm working.")
    }
}

runBlocking {
    delay(3000L)
    job.cancel()
}

print("stop")

실행 결과는 다음과 같습니다.

V: start..
V: I'm working.
V: I'm working.
V: stop

이번에는 Job 객체를 통해 코루틴이 완전이 종료될때까지 기다리는 예제입니다.

print("start..")

val job = GlobalScope.launch() {
    repeat(10) {
        delay(1000L)
        print("I'm working.")
    }
}

runBlocking {
    job.join()
}

print("stop")

결과는 다음과 같습니다.

V: start..
V: I'm working.
V: I'm working.
V: I'm working.
V: I'm working.
V: I'm working.
V: I'm working.
V: I'm working.
V: I'm working.
V: I'm working.
V: I'm working.
V: stop

코루틴은 정해진 시간이 되면 코루틴의 완료되지 못할지라도 중지하게 할 수 있는데, 아래의 코드가 바로 그 예입니다.

print("start")

val job = GlobalScope.launch {
    withTimeout(4000L) {
        repeat(10) {
            delay(1000L)
            print("I'm working.")
        }
    }
}

print("end")

결과는 다음과 같습니다.

V: start
V: end
V: I'm working.
V: I'm working.
V: I'm working.

코루틴은 채널(Channel)이라는 개념을 통해 코루틴에서 생성한 데이터를 또 다른 코루틴에게 전달할 수 있습니다. 아래의 코드는 코루틴에서 1~5까지의 정수에 대한 제곱값을 생성하면 생성된 정수 4개를 또 다른 코루틴에서 받아 출력하는 예입니다.

runBlocking {
    print("start")

    val channel = Channel()

    launch {
        for (x in 1..5) {
            channel.send(x * x)
        }
    }

    repeat(5) {
        val v = channel.receive()
        print("$v")
    }

    print("end")
}

결과는 다음과 같습니다.

V: start
V: 1
V: 4
V: 9
V: 16
V: 25
V: end

데이터를 생성하는 쪽이나 받는 쪽에서는 얼마나 많은 데이터를 생성할지 또는 받을지를 예측할 수 없는 경우가 대부분입니다. 데이터를 생성하는 쪽에서 채널의 close 함수를 호출하면 받는쪽에서 더 이상 데이터가 없다는 것을 인지하게 되는데, 아래는 이에 대한 코드 예입니다.

runBlocking {
    print("start")

    val channel = Channel()

    launch {
        for(x in 1..5) channel.send(x*x)
        channel.close()
    }

    for(y in channel) print("$y")

    print("end")
}

결과는 다음과 같습니다.

V: start
V: 1
V: 4
V: 9
V: 16
V: 25
V: end

다음은 데이터를 생성하는 코루틴을 함수화하여 이 함수를 통해 생성된 데이터를 처리하는 예제입니다.

runBlocking {
    print("start")

    val squares = procedureSquares()
    squares.consumeEach {
        print("$it")
    }

    print("end")
}

private fun CoroutineScope.procedureSquares(): ReceiveChannel = produce {
    for(x in 1..5) send(x*x)
}

결과는 다음과 같습니다.

V: start
V: 1
V: 4
V: 9
V: 16
V: 25
V: end

다음은 데이터를 생성하는 코루틴을 파이프라인 형태로 묶어 처리하는 것으로, 첫번째 코루틴에서 생성한 값을 또 다른 코루틴에서 받아 처리하여 또 다른 코루틴으로 전달하는 예제입니다.

runBlocking {
    print("start")

    val numbers = productNumbers()
    val squares = squares(numbers)

    for(i in 1..5) print("${squares.receive()}")

    print("end")

    coroutineContext.cancelChildren()
}

private fun CoroutineScope.productNumbers() = produce {
    var x = 1
    while(true) {
        print("send ${x} on productNumbers")
        send(x++)
        delay(100)
    }
}

private fun CoroutineScope.squares(numbers:ReceiveChannel): ReceiveChannel = produce {
    for(x in numbers) {
        print("send ${x} on squares")
        send(x*x)
    }
}

결과는 다음과 같습니다.

V: start
V: send 1 on productNumbers
V: send 1 on squares
V: 1
V: send 2 on productNumbers
V: send 2 on squares
V: 4
V: send 3 on productNumbers
V: send 3 on squares
V: 9
V: send 4 on productNumbers
V: send 4 on squares
V: 16
V: send 5 on productNumbers
V: send 5 on squares
V: 25
V: end

데이터를 생성하는 코루틴은 1개지만, 이를 원활하게 처리하기 위해 여러개의 코루틴으로 생성된 데이터를 처리할 수 있습니다. 아래는 데이터를 생성하는 코루틴 1개와 생성된 데이터를 처리하는 5개의 코루틴에 대한 예제입니다.

runBlocking {
    val producer = productNumbers()
    repeat(5) {
        launchProcessor(it, producer)

    }

    delay(1000L)
    producer.cancel()
}

fun CoroutineScope.launchProcessor(id:Int, channel: ReceiveChannel) {
    launch {
        for(msg in channel) {
            print("Processor #$id received $msg")
        }
    }
}

private fun CoroutineScope.productNumbers() = produce {
    var x = 1
    while(true) {
        print("send ${x} on productNumbers")
        send(x++)
        delay(100)
    }
}

결과는 다음과 같습니다.

V: send 1 on productNumbers
V: Processor #0 received 1
V: send 2 on productNumbers
V: Processor #0 received 2
V: send 3 on productNumbers
V: Processor #1 received 3
V: send 4 on productNumbers
V: Processor #2 received 4
V: send 5 on productNumbers
V: Processor #3 received 5
V: send 6 on productNumbers
V: Processor #4 received 6
V: send 7 on productNumbers
V: Processor #0 received 7
V: send 8 on productNumbers
V: Processor #1 received 8
V: send 9 on productNumbers
V: Processor #2 received 9
V: send 10 on productNumbers
V: Processor #3 received 10

반대로 데이터를 생성하는 코루틴은 여러개이고 처리하는 코루틴은 1개인 경우도 있습니다. 아래는 데이터를 생성하는 코루틴 2개와 생성된 데이터를 처리하는 코루틴 1개에 대한 예제입니다.

runBlocking {
    val channel = Channel<String>()
    launch {
        sendString(channel, "foo", 200L)
    }

    launch {
        sendString(channel, "BAR", 500L)
    }

    repeat(6) {
        print("${channel.receive()}")
    }

    coroutineContext.cancelChildren()
}

private suspend fun sendString(channel: SendChannel<String>, s:String, time:Long) {
    while(true) {
        delay(time)
        channel.send(s)
    }
}

결과는 다음과 같습니다.

V: foo
V: foo
V: BAR
V: foo
V: foo
V: BAR

아래의 예제는 2개의 코루틴에서 하나의 데이터에 대해 어떤 처리를 해서 주고 받는 기능에 대한 코드입니다.

print("start")
data class Ball(var hits:Int)

suspend fun player(name:String, table: Channel) {
    for(ball in table) {
        ball.hits++
        print("$name $ball")
        delay(300)
        table.send(ball)
    }
}

runBlocking {
    var table = Channel<Ball>()

    launch {
        player("ping", table)
    }

    launch {
        player("pong", table)
    }

    table.send(Ball(0))
    delay(1000)
    coroutineContext.cancelChildren()
}
print("end")

결과는 다음과 같습니다.

V: start
V: ping Ball(hits=1)
V: pong Ball(hits=2)
V: ping Ball(hits=3)
V: pong Ball(hits=4)
V: end

ViewPropertyAnimator를 이용한 개별 뷰(View) 단위 애니메이션

안드로이드에서 하나의 뷰에 대한 특정 속성값을 애니메이션화하기 위한 API인 ViewPropertyAnimator에 대한 내용입니다. 먼저 아래와 같은 레이아웃에 애니메이션을 위한 버튼 뷰를 하나 배치합니다.

<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:scrollbars="none">

    <Button
        android:id="@+id/myView"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:layout_marginTop="8dp"
        android:layout_marginBottom="8dp"
        android:text="Hello" />

</ScrollView>

이제 버튼을 터치할때 애니매이션이 시작될 수 있도록 다음 코드를 추가합니다.

myView.setOnClickListener {
    myView
        .animate()
        .scaleX(5.0f)
        .scaleY(5.0f)
        .alpha(0.0f)
        .translationX(400.0f)
        .translationY(400.0f)
        .rotation(360.0f)
        .setDuration(1000)
        .withStartAction {
            Toast.makeText(this, "애니메이션 시작", Toast.LENGTH_SHORT).show()
        }
        .withEndAction {
            myView.scaleX = 1.0f;
            myView.scaleY = 1.0f;
            myView.alpha = 1.0f;
            myView.x = 0.0f;
            myView.y = 0.0f;
            myView.rotation = 0.0f;
            Toast.makeText(this, "완료", Toast.LENGTH_SHORT).show()
        }.start()
}

참고로 언어는 코들린입니다. 1초간 뷰의 크기를 5배로, 점점 투명하게, X와 Y의 위치를 각각 400과 400으로, z축으로 360회전 하도록 하는애니메이션입니다. 그 결과는 아래와 같습니다.