동기/비동기 + 블로킹/논블로킹 네 가지 조합 정리
현대 프로그래밍에서 I/O나 병렬 처리 구조를 이해하려면 "동기/비동기", "블로킹/논블로킹"의 차이를 정확히 파악해야 한다. 이 둘은 각각의 의미가 다르며, 조합에 따라 네 가지 상황이 생긴다. 이 글에서는 각 조합을 직관적인 비유와 실제 기술 사례로 정리한다.
동기 vs 비동기
동기(Synchronous)
동기는 어떤 작업을 요청한 뒤, 그 작업이 끝날 때까지 계속 기다리는 방식이다.
예를 들어, 친구에게 “물 좀 떠와”라고 시켰을 때, 친구가 물을 가져올 때까지 그 자리에 멈춰서 기다리는 것과 같다.
즉, 결과가 나올 때까지 다른 일을 하지 못한다.
비동기(Asynchronous)
비동기는 작업을 요청한 뒤, 결과가 나올 때까지 기다리지 않고 다른 일을 할 수 있는 방식이다.
친구에게 “물 좀 떠와”라고 시키고, 나는 내 할 일을 계속한다.
친구가 물을 가져오면 그때 다시 신경 쓰면 된다.
즉, 결과가 준비될 때까지 기다릴 필요가 없다.
블로킹 vs 논블로킹
블로킹(Blocking)
블로킹은 어떤 작업을 요청했을 때, 그 작업이 끝날 때까지 프로그램(혹은 스레드)이 멈춰 있는 상태다.
예를 들어, 은행 창구에서 번호표를 뽑고 내 차례가 올 때까지 줄에서 계속 기다리는 것과 비슷하다.
그동안 다른 일을 할 수 없다.
논블로킹(Non-Blocking)
논블로킹은 작업을 요청해도 프로그램이 멈추지 않고, 바로 다음 일을 할 수 있는 상태다.
예를 들어, 은행에 가서 번호표를 뽑고, 내 차례가 올 때까지 근처 카페에서 커피를 마시며 기다리는 것과 같다.
내 차례가 되면 알림을 받고 창구로 가면 된다.
네 가지 조합
이제 이 두 가지 개념을 조합하면 총 네 가지 상황이 만들어진다.
1. 동기 + 블로킹
특징
- 작업이 끝날 때까지 호출한 코드가 멈춘다.
- 가장 전통적인 방식이며, 이해하기 쉽고 구현도 단순하다.
- 동시성이나 병렬성을 고려하지 않아도 된다.
실생활 비유
- 식당에서 음식을 주문하고, 음식이 다 나올 때까지 식탁에서 계속 기다리는 상황.
실제 예
- Java의
HttpURLConnection - 대부분의 전통적 웹 프레임워크 (예: Spring MVC, Django)
예제 코드
URL url = new URL("http://example.com");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
InputStream in = conn.getInputStream(); // 여기서 멈춤 (블로킹)
2. 동기 + 논블로킹
특징
- 호출자는 함수가 끝날 때까지 결과를 기다리지만,
- 함수 내부에서는 계속 상태를 확인하거나, "아직 안 됐어"라고 바로 알려준다.
- CPU가 유휴 상태 없이 계속 바쁘게 움직인다.
실생활 비유
- 버스를 기다리는데, 계속 정류장에 나가서 버스가 왔는지 확인하는 사람.
- 택배가 왔는지 계속 문을 열어보는 것과 같다.
실제 예
- C의
select()혹은poll()을 활용한 이벤트 루프 - GUI 프로그램의 이벤트 폴링 루프
예제 코드 (Java)
while (true) {
if (checkEvent()) {
System.out.println("이벤트 발생");
break;
}
try {
Thread.sleep(1000); // 1초마다 확인
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
3. 비동기 + 블로킹
특징
- 작업은 다른 스레드에서 비동기로 실행되지만,
- 호출자는 결과를 기다리기 위해 스레드를 멈춘다.
- 구조는 비동기지만, 실제로는 동기 블로킹과 비슷하게 동작한다.
실생활 비유
- 친구에게 과자 사오라고 시켰지만, 결과가 나올 때까지 소파에 가만히 앉아 있는 것.
- "숙제 다 할 때까지 TV 안 볼래."와 비슷하지만, 숙제는 동생이 대신 해주는 상황.
실제 예
- Java의
Future.get() - Kotlin의
Deferred.await()(코루틴 외부에서)
예제 코드 (Java)
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(() -> {
Thread.sleep(3000);
return "과자";
});
String result = future.get(); // 여기서 블로킹
4. 비동기 + 논블로킹
특징
- 작업은 백그라운드에서 비동기로 처리되고,
- 호출자는 결과를 기다리지 않고 다음 작업을 계속 진행할 수 있다.
- 고성능 시스템에 가장 적합한 방식이다.
실생활 비유
- 택배를 주문한 뒤 잊어버리고 있다가, 알림이 오면 문을 여는 상황.
실제 예
- JavaScript의
Promise기반 처리 - Spring WebFlux, Node.js
- Kotlin Coroutine, Reactor, RxJava
예제 코드 (Java)
CompletableFuture.supplyAsync(() -> {
// 비동기 작업 예시: 데이터 가져오기
try {
Thread.sleep(2000); // 지연 시뮬레이션
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return "데이터";
}).thenAccept(data -> {
System.out.println("받은 데이터: " + data);
});
네 가지 조합 한눈에 보기
| 조합 | 설명 | 사용 예 |
|---|---|---|
| 동기 + 블로킹 | 요청 후 결과 나올 때까지 멈춤 | Java IO, 전통 웹 프레임워크 |
| 동기 + 논블로킹 | 직접 계속 상태 확인 | select(), GUI 폴링 루프 |
| 비동기 + 블로킹 | 남에게 맡기고 결과 올 때까지 기다림 | Future.get() |
| 비동기 + 논블로킹 | 남에게 맡기고 나는 다른 일 함 | WebFlux, Node.js, JS Promise |
마무리 요약
이 구조는 단순한 개념 차이를 넘어서, 프로그램의 성능과 아키텍처를 크게 바꾼다.
각 방식의 차이를 정확히 이해하고, 상황에 맞게 선택하는 것이 중요하다.
추가 설명: 언제 어떤 방식을 써야 할까?
동기 + 블로킹
- 가장 단순하고, 코드가 이해하기 쉽다.
- 하지만 여러 작업을 동시에 처리해야 하는 상황에서는 비효율적이다.
동기 + 논블로킹
- CPU를 계속 사용해야 하므로, 실제로는 잘 쓰이지 않는다.
- 하지만 아주 단순한 이벤트 루프나, 오래된 GUI 프로그램에서는 볼 수 있다.
비동기 + 블로킹
- 비동기 작업을 지원하지만, 결과를 꼭 기다려야 하는 상황에 쓴다.
- 예를 들어, 여러 작업 중 하나의 결과가 꼭 필요할 때.
비동기 + 논블로킹
- 현대 웹 서버, 대규모 데이터 처리, 실시간 서비스 등에서 가장 많이 쓰인다.
- 효율적으로 자원을 사용하고, 많은 요청을 동시에 처리할 수 있다.