개발을 하다 보면 ‘동기/비동기’, ‘블로킹/논블로킹’이라는 말을 정말 많이 듣게 됩니다. 비슷해 보이지만 명확히 다른 이 개념들, 처음에는 헷갈리기 쉽습니다. 하지만 서버 성능과 효율적인 코드 작성을 위해 반드시 알아야 할 핵심 개념이기도 합니다.
이번 포스트에서는 각 개념을 명확히 구분하고, 실제 자바(Java) 코드 예제를 통해 어떻게 동작하는지 확실하게 정리해 드리겠습니다.
1. 실행 순서: 동기(Synchronous) vs. 비동기(Asynchronous)
두 개념은 작업 간의 실행 순서를 결정하는 방식에 대한 이야기입니다.
동기(Synchronous)란?
요청을 보낸 후, 해당 요청의 응답이 올 때까지 기다리는 방식입니다.
한 작업이 완전히 끝나야만 다음 작업을 시작할 수 있는, 순차적인 처리 방식입니다.
- 특징: 작업이 순서대로 실행되어 흐름을 이해하기 쉽습니다.
- 예시: 전화 통화 📞
- 내가 말을 하면 상대방이 듣고 대답할 때까지 기다려야 다음 대화를 이어갈 수 있습니다.
비동기(Asynchronous)란?
요청을 보낸 후, 응답을 기다리지 않고 즉시 다음 작업을 실행하는 방식입니다.
요청한 작업이 언제 끝날지 신경 쓰지 않고, 일단 다른 작업을 계속 진행합니다. 나중에 응답이 오면 그때 처리합니다.
- 특징: 여러 작업을 동시에 처리할 수 있어 시스템 자원을 효율적으로 사용할 수 있습니다.
- 예시: 문자 메시지 💬
- 메시지를 보내놓고 상대방이 언제 답장할지 모르지만, 그동안 다른 일을 할 수 있습니다. 답장이 오면 그때 확인하면 됩니다.
2. 제어권: 블로킹(Blocking) vs. 논블로킹(Non-blocking)
두 개념은 요청한 작업이 자원을 사용하는 동안, 제어권이 누구에게 있는지에 대한 이야기입니다.
블로킹(Blocking)이란?
요청한 작업이 완료될 때까지 현재 실행 중인 스레드를 멈추고 기다리는 방식입니다.
A 함수가 B 함수를 호출하면, B 함수의 실행이 완전히 끝날 때까지 A 함수는 멈춘 상태(대기 상태)로 제어권을 넘겨줍니다.
- 특징: 호출된 함수의 작업이 끝날 때까지 다른 작업을 수행할 수 없습니다.
- 예시: 파일 읽기 작업 💾
- 파일을 읽는 코드를 실행하면, 파일을 다 읽어올 때까지 프로그램이 잠시 멈춥니다.
논블로킹(Non-blocking)이란?
요청한 작업을 시작한 후, 완료 여부와 상관없이 즉시 제어권을 반환하여 다음 코드를 실행하는 방식입니다.
A 함수가 B 함수를 호출해도 제어권을 계속 가지고 있으면서 다른 작업을 수행할 수 있습니다.
- 특징: 호출한 작업의 완료를 기다리지 않고 다른 작업을 계속할 수 있습니다.
- 예시: 논블로킹 파일 읽기 ⚡
- 파일 읽기를 요청한 후 즉시 제어권을 돌려받아 다른 코드를 실행하고, 파일 읽기가 완료되면 별도의 알림(콜백)을 통해 데이터를 처리합니다.
개념 한눈에 비교하기
구분 | 동기 (Synchronous) | 비동기 (Asynchronous) | 블로킹 (Blocking) | 논블로킹 (Non-blocking) |
---|---|---|---|---|
핵심 개념 | 작업의 순서 | 작업의 순서 | 제어권의 흐름 | 제어권의 흐름 |
설명 | 요청 후 응답을 기다림 | 요청 후 응답을 기다리지 않음 | 요청한 작업이 끝날 때까지 멈춤 | 요청한 작업이 끝나지 않아도 즉시 반환 |
예시 | 전화 통화 | 문자 메시지 | 대기 줄이 긴 맛집에서 기다리기 | 맛집에 대기 명단 올려놓고 다른 곳 구경하기 |
코드 실행 흐름 | 순차적으로 진행 | 다른 작업과 병행 가능 | 실행이 멈춤 (대기) | 실행이 멈추지 않음 |
4가지 조합으로 개념 완벽 이해하기 (Java 코드 예제)
이제 네 가지 개념을 조합하여 실제 코드에서 어떻게 나타나는지 살펴보겠습니다.
1. 동기 + 블로킹 (Sync + Blocking)
가장 일반적이고 직관적인 모델입니다. 요청을 보내고, 작업이 끝날 때까지 멈춰서 기다립니다.
public class SyncBlockingExample {
public static void main(String[] args) {
System.out.println("작업 시작");
try {
// 이 코드를 호출한 main 스레드는 3초 동안 멈춥니다(Blocking).
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("작업 완료");
}
}
- 실행 흐름
"작업 시작"
출력Thread.sleep(3000)
이 호출되고, main 스레드는 3초 동안 블로킹(멈춤)됩니다.- 3초 후, 제어권이 돌아오고
"작업 완료"
가 출력됩니다.
2. 동기 + 논블로킹 (Sync + Non-blocking)
요청을 보내고 바로 제어권을 돌려받지만, 응답이 왔는지 주기적으로 직접 확인(Polling)합니다.
public class SyncNonBlockingExample {
public static void main(String[] args) {
System.out.println("작업 시작");
long startTime = System.currentTimeMillis();
// 작업이 끝났는지 계속 확인하지만, 스레드가 멈추진 않습니다(Non-blocking).
while (System.currentTimeMillis() - startTime < 3000) {
// 다른 작업을 할 수 있지만, 계속 상태를 확인하느라 바쁩니다.
}
System.out.println("작업 완료");
}
}
- 실행 흐름
"작업 시작"
출력while
루프는 3초가 지났는지 멈추지 않고(Non-blocking) 계속 확인합니다.- 하지만 결과가 나올 때까지 기다렸다가 다음 코드로 넘어가는 동기 방식입니다.
- CPU 자원을 계속 사용하므로 비효율적입니다.
3. 비동기 + 블로킹 (Async + Blocking)
조금 특이한 조합입니다. 다른 스레드에서 비동기적으로 작업을 시작했지만, 그 결과를 얻기 위해 결국 멈춰서 기다리는 경우입니다.
import java.util.concurrent.*;
public class AsyncBlockingExample {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executor = Executors.newSingleThreadExecutor();
System.out.println("작업 요청 (비동기)");
// 별도의 스레드에서 작업을 실행 (비동기)
Future<String> future = executor.submit(() -> {
Thread.sleep(3000);
return "비동기 작업 완료";
});
System.out.println("다른 작업 수행 가능");
// 결과를 가져오기 위해 멈춰서 기다립니다 (블로킹)
String result = future.get();
System.out.println(result);
executor.shutdown();
}
}
- 실행 흐름
executor.submit()
으로 별도 스레드에서 3초짜리 작업을 비동기로 시작합니다.- main 스레드는 멈추지 않고
"다른 작업 수행 가능"
을 바로 출력합니다. future.get()
을 만나는 순간, 비동기 작업의 결과가 올 때까지 main 스레드는 블로킹(멈춤)됩니다.- 3초 후 비동기 작업이 끝나면 결과가 반환되고, 프로그램이 종료됩니다.
4. 비동기 + 논블로킹 (Async + Non-blocking)
가장 효율적인 모델입니다. 비동기적으로 작업을 요청하고, 멈추지 않고 다른 일을 하다가, 작업이 완료되면 콜백(Callback) 함수를 통해 결과를 처리합니다.
import java.util.concurrent.CompletableFuture;
public class AsyncNonBlockingExample {
public static void main(String[] args) throws InterruptedException {
System.out.println("메인 스레드: 작업 시작");
// 비동기 작업 시작 + 완료되면 실행할 콜백 함수 등록 (논블로킹)
CompletableFuture.supplyAsync(() -> {
System.out.println("별도 스레드: 작업 처리 중...");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {}
return "비동기 작업 완료";
}).thenAccept(result -> {
// 작업이 완료되면 이 부분이 실행됩니다.
System.out.println("메인 스레드: 콜백 실행! 결과: " + result);
});
System.out.println("메인 스레드: 다른 작업 수행 중...");
// 메인 스레드가 먼저 종료되는 것을 방지하기 위해 잠시 대기
Thread.sleep(5000);
}
}
- 실행 흐름
supplyAsync()
로 비동기 작업을 요청하고, main 스레드는 멈추지 않고(Non-blocking) 바로 다음 코드로 넘어갑니다."메인 스레드: 다른 작업 수행 중..."
이 즉시 출력됩니다.- 별도의 스레드에서 3초간의 작업이 진행됩니다.
- 3초 후 작업이 완료되면,
thenAccept()
에 등록된 콜백 함수가 실행되어 결과를 출력합니다.
최종 정리
블로킹 (Blocking) | 논블로킹 (Non-blocking) | |
---|---|---|
동기 (Synchronous) | Sync + Blocking (가장 흔한 모델) Thread.sleep() | Sync + Non-blocking (주기적 확인, 비효율적) while 루프 Polling |
비동기 (Asynchronous) | Async + Blocking (결과를 기다리며 블로킹) future.get() | Async + Non-blocking (가장 효율적인 모델) CompletableFuture |
- 동기 + 블로킹: 호출한 함수가 끝날 때까지 기다리며 멈춘다.
- 동기 + 논블로킹: 함수 호출 후 바로 반환받지만, 결과가 나올 때까지 주기적으로 확인하며 기다린다.
- 비동기 + 블로킹: 별도 스레드에서 실행시키지만, 결국 결과를 얻기 위해 멈춰서 기다린다.
- 비동기 + 논블로킹: 별도 스레드에서 실행시키고 신경 끄고 다른 일을 하다가, 결과가 오면 콜백으로 처리한다.
이제 동기/비동기, 블로킹/논블로킹의 차이점이 확실히 정리되었을 거예요! 이 개념들을 잘 활용하여 더 효율적이고 성능 좋은 코드를 작성해 보세요. 🚀
