글을 작성하게 된 계기
Blocking, Non-Blocking, Sync, Async의 개념을 명확하게 설명하지 못하는 것을 보고 이를 확실히 정리하기 위해 글을 작성하게 되었습니다.
- Blocking
- Non-Blocking
- Sync
- Async
1. Blocking / Non-Blocking
Blocking과 Non-Blocking은 한 작업이 실행되는 동안 다른 작업을 할 수 있는지, 프로그램의 실행 제어권을 누가 가지고 있는지에 따라 구분할 수 있습니다. 이를 간단한 그림과 함께 살펴보겠습니다.
- Blocking: 프로그램의 제어권을 넘긴다.
- Non-Blocking: 프로그램의 제어권을 넘기지 않는다.
1-1. Blocking
Blocking은 프로세스나 쓰레드가 특정 이벤트/조건이 충족될 때까지 대기하는 상태 를 의미합니다. 한 작업이 실행되는 동안 다른 작업은 멈추게 되며, 프로그램의 제어권을 넘겨줍니다.
아래 그림에서 파란색 작업은 빨간색 작업이 끝날 때까지 멈춘 후, 끝나면 자신의 작업을 이어서 실행되게 됩니다. 즉, 프로그램의 실행 제어권을 빨간색에 넘겼다가 다시 돌려받는 것이죠.
1-2. Non-Blocking
Non-Blocking은 특정 작업이 완료되는 동안 다른 작업을 처리할 수 있게 하는 입/출력 처리의 형태 입니다. 이는 함수/메서드를 호출한 후, 작업을 멈추지 않고 진행중인 작업을 이어 가며, 프로그램의 제어권을 넘기지 않습니다.
아래 그림에서 파란색은 빨간색과 별도로 작업이 진행됩니다. 즉, 파란색은 빨간색과 별도의 문맥에서 실행되며, 함수/메서드 호출만 할 뿐, 자신의 제어권을 넘기지 않습니다.
함수/메서드의 차이에 대해서는 해당 글을 참조해보세요.
2. Sync/Async
동기(Synchronization)와 비동기(Asynchronous)는 순서 와 결과 에 관심이 있는지 아닌지로 구분할 수 있습니다.
- 동기(Synchronization): 작업 실행 순서와 작업 결과에 관심이 있습니다.
- 비동기(Asynchronous): 작업 실행 순서와 작업 결과에 관심이 없습니다.
2-1. Sync
동기는 특정 작업의 실행이 순차적으로 진행 되며, 작업의 결과가 다음 작업의 실행에 영향 을 미칠 수 있습니다.
즉, 순차적으로 진행되며, 한 작업의 결과가 다음 작업에 영향을 미칠 수 있습니다. 아래 그림에서 파란색 작업은 빨간 작업이 끝난 후에 실행됩니다.
2-2. Asynchronous
비동기는 작업이 독립적으로 진행되며, 다른 작업을 즉시 실행할 수 있습니다. 따라서 여러 작업이 동시에 진행될 수 있으며 작업 완료 순서는 보장되지 않습니다. 한 작업의 결과는 다음 작업의 실행에 영향을 줄 수도, 아닐 수도 있습니다.
이를 그림으로 보면 다음과 같은데, 파란색 작업은 빨간색 작업을 호출한 후, 바로 자신의 작업을 처리합니다. 둘은 별도로 실행되며, 작업 완료 순서는 보장되지 않습니다.
비동기 함수를 사용할 때, 주의할 점이 있는데, 프로그램이 비동기로 동작하면 동기 함수가 있어서는 안 되는 것 입니다. 하나의 Blocking 함수로 인해 전체 프로그램이 동기적으로 동작할 수도 있기 때문입니다.
프로그램이 Non-Blocking으로 동작 하려면 모든 함수가 Non-Blocking이어야 하며, I/O Bound Blocking 또한 발생하면 안 됩니다. 이는 스프링 WebFlux, Node의 EventLoop를 사용할 경우, 한 쓰레드의 Blocking이 전체 성능을 좌우할 수도 있습니다.
참고로 Thread.sleep는 쓰레드의 실행을 동기적으로 일시 정지시키는 기능 이며, 작업을 수행하는 것이 아닌 실행 흐름을 일시적으로 제어하는 데 사용됩니다.
1
2
3
public static void main(String[] args) {
Thread.sleep(ONE_MINUTES);
}
3. 비교
해당 주제에 관심을 가지고 찾다 보면 아래와 같은 표 를 한 번쯤 보신 적이 있을 텐데요, 위에서 정리한 내용을 토대로 이를 살펴보겠습니다.
Blocking/Async는 Not exist라고 적혀있지만, 자바의 CompletableFuture에서 join/get 메서드를 사용하면 분할된 태스크는 비동기로 처리되지만, 모든 작업이 완료될 때까지 다른 쓰레드는 대기 상태로 있게 됩니다.
3-1. Blocking과 Sync/Async 모델
Blocking/Sync 모델은 작업이 순차적으로 실행되며, 한 작업이 끝날 때까지 다음 작업이 실행되지 않습니다. 반면 Blocking/Async 모델은 특정 작업의 작업 완료를 기다리는 동안에도 비동기적으로 작업이 실행됩니다.
Blocking/Sync 모델은 일반적으로 많이 볼 수 있기에 Blocking/Async 모델의 예제를 살펴보겠습니다. CompletableFuture의 supplyAsync 메서드는 작업을 비동기로 실행합니다. 즉, taskA와 taskB는 비동기적으로 동작합니다. 하지만 join 메서드가 있기 때문에 taskA가 먼저 끝나더라도 taskB의 실행 결과를 기다립니다. 전체 프로그램은 Blocking으로 동작하지만, 각 작업은 비동기로 동작하게 되는 것입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fun main() {
// 비동기
val taskA = CompletableFuture.supplyAsync {
Thread.sleep(1_000)
1
}
// 비동기
val taskA = CompletableFuture.supplyAsync {
Thread.sleep(2_000)
2
}
// 동기. taskA와 taskB가 모두 끝날 때까지 기다린다.
val result = taskA.join() + taskB.join()
}
Blocking/Async 모델은 일반적이지 않기 때문에 이런 모델이 있다는 것 정도만 인지하고 있도록 합니다. 이에 대해 조금 더 자세히 알고 싶다면 해당 글을 참조해 보세요.
3-2. Non-Blocking과 Sync/Async 모델
Non-Blocking과 Sync/Async을 조합한 모델은 모두 자주 사용됩니다.
Non-Blocking/Sync 모델은 Non-Blocking 작업 중, 특정 조건/시점에 동기적 대기/확인이 필요할 때 사용합니다. 아래 작업에서 job은 비동기적으로 실행되지만, while 문에서 job의 실행 여부를 동기적으로 체크합니다.
1
2
3
4
5
6
7
8
9
10
11
12
fun main() = runBlocking {
// 비동기로 작업 실행
val job = launch {
delay(1_000L)
}
// while문이 실행되며 job의 상태를 주기적으로 체크
while (job.isActive) {
// job이 active가 아니라면 동기적으로 while문 실행
delay(500L)
}
}
Non-Blocking/Async 모델은 Non-Blocking 작업 중, 다른 작업을 비동기로 호출합니다. 이를 통해 두 작업은 전혀 다른 문맥에서 실행될 수 있습니다. 아래 두 작업은 서로 다른 문맥에서 비동기로 작업하므로, 각 작업은 비순차적으로 진행되지만, 한 작업의 결과가 다른 작업에 영향을 미치지 않습니다.
1
2
3
4
5
6
7
8
9
fun main() = runBlocking {
val jobB = launch {
delay(1_000L)
}
val jobB = launch {
delay(1_000L)
}
}
3-3. Blocking vs Sync
Blocking은 한 작업이 끝날 때까지 다른 작업을 수행할 수 없는 것을 말하며, 동기는 작업이 순차적으로 진행되는 것을 말합니다. Blocking은 동기 처리의 한 방법이지만, 모든 동기 작업이 Blocking이 되진 않습니다.
즉, Blocking/Sync 모델이 일반적으로 사용되지만, Blocking이라고 반드시 동기적으로 동작하지는 않습니다.
위에서 봤던 예제를 다시 살펴보면, 다음과 같을 경우 동기 작업이지만 Non-Blocking을 사용 할 수도 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
fun main() = runBlocking {
// 비동기로 작업 실행
val job = launch {
delay(1_000L)
}
// 비동기 작업의 완료를 폴링하는 것보다는 코루틴 제공 기능을 사용
while (job.isActive) {
// job이 active가 아니라면 동기적으로 while문 실행
delay(500L)
}
}
3-4. Non-Blocking vs Async
Non-Blocking은 호출된 작업이 즉시 반환되는 것을 의미하며, 이는 작업이 완료되었는지 여부와 관계없이 다음 작업으로 넘어갈 수 있습니다. 반면, 비동기는 작업의 완료 여부를 비동기적으로 알리며, 작업의 실행과 완료 사이에 다른 작업을 수행할 수 있도록 합니다.
4. 콜백(Callback)
콜백(Callback)은 실행 가능한 코드에 대한 참조 로, 다른 코드 조각에 인자(argument)로 전달됩니다.
4-1. 동기 콜백(Synchronous Callback)
동기 콜백(Synchronous Callback)은 함수가 호출되는 즉시 실행 되며, 함수의 실행 완료를 호출한 코드가 작업 완료 를 기다립니다. 즉, 코드의 작업 순서/실행 순서를 중요시 하는데, 아래 함수에서 numbers의 원소들은 순차적으로 출력됩니다.
1
2
3
4
5
6
7
8
9
fun main() {
val numbers = listOf(1, 2, 3, 4, 5)
// 순차적으로 진행 및 결과 반환
numbers.forEach { element ->
println(element)
}
println("Hello World")
}
4-2. 비동기 콜백(Asynchronous Callback)
비동기 콜백(Asynchronous Callback)은 호출한 코드가 콜백 함수의 작업 완료를 기다리지 않고 즉시 다음 작업을 수행 합니다. 아래 함수에서 launch 내부 코드는 비동기로 실행되며, 해당 코드 블록을 호출한 후 즉시 다음 작업을 실행합니다.
1
2
3
4
5
6
7
8
9
10
fun main() = runBlocking {
// 비동기
launch {
delay(1_000L)
println("Work")
}
// 즉시 실행
println("Hello World")
}
5. 함수형 프로그래밍
함수형 프로그래밍에서 함수는 함수를 호출한 쓰레드에서 실행 됩니다. 즉, 함수형 인터페이스를 인자로 넘기면 이를 호출한 쪽에서 실행되는 것입니다.
1
2
3
4
5
6
7
8
9
fun sum(a: Int, b: Int): Int {
println("${Thread.currentThread().name}")
return a + b
}
fun main() {
val result = sum(5, 7)
println("${Thread.currentThread().name}")
}
1
2
main
main
6. 예외
동기 함수/메서드는 호출한 쓰레드에서 순차적으로 실행됩니다. 동기 메서드 내에서 예외가 발생하면, 이 예외는 호출 스택을 따라 역전파됩니다.
1
2
3
4
5
6
7
8
9
10
11
fun call() {
throw Exception()
}
fun main() {
try {
call()
} catch (ex: Exception) {
// 예외 처리
}
}
비동기 작업은 다른 문맥에서 실행되기 때문에, 예외 처리 방식이 동기 방식과 다릅니다. 비동기 작업에서 발생한 예외는 해당 작업을 호출한 쓰레드로 전파되지 않으며, 비동기 작업의 결과를 처리하는 콜백 함수나 다른 방법을 통해 예외를 처리해야 합니다.
1
2
3
4
5
6
7
8
9
10
11
fun main() = runBlocking {
val deferred = async(Dispatchers.Default) {
throw Exception()
}
try {
deferred.await() // 비동기 작업의 결과(또는 예외)를 기다림
} catch (ex: Exception) {
// 예외 처리
}
}
즉, 예외가 발생하더라도 다른 작업에 영향을 미치지 않습니다.
7. 정리
Blocking, Non-Blocking, Sync, Async의 개념에 대해 살펴보았습니다. 이는 크게 제어권, 순서와 결과의 관심 유무 에 따라 나눌 수 있는데요, 헷갈릴 수 있으므로 철저한 복습으로 자기 것으로 만들 수 있도록 합시다.
- Blocking: 한 작업이 끝날 때까지 다른 작업을 하지 않으며, 프로그램의 제어권을 넘긴다.
- Non-Blocking: 한 작업이 끝나지 않더라도 다른 작업을 실행하며, 프로그램의 제어권을 넘기지 않습니다.
- Sync: 작업이 순차적으로 진행되며, 한 작업의 완료를 기다린 후 다음 작업을 시작합니다.
- Async: 작업이 비 순차적으로 진행되며, 한 작업의 완료가 다음 작업에 영향을 미칠 수도, 아닐 수도 있습니다.

