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

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

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

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

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


상황

  • 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개의 댓글