Flutter에서 async, await 비동기 함수 Kotlin 에선 어떻게?

💜Dabo (개발자 다보)·2023년 4월 20일

결론 부터 얘기하자면
해당 글에서 얻을 내용이 많지 않고, 심지어 해결을 하지 못했습니다

상황을 해결해나가는 과정을 적고자 합니다.

해결이 되지 않은 내용은
다음과 같이 상단에 해결 못했다고 안내글을 남기도록 하겠습니다.


상황

  • TextFieldButton 그리고 결과내용이 보일 Text가 있는 화면이 있다

  • 여기서 Button을 누르면 remote API를 호출하고 검색 결과를 가져오고 화면에 출력하고 있는 list에 addAll을 한다

여기서 remote API 호출을 할 때 retrofit2을 사용했고
내부 call.enqueue{}로 수행하는데 내부적으로 비동기 처리를 하고있다.


kotlin code

val list = mutableListOf<Document>()

...

// button (onclick)
onSearch = {
    val results = searchImage(query = query)
    list.addAll(results)
    Log.d("TAG", "onCreate: list $list")
}

그러면 기대하는 상황

D/REMOTE DATA SOURCE: API 응답 성공
D/REMOTE DATA SOURCE: doc: Document(collection=cafe, datetime=2016-03-13T20:12:36.000+09:00, displaySitename=Daum카페, docURL=https://cafe.daum.net/yangdreamcommunity/BmPG/289, height=200.0, imageURL=https://t2.search.daumcdn.net/argon/0x200_85_hr/7cTaHuAj0uD, thumbnailURL=null, width=265.0)
D/REMOTE DATA SOURCE: doc: Document(collection=cafe, datetime=2004-04-02T22:45:16.000+09:00, displaySitename=Daum카페, docURL=http://cafe.daum.net/luxury206/PsUB/30, height=350.0, imageURL=http://cafe159.daum.net/_c21_/pds_down_hdn?grpid=nnXB&fldid=Pl8Q&dataid=21&grpcode=luxury206&realfile=yk37.jpg, thumbnailURL=null, width=467.0)
D/REMOTE DATA SOURCE: doc: Document(collection=cafe, datetime=2009-04-10T17:27:01.000+09:00, displaySitename=Daum카페, docURL=https://cafe.daum.net/wolnam16/VFO/337, height=285.0, imageURL=http://cfile293.uf.daum.net/image/1175340D49DF05F3D6617D, thumbnailURL=null, width=401.0)
D/REMOTE DATA SOURCE: doc: Document(collection=cafe, datetime=2023-04-10T18:59:17.000+09:00, displaySitename=Daum카페

...

D/TAG: onCreate: list [Document(collection=cafe, datetime=2016-03-13T20:12:36.000+09:00, displaySitename=Daum카페, docURL=https://cafe.daum.net/yangdreamcommunity/BmPG/289, height=200.0, imageURL=https://t2.search.daumcdn.net/argon/0x200_85_hr/7cTaHuAj0uD, thumbnailURL=null, width=265.0), Document(collection=cafe, datetime=2004-04-02T22:45:16.000+09:00, displaySitename=Daum카페, docURL=http://cafe.daum.net/luxury206/PsUB/30, height=350.0, imageURL=http://cafe159.daum.net/_c21_/pds_down_hdn?grpid=nnXB&fldid=Pl8Q&dataid=21&grpcode=luxury206&realfile=yk37.jpg, thumbnailURL=null, width=467.0), Document(collection=cafe, datetime=2009-04-10T17:27:01.000+09:00, displaySitename=Daum카페, docURL=https://cafe.daum.net/wolnam16/VFO/337, height=285.0, imageURL=http://cfile293.uf.daum.net/image/1175340D49DF05F3D6617D, thumbnailURL=null, width=401.0), Document(collection=cafe, datetime=2023-04-10T18:59:17.000+09:00, displaySitename=Daum카페, docURL=https://cafe.daum.net/woori032/LzMX/9238, height=316.0, imageURL=https://t1.da ...

하지만 나온 결과

D/TAG: onCreate: list []
D/REMOTE DATA SOURCE: API 응답 성공
D/REMOTE DATA SOURCE: doc: Document(collection=cafe, datetime=2016-03-13T20:12:36.000+09:00, displaySitename=Daum카페, docURL=https://cafe.daum.net/yangdreamcommunity/BmPG/289, height=200.0, imageURL=https://t2.search.daumcdn.net/argon/0x200_85_hr/7cTaHuAj0uD, thumbnailURL=null, width=265.0)
D/REMOTE DATA SOURCE: doc: Document(collection=cafe, datetime=2004-04-02T22:45:16.000+09:00, displaySitename=Daum카페, docURL=http://cafe.daum.net/luxury206/PsUB/30, height=350.0, imageURL=http://cafe159.daum.net/_c21_/pds_down_hdn?grpid=nnXB&fldid=Pl8Q&dataid=21&grpcode=luxury206&realfile=yk37.jpg, thumbnailURL=null, width=467.0)
D/REMOTE DATA SOURCE: doc: Document(collection=cafe, datetime=2009-04-10T17:27:01.000+09:00, displaySitename=Daum카페, docURL=https://cafe.daum.net/wolnam16/VFO/337, height=285.0, imageURL=http://cfile293.uf.daum.net/image/1175340D49DF05F3D6617D, thumbnailURL=null, width=401.0)
D/REMOTE DATA SOURCE: doc: Document(collection=cafe, datetime=2023-04-10T18:59:17.000+09:00, displaySitename=Daum카페

...

Flutter에서 Dart로 했다면..

searchImage()가 자체 비동기 처리를 하고 있을 때

final list = <Document>[];

...

// button (onclick)
onSearch () async {
    val results = await searchImage(query)
    list.addAll(results)
    log("TAG", "onCreate: list $list")
}

이렇게 위처럼 await 키워드 추가하면 끝인데..
searchImage 액션을 기다리고 list.add 하면 list에 결과가 잘 담길텐데..


kotlin에서는 그러면 어떻게 할까?


Kotlin 비동기 프로그래밍 공부

[kotlin 공식문서] 비동기 프로그래밍
https://kotlinlang.org/docs/async-programming.html

Threading

오래걸리는 함수로 인해 UI가 block된다고 가정

fun postItem(item: Item) {
    val token = 오래걸리는첫번째함수()
    val post = 두번째함수(token, item)
    세번째함수(post)
}

fun 오래걸리는 함수(): Token {
	...
    return token
}
  • 그러면 일반적으로 스레딩 작업으로 해결할 수 있음

단점은?

  • 스레드는 저렴하지 않음. 스레드에는 비용이 많이 드는 컨텍스트 스위치가 필요함
  • 스레드는 무한하지 않음. 시작할 수 있는 스레드 수는 기본 운영 체제에 의해 제한되고, 서버 측 애플리케이션에서 이로 인해 주요 병목 현상이 발생할 수 있음.
  • 스레드를 항상 사용할 수 있는 것은 아님. JavaScript와 같은 일부 플랫폼은 스레드를 지원 안함
  • 스레드는 easy하지 않음. 스레드 디버깅, race conditions 방지는 멀티 스레드 프로그래밍에서 겪는 일반적인 문제임

공식문서에는 스레드 작성법이 따로 안 나와있지만Thread 작성법이 궁금하시다면 이 블로그를 참고

CallBack

하나의 함수를 매개변수로 전달하고프로세스가 완료되면 함수가 호출되도록 하는 것

fun postItem(item: Item) {
    오래걸리는첫번째함수 { token ->
        두번째함수(token, item) { post ->
            세번째함수(post)
        }
    }
}

fun 오래걸리는첫번째함수(callback: (Token) -> Unit) {
	...
}
  • 중첩된 콜백의 어려움, 한번에 이해할 수 없는 코드로 이어지는 일련의 중첩된 콜백이 발생
  • 오류처리가 복잡시러움

javascript와 같은 이벤트 루프 아키텍처에서 매우 일반적이지만Promise 또는 Reactive Extensions와 같은 다른 접근 방식을 사용합니다

어.. 그래서! 결론이 뭐야?

Threading, CallBack, Future, promises, .. 등 비동기 작업에 다양한 접근 방식이 있지만 Kotlin에선 코루틴을 사용한다

코루틴?

이제 그러면 코틀린에서 사용하는 비동기 작업을 보자!

Coroutines 코루틴

[kotlin 공식문서] Coroutines

  • official Library
    : kotlinx.coroutinesJetBrains에서 개발한 풍부한 코루틴용 라이브러리입니다

  • 나머지 코드와 동시에 작동하는 코드 블록을 실행해야 한다는 점에서
    개념적으로 스레드와 유사합니다
    : 코루틴은 가벼운 스레드로 생각할 수 있지만 스레드와 다른 중요한 차이점이 많음
    : 코루틴은 특정 스레드에 바인딩 되지 않음, 한 스레드에서 실행을 일시중단하고 다른 스레드에서 다시 시작할 수 있음

  • 구조화된 동시성(Structured concurrency)


code:

fun main() = runBlocking { 
    launch { // new coroutine
        delay(timeMillis = 3000L) // non-blocking delay for 3 second (default time unit is ms)
        println("World!") // print after delay
    }
    println("Hello")
}

result:

Hello
(3초 뒤)
World!

코드 분석:

  • runBlocking{} 일반 코드와 코투틴 코드를 연결해주는 coroutine builder
    : 내부의 모든 코루틴이 실행을 완료할 때 까지 호출 기간 동안 이를 실행하고 있는 main thread가 blocked(차단됨)을 의미
    : 스레드는 값 비싼 리소스이고 스레드를 차단하는 것은 비효율적이며 바람직하지 않아서 실제 코드 내에선 거의 사용되지 않고, runBlocking은 자주 보임

  • launch{} coroutine builder
    : 새로운 코루틴을 시작
    : 단독으로 못씀
    (꼭 연결해주는 runBlocking{}과 같은 coroutine builder 안에 사용 해야 됨)

  • delay(timeMillis = 3000L) special suspending function
    : 특정시간 코루틴을 일시 중단


extract function refactoring

함수 추출(extract function) 리팩토링을 하면
suspending function이 생김

fun main() = runBlocking { 
    launch { doWorld() }
    println("Hello")
}

// suspending function 정지 함수
suspend fun doWorld() {
    delay(1000L)
    println("World!")
}
  • 정지함수는 일반 함수처럼 코루틴 내부에서 쓸 수 있고,
    delay와 같은 suspending 함수를 사용할 수 있음

scope builder

  • coroutineScope 빌더를 사용해, 범위를 선언
    : runBlocking과 마찬가지로 body와 모든 자식이 완료될 때 까지 기다림

runBlocking vs coroutineScope

  • runBlocking{} 대기를 위해 현재 스레드 차단
    : 일반 함수

  • coroutineScope{} 대기를 위해 일시 중단하고
    다른 용도를 위해 기본 스레드를 해제
    : 정지 함수


두개의 코루틴을 실행하면?

code:

fun main() {
    runBlocking {
        doWorld()
        println("Done")
    }
}

suspend fun doWorld() {
    coroutineScope { 
        launch {
            delay(2000L)
            println("World 2")
        }
        launch {
            delay(1000L)
            println("World 1")
        }
        println("Hello")
    }
}

result:

Hello
World 1
World 2
Done

코드 분석:

  • coroutineScope{} 내 일반 코드(print)와 코루틴 코드(launch) 있음

  • launch{} 2개의 코드블럭과 println("Hello") 동시에 실행

  • 시작부터 1초 뒤 "World1" 프린트 되고, 시작부터 2초 뒤 "World2" 프린트

  • coroutineScope는 모든 함수 처리가 완료 된 후에 반환됨
    : 그래서 2초 뒤 "World2" 프린트 후 doWorld이 반환 후 "Done" 프린트


명시적 작업

명시적 작업..? 이라는 뜻이 맞는지 정확히 모르겠네요..ㅠ

아무튼 lanch{}가 Job 타입 인데
그 안에 job.join() 을 넣어 코루틴에게 기다리라고 명령 합니다.

code:

fun main() {
    runBlocking {
        println("---START---")
        val job = launch { // launch a new coroutine and keep a reference to its Job
            delay(1000L)
            println("World!")
        }
        println("Hello")

        job.join() // wait until child coroutine completes
        println("Done")
    }
    println("---END---")
}

result:

---START---
Hello
World!
Done
---END---
  • job.join()이 빠졌다면 "Hello" 출력 후 바로 "Done"이 출력 되었을 텐데
  • job.join()을 넣어 "World!" 출력을 기다리고 끝나고 나서야 "Done " 출력

Coroutines are light-weight

JVM 스레드보다 코루틴은 자원 집약도가 낮습니다.

5만개의 서로 다른 코루틴을 실행해 매우 적은 메모리를 사용하면서 "A" 출력

code:

fun main() {
    runBlocking {
        repeat(50000) { // 함수를 5만번 실행
            launch {
                print("A")
            }
        }
    }

result:

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA ...

그래서?

여기 위 까지 공식문서 내용인데 용어들이 낯설고 어렵다..
그래서 여태 위에 설명된 공식문서대로 내 문제를 대입 시키면?

할 수 있는게 없다


아까 봤던 코드를 다시보면

val list = mutableListOf<Document>()

...

// button (onclick)
onSearch = {
    val results = searchImage(query = query)
    list.addAll(results)
    Log.d("TAG", "onCreate: list $list")
}
  • searchImage() 함수의 반환타입을 비동기 함수로 선언할 수 있는 방법을 안 것도 아니고
  • 심지어 searchImage() 함수 내에서 call.enqueue{} 를 수행하는데 Asynchronously send 라서 코드 자체에 이해도가 부족한 내가 비동기 함수 선언하는 코드 몇자 배웠다고 할 수 있는게 아니였다.

현재 .. 매우 절망 상태..

결론적으론 다시.. 어떻게든
retrofit2(http 통신 라이브러리) + compose 조합으로
화면 변화를 일으킬 수 있는 코드를 작성해보려한다.

다음글에서 봬어요🙇🏻‍♀️

profile
𝙸 𝚊𝚖 𝚊 𝚌𝚞𝚛𝚒𝚘𝚞𝚜 𝚍𝚎𝚟𝚎𝚕𝚘𝚙𝚎𝚛 𝚠𝚑𝚘 𝚎𝚗𝚓𝚘𝚢𝚜 𝚍𝚎𝚏𝚒𝚗𝚒𝚗𝚐 𝚊 𝚙𝚛𝚘𝚋𝚕𝚎𝚖. 🇰🇷👩🏻‍💻

0개의 댓글