해결된 질문
작성
·
200
1
fun main(): Unit = runBlocking {
val job1 = async { apiCall1() }
val job2 = async { apiCall2(job1.await()) }
printWithThread(job2.await())
}
suspend fun apiCall1(): Int {
delay(1_000L)
return 1
}
suspend fun apiCall2(num: Int): Int {
delay(1_000L)
return num + 2
}
위와 같은 코드에서, 코루틴을 사용해도 apiCall1()
의 반환값이 apiCall2()
의 인자로 사용되기 때문에, apiCall1()이 완료된 후에 apiCall2()가 실행되는 것은 이해했습니다. 그리고 코루틴을 사용할 때의 이점이 마치 한줄한줄 동기식 코드를 작성할 때처럼 비동기 코드를 사용할 수 있다. 로 이해했습니다.
그럼 위와 같은 상황에서는 굳이 코루틴을 사용하지 않고, 아래와 같이 동기적으로 코드를 작성해도 같은 것일까요? (두 apiCall1,2를 사용하는 외부 메서드가 없을 때)
fun main() {
val job1 = apiCall1()
val job2 = apiCall2(job1)
printWithThread(job2)
}
fun apiCall1(): Int {
delay(1_000L)
return 1
}
fun apiCall2(num: Int): Int {
delay(1_000L)
return num + 2
}
비동기 처리가 처음이다 보니 직관적으로 잘 와닿지 않는 부분이 많아 자꾸 질문드리네요 ㅜㅜ 죄송합니다.
답변 2
1
안녕하세요 응애님! 🙂 좋은 질문 감사합니다~~
비동기 자체가 직관적이지 않다보니 이해하기 되게 어려운 것 같아요!! 편하게 계속 질문 남겨주셔도 괜찮습니다~! 😊
하나씩 답변 드려 볼게요!!
[1. 본문 - awiat()을 사용하면 굳이 비동기 코드를 사용하지 않아도 되는가]
결론부터 말씀드리면 아래 코드 처럼 두 번째 작업이 첫 번째 작업에 의존하는 상황이라면, 비동기 코드를 사용하건~ 동기 코드를 사용하건 최종 시간은 동일합니다.
val job1 = async { apiCall1() }
val job2 = async { apiCall2(job1.await() }
예를 들어 apiCall1()
이 1초 apiCall2()
가 1초 걸린다면, 어차피 apiCall2()
는 apiCall1()
이 완전히 끝난 후 호출할 수 있기 때문에 총 2초가 걸리게 되고, 이는 그냥 동기적으로 작성하는 것과 같죠
하지만, 단순히 blocking + sync 스타일로 작성하는게 아니라 코루틴을 활용해 non-blocking + async 스타일로 작성하게 되면, 내부적으로는 조금 다르게 동작합니다.
blocking + sync 스타일로 작성하면 스레드가 2초간 완전히 blocking 되어 그 스레드를 다른 곳에 활용할 수 없지만, non-blocking + async 스타일로 작성하게 되면, API를 호출해야 할 때만 잠시 스레드를 사용하고, 그 외에는 다른 작업에 해당 스레드를 사용할 수 있게 되죠
결론적으로, 최종 시간은 동일하되 스레드 사용량은 코드 구현에 따라 다를 수 있습니다.
[2. 리스트를 순회하면서 suspend fun A, B를 호출했을 때는 단건 호출과 다르게 비동기 코드의 이점이 있다? (리스트 하나의 원소의 처리가 모두 다 끝나기를 기다리지 않고 다음 원소 처리로 넘어가는가?)]
이는 구현에 따라 다릅니다!
호출하는 함수가 Thread를 blocking 하고, for 문 전체를 하나의 Thread에서 돌리는 경우
이 경우는 loop의 로직 전체가 실행되어야만 다음 loop로 넘어갈 수 있기 때문에 리스트 처리 역시 하나씩 하나씩 이루어지게 됩니다.
호출하는 함수가 Thread를 blocking 하고, for 문 전체를 여러개의 Thread에서 돌리는 경우
이 경우는 몇개의 Thread에서 돌리느냐에 따라 여러 루프 로직이 동시에 실행되게 됩니다.
호출하는 함수가 Thread를 blocking 하지 않고, for 문 전체를 하나의 Thread에서 돌리는 경우
이 경우는 리스트 하나의 원소 처리가 모두 끝나지 않더라도 다음 리스트를 처리할 수 있습니다.
호출하는 함수가 Thread를 blocking 하지 않고, for 문 전체를 여러개의 Thread에서 돌리는 경우
3번과 동일하게, 리스트의 여러 원소를 동시에 처리할 수 있습니다.
Thread를 blocking 한다에 대해 더 말씀드리면
Thread.sleep(1_000L)
과 같은 코드로 스레드를 잠시 재워둘 수도 있고
delay(1_000L)
과 같은 코드로 해당 코루틴을 잠시 멈춰둘 수 있죠!
전자는 Thread자체가 blocking 되고, 후자는 Thread를 blocking 하지는 않습니다.
[3. EDIT - 리스트의 각 원소마다 새로운 코루틴이 생성되어 각각의 원소를 병렬처리할 수 있을 것으로 보았는데 맞을까요?]
이 역시 내부 구현이 중요합니다! 현재 구현상 리스트는 하나의 스레드에서만 돌도록 되어 있고요!
만약 리스트에서 특정 함수를 호출했는데, 그 함수 내부에 Thread를 blocking 하는 코드가 들어 있다면, 병렬 처리가 불가능합니다.
Thread를 blocking 하는 코드가 없다면, 위에서 말씀드린 3번 경우와 비슷하게 새로운 코루틴이 계속해서 생기며 각각의 원소를 병렬 처리할 수 있을거에요! 😊
답변이 도움이 되었으면 좋겠습니다.
감사합니다! 🙏
네네 맞습니다! 맞게 이해하셨습니다! 🙂
혹시나 스프링이라는 프레임워크를 사용하고 계시는거라면, 외부 API를 호출할 때 WebClient
를 이용해 non-blocking 호출을 할 수도 있습니다. 그렇게 되면 하나의 thread에서 여러 API 호출을 돌리려 할 때 훨씬 성능이 개선될 거에요!
좋은 방법 잘 찾으셨으면 좋겠습니다. 감사합니다! 🙏
0
앗 그리고 추가적으로, 만약 아래와 같이 main 함수 내에서 List를 돌면서 apiCall1, apiCall2를 호출했을 때,
동기적 코드
fun main() {
val list: List<Example> = listOf(Example(1), Example(2)... ,Example(100))
list.forEach { ex ->
val job1 = apiCall1(ex)
val job2 = apiCall2(job1)
}
}
fun apiCall1(ex: Example): Int {
// 네트워크를 타는 어떤 외부 api A
return 1
}
fun apiCall2(num: Int): Int {
// 네트워크를 타는 어떤 외부 api B
return num + 2
}
비동기적 코드
fun main(): Unit = runBlocking {
val list: List<Example> = listOf(Example(1), Example(2)... ,Example(100))
list.forEach { ex ->
val job1 = async { apiCall1(ex) }
val job2 = async { apiCall2(job1.await()) }
}
}
suspend fun apiCall1(ex: Example): Int {
// 네트워크를 타는 어떤 외부 api A
return 1
}
suspend fun apiCall2(num: Int): Int {
// 네트워크를 타는 어떤 외부 api B
return num + 2
}
이 상황에서는 apiCall2()
의 실행은 마찬가지로 apiCall1()
이 완료된 후에 진행되지만,
비동기적으로 코드를 작성했을 경우에는 리스트의 첫번째 원소의 apiCall1()
이 끝나면, apiCall2()
가 완료되기를 기다리지 않고, 바로 다음 원소의 apiCall1()
이 실행될 것으로 예상했는데... 혹시 맞을까요?
forEach 자체가 비동기 함수가 아니라서 아닐 수도 있겠네요...ㅜㅜ
첫번째 질문과 두번째 질문을 정리하자면,
1. 한 suspend fun A()의 반환값을 파라미터로 받는 다른 suspend fun B()가 있으면, B의 실행은 A가 종료된 이후에 실행되므로 동기적 코드에 비교했을 때 큰 이점이 없다?
2. 리스트를 순회하면서 suspend fun A, B를 호출했을 때는 단건 호출과 다르게 비동기 코드의 이점이 있다? (리스트 하나의 원소의 처리가 모두 다 끝나기를 기다리지 않고 다음 원소 처리로 넘어가는가?)
입니다.
EDIT) 혹시 다음과 같이 작성하면 리스트의 각 원소마다 새로운 코루틴이 생성되어 각각의 원소를 병렬처리할 수 있을 것으로 보았는데 맞을까요? 그리고 아래와 같은 상황이라면 apiCall1, apiCall2는 suspend fun이 아니어도 될 것 같습니다..!
fun main(): Unit = runBlocking {
val list: List<Example> = listOf(Example(1), Example(2)... ,Example(100))
list.forEach { ex ->
launch {
val job1 = apiCall1(ex)
val job2 = apiCall2(job1)
}
}
}
fun apiCall1(ex: Example): Int {
// 네트워크를 타는 어떤 외부 api A
return 1
}
fun apiCall2(num: Int): Int {
// 네트워크를 타는 어떤 외부 api B
return num + 2
}
질문량이 상당했는데 하나하나 정성스럽게 답변해주셔서 정말 감사합니다.
코루틴을 사용하여 비동기 코드를 작성하면 내부적으로 스레드를 다른 곳에 사용할 수 있게 되겠네요. 실행 속도에만 집중하다보니 그러한 이점을 생각지 못했습니다. (_ _)
3. 사실 제가 지금 작성하고 있는 코드에서 코루틴을 적용하려고 하다 보니 좀 지엽적인 질문이 되었는데요, 상세하게 답변 달아주셔서 정말 감사합니다! 저는 현재 유저 리스트를 순회하면서 각각의 유저에 대해 외부 API 2개를 호출하는 동작을 개발하고 있었습니다. 외부 API 호출 또한 네트워크 I/O 작업이므로 스레드를 Blocking 하게 되니까, EDIT)의 코드와 비슷한 형태로 코루틴을 적용하더라도 말씀 주신 4가지 경우 중
호출하는 함수가 Thread를 blocking 하고, for 문 전체를 하나의 Thread에서 돌리는 경우
이 경우는 loop의 로직 전체가 실행되어야만 다음 loop로 넘어갈 수 있기 때문에 리스트 처리 역시 하나씩 하나씩 이루어지게 됩니다.
위와 같은 상황에 해당하기 때문에 병렬 처리가 되지 않을 것으로 이해하였습니다. ㅎㅎ