예제로 정리하는 코틀린의 코루틴(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<Int>()

    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<Int>()

    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<Int> = 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<Int> {
    var x = 1
    while(true) {
        print("send ${x} on productNumbers")
        send(x++)
        delay(100)
    }
}

private fun CoroutineScope.squares(numbers:ReceiveChannel<Int>): ReceiveChannel<Int> = 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<Int>) {
    launch {
        for(msg in channel) {
            print("Processor #$id received $msg")
        }
    }
}

private fun CoroutineScope.productNumbers() = produce<Int> {
    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

FingerEyes-Xr을 이용한 HeatMap 시각화

웹 GIS 엔진인 FingerEyes-Xr은 밀도도를 시각화하기 위한 방법 중의 하나인 HeatMap 기능을 제공합니다. 바로 Xr.layers.HeatMapLayer 클래스를 통해 수행이 가능합니다. HeatMap이 아닌 또 다른 밀도도에 대한 시각화 방법은 Xr.layers.GridLayer 클래스를 사용하는 것인데, Xr.layers.GridLayer는 셀 기반의 연산을 이용해 밀도도 분석을 수행합니다. 좀더 정밀한 밀도도 분석은 Xr.layers.GridLayer이 Xr.layers.HeatMapLayer보다 우수하지만 연산 시간은 Xr.layers.HeatMapLayer가 훨씬 빨라 실시간으로 밀도도를 시각화할 수 있다는 장점을 갖습니다.

아래의 영상은 FingerEyes-Xr의 Xr.layers.HeatMapLayer를 사용하여 생성된 밀도도입니다.

이에 대한 코드는 다음처럼 간단 명료합니다.

let heatMapLayer = map.layers('heatmap');
if (!heatMapLayer) {
    heatMapLayer = new Xr.layers.HeatMapLayer('heatmap');
    map.layers().add(heatMapLayer);
}
            
heatMapLayer.generateByLayer(lyr);

밀도도 분석을 위한 입력 데이터로 공간상에 분포된 포인트 좌표들이 필요하며, 이 포인트 좌표는 7번 코드의 lyr 변수명의 레이어를 통해 입력됩니다.

Xr.layers.HeatMapLayer는 밀도도 분석이 매우 빨라 실시간으로 밀도도를 생성할 수 있습니다. 아래의 코드는 입력 데이터가 변경되는 즉시 밀도도를 새롭게 생성해 표시합니다.

map.addEventListener(Xr.Events.LayerUpdateCompleted, function (e) {
    if (e.layerName === lyr.name()) {
        let heatMapLayer = map.layers('heatmap');
        if (heatMapLayer) {
            heatMapLayer.generateByLayer(lyr); 
        }
    }
});

끝으로 보다 정밀한 밀도도를 생성하기 위한 또다른 방식인 Xr.layers.GridLayer에 대한 실제 활용예는 아래의 글을 참고하기 바랍니다.

NexGen, 공간 데이터의 분포경향 분석을 위한 밀도맵 기능

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회전 하도록 하는애니메이션입니다. 그 결과는 아래와 같습니다.

안드로이드의 Shape 형태의 Drawable

직사각형(Rectangle) 형태의 Shape Drawable 정의는 다음과 같다.

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
       android:shape="rectangle">
    <stroke android:width="10dp"
            android:color="#ffff00" />
    <solid android:color="@android:color/transparent" />
    <corners android:radius="50dp" />
</shape>

위 형태에 대한 결과는 다음과 같다.

실제 뷰의 배경(Background)에 적용할 수 있는데, 그 예는 다음과 같다.

<LinearLayout android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:background="@drawable/rectangle_shape_drawable"
              android:orientation="vertical">

...

Javascript의 async, await 정리

이 글을 이해하기 위해서는 먼저 Promise API에 대해 알고 있어야 하며 아래의 글을 참고하시기 바랍니다.

Javascript의 Promise API 요약

async와 awit의 사용은 비동기 처리에 대한 혼란스러운 코드의 가독성을 향상 시켜줌으로써 코드의 유지보수 및 견고한 코드를 작성할 수 있습니다.

먼저 아래의 코드는 Promise API를 이용한 비동기처리입니다.

function getItem() {
    return new Promise((resolve, reject) => {
        setTimeout(function() {
            let result = { name: 'Dip2K', age: 44 };
            resolve(result);
        }, 2000);
    });
}

function callback(result) {
    console.log(result);
}

console.log('1');
getItem().then(callback);
console.log('2');

위의 코드를 async와 await를 이용해 동일하게 작동하도록 코드를 작성하면 다음과 같습니다.

function getItem() {
    return new Promise((resolve, reject) => {
        setTimeout(function() {
            let result = { name: 'Dip2K', age: 44 };
            resolve(result);
        }, 2000);
    });
}

async function get() {
    let result = await getItem();
    console.log(result);
}

console.log('1');
get();
console.log('2');

콜백함수 없이 비동기처리가 된 경우로, 순차적인 흐름의 코드로 작성되었습니다.

아래의 코드는 Promise의 예외의 처리를 async 및 await 구분에서 어떻게 처리 하는지를 보여주는 코드입니다.

function getItem() {
    return new Promise((resolve, reject) => {
        setTimeout(function() {
            let bOK = false;

            if(bOK) {
                let result = { name: 'Dip2K', age: 44 };
                resolve(result);
            } else {
                reject(null);
            }
        }, 2000);
    });
}

async function get() {
    try {
        let result = await getItem();
        console.log(result);
    } catch(e) {
        console.log(e);
    }
}

console.log('1');
get();
console.log('2');

await와 fetch를 함께 사용하는 코드 예시를 마지막으로 언급하고 마무리 합니다.

async function fetchData() {
  const response = await fetch("http:/...")
  const data = await response.json()
  cosnole.log(data)
}

fetchData()