안녕하세요! NewCodes입니다!
프로그래밍을 배우다보면 꼭 마주치는 개념이 있죠!
바로 동기 / 비동기 / 블로킹 / 논블로킹입니다.
처음 접하면 너무 헷갈리고 어렵지만,
이 글 하나로 명확하게 정리해드릴게요! 🔥
이를 잘 알아야
컴퓨터 자원을 효율적으로 사용할 수 있고,
빠른 성능의 프로그램을 만들 수 있습니다.
읽고 나면
Node.js, Spring Webflux, Redis, NginX
등과 같은 기술을 더욱 잘 이해할 수 있게 되실 겁니다!
또, 효율적인 코드를 작성하는 데 도움이 되실 겁니다!
🎯 동기, 비동기, 블로킹, 논블로킹의 차이점
'동기 비동기 블로킹 논블로킹' 해당 개념은 운영체제에서 비롯된 개념입니다. 느린 I/O를 어떻게 하면 최적화할 수 있을까를 고민한 결과의 산물입니다.
하나씩 차근차근 살펴보시죠!
동기 <-> 비동기
동기와 비동기는 작업 완료 여부를 기다리느냐 아니냐로 차이가 발생합니다!
아래 그림을 유심히 살펴보시죠!
동기는 작업의 완료 여부를 신경 쓰며 기다립니다.
동기는 항상 작업의 완료 여부를 기다리기에, 이전 작업이 끝나야만 다음 작업으로 넘어갑니다.
그렇기에 코드 실행 흐름이 순차적이라 할 수 있습니다.
비동기는 작업의 완료 여부를 신경 쓰지 않기에 기다리지 않습니다.
비동기는 항상 작업의 완료 여부를 기다리지 않기에, 이전 작업을 기다리지 않고 다음 작업을 바로 실행합니다.
그렇기에 코드 실행 흐름이 순차적이지 않을 수 있습니다.
프로세스가 여러 개인 상황도 살펴보시죠!
request1 -> response1 -> request2 -> response2... 이렇게 동기는 요청과 응답이 순차적으로 이루어지는 걸 볼 수 있습니다.
request1 -> request2 -> response2 -> response1... 이렇게 비동기는 요청과 응답이 순차적이지 않습니다.
블로킹 <-> 논블로킹
블로킹과 논블로킹은 제어권을 가지고 있느냐 아니냐로 차이가 발생합니다!
제어권이란 코드를 실행할 수 있는 권리라고 보시면 됩니다.
아래 그림을 유심히 살펴보시죠!
블로킹은 호출한 측에 제어권을 넘겨줍니다.
그렇기에 호출한 작업이 실행되는 동안 자신은 block되어 다른 작업을 실행할 수 없습니다.
논블로킹은 호출한 이후에도 제어권을 그대로 가지고 있습니다.
그렇기에 호출한 작업이 실행되는 동안 자신은 block되지 않기에 다른 작업을 실행할 수 있습니다.
동기/비동기 <-> 블로킹/논블로킹
동기, 비동기, 블로킹, 논블로킹 이 4가지 단어는 항상 함께 따라다니곤 하는데요!
동기/비동기와 블로킹/논블로킹은 얼핏 보면 비슷해 보이는데 어떤 차이가 있을까요?
차이점은 초점이 다르다는 것입니다! 아래 그림을 살펴보시죠!
동기/비동기는 코드 실행 흐름이 어떻게 되는지에 초점을 둔 개념입니다.
블로킹/논블로킹은 스레드 상태가 block되는지 안 되는지에 초점을 둔 개념입니다.
보통 동기와 비동기는 API를 설계하거나, 라이브러리를 사용할 때 더욱 신경 써야 하는 개념입니다. 이를 알아야 실행 흐름을 파악하고 그에 따라 코드를 작성할 수 있기 때문이죠. 예를 들면 아래와 같습니다.
JavaScript 언어로 라이브러리를 사용할 때, ‘이 라이브러리의 함수는 비동기 함수구나. 그럼 이 함수를 호출한 후에 바로 결과가 나오는 게 아니니까, Promise나 callback으로 결과를 받아 처리해야겠네.’와 같이 동기/비동기가 고려될 수 있습니다.
블로킹과 논블로킹은 주로 성능적인 측면에서 참고하는 개념입니다. 리소스를 잘 활용하고 있는지, 치명적인 block이 없는지 등을 판단할 수 있습니다. 이와 관련한 글 'Node.js에서 readFileSync를 쓰면 안 되는 이유'를 추천합니다!
Node.js에서 readFileSync를 쓰면 안 되는 이유 (feat. Event Loop)
안녕하세요! NewCodes입니다! Node.js에서`readFileSync`를 써보신 적이 있으신가요? `readFileSync`는 특별한 경우를 제외하고써서는 안 되는 함수입니다. `readFileSync`를 쓰면주방에 있는 셰프가 멈추는 것과
newcodes.tistory.com
코드 실행 흐름만 신경 써서 동기 함수인 readFileSync를 사용했다가,
main thread가 block되어 성능을 놓치게 된 케이스에 대해서 짚어낸 글입니다.
🍀 동기, 비동기, 블로킹, 논블로킹의 조합
Linux의 I/O를 기반으로 해서 각각의 조합에 대해 설명드리겠습니다!
Linux I/O에 대해서 잘 모르더라도 이해할 수 있도록 작성했으니 걱정 안 하셔도 됩니다!
1) 동기 블로킹 I/O
동기 블로킹 I/O는 전형적으로 사용되는 I/O 방식입니다.
동기/블로킹 I/O는 작업이 완료되는 걸 기다리며,
이 동안 block되어 다른 작업을 수행할 수 없는 방식입니다.
코드를 작성한 순서대로 한 줄 한 줄 실행되는 절차적인 방식입니다. 장점으로는 코드의 실행 흐름을 예측하기 쉽다는 점입니다. 그렇다 보니 디버깅이나 테스트하기가 용이합니다.
단점으로는 성능 문제가 있습니다. 여러 작업을 동시적으로 수행해야 할 때 전체적인 지연이 유발될 수 있습니다. 특정 작업이 하나라도 오래 걸린다면 전체 작업 완료 시간은 그만큼 더 늘어나기 때문입니다.
말로만 하면 안 와닿을 수 있으니, Java 코드 예제를 살펴보시죠!
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
System.out.print("입력하세요: ");
Scanner scanner = new Scanner(System.in);
String input = scanner.nextLine(); // 🔴 블로킹 발생
System.out.println("이제야 출력됩니다!");
System.out.println("입력값: " + input);
scanner.close();
}
}
이를 실행하면 아래와 같이 출력됩니다.
코드의 흐름이 순차적으로 실행되며, `scanner.nextLine`에서는 block됩니다. 사용자가 입력값을 전달해야만 다음 코드가 실행될 수 있습니다. 또, 사용자가 값을 입력하는 동안은 다음 코드가 실행되지 못합니다.
2) 비동기 블로킹 I/O
이번엔 비동기 블로킹 I/O입니다. 이는 실제로 잘 쓰이지 않지만, 그래도 살펴봅시다!
비동기 블로킹 I/O는 작업의 결과를 기다리지 않아도 되지만,
block이 되어 다른 작업을 못하는 방식입니다.
select 시스템콜을 통해 파일 디스크립터를 감시하며 파일 읽기 완료에 대한 이벤트를 받습니다. 하지만, select는 blocking 방식이기에 제어권을 잃고 다른 작업을 할 수 없게 됩니다.
비동기 블로킹 I/O는 비동기의 장점이 블로킹에 의해 희석되는 셈이 됩니다. 비동기의 장점은 작업의 결과를 기다리지 않기에 다른 작업을 수행할 수도 있다는 장점이 있지만, block되어 그렇게 하진 못합니다. 그래서 이는 잘 사용되지 않는 방식입니다.
3) 동기 논블로킹 I/O
이번엔 동기 논블로킹 I/O입니다!
동기 논블로킹 I/O는 block되지 않으며,
작업의 결과 완료 여부를 계속해서 물어보는 방식입니다.
읽기 작업이 수행되는 동안 Application은 여러 번 Kernel에게 읽기 작업이 완료되었는지 물어봅니다. 하지만, 완료되지 않았다면 `EAGAIN / EWOULDBLOCK`이라는 결과를 반환합니다. 이것의 의미는 지금은 읽을 수 있는 데이터가 없다는 것입니다.
비동기 블로킹 I/O보다 나은 점은 다른 I/O를 동시에 처리할 수 있다는 점입니다. 제어권이 있는 상태이기에 다른 I/O를 요청할 수 있습니다. 단점으로는 계속해서 완료 여부를 확인하는 방식은 효과적이지 않습니다. 작업의 완료시점과 결과 확인시점 사이에 gap이 있기 마련이기 때문입니다.
이 또한 Java 코드로 살펴보시죠!
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
class MyTask implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("작업 시작합니다!");
Thread.sleep(2000); // 작업 시간 시뮬레이션
return "Hello World";
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
FutureTask<String> futureTask = new FutureTask<>(new MyTask());
Thread thread = new Thread(futureTask);
thread.start(); // 논블로킹
System.out.println("start() 호출했지만, block되지 않았지롱!");
// 동기: 작업이 완료되었는지 반복해서 확인
while (!futureTask.isDone()) {
System.out.println("아직 작업 중인가 보군...");
Thread.sleep(300); // 너무 빠른 반복을 막기 위해 잠깐 쉼
// 아래에 코드 작성하면, 확인하면서도 다른 작업 가능
}
try {
String result = futureTask.get(); // 결과 받기
System.out.println("작업 완료! 결과: " + result);
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println("프로그램 종료");
}
}
이를 실행하면 아래와 같이 출력됩니다.
Java를 잘 모르더라도 읽어보시면 대략적으로 감이 오실 겁니다! `thread.start()`를 통해 작업을 실행하고, 이는 다른 스레드를 사용하기에 논블로킹 방식으로 작동합니다. 이에 대한 결과를 동기적으로 확인하기 위해 `futureTask.isDone()`을 통해 매번 작업이 완료되었는지 확인합니다.
그리고 작업이 완료되면 while문을 벗어나 작업의 결과를 받을 수 있습니다!
4) 비동기 논블로킹 I/O
드디어 마지막 비동기 논블로킹 I/O입니다! 이는 중요하니 더 꼼꼼히 봅시다!
비동기 논블로킹 I/O는 작업의 결과를 신경 쓰지 않아도 되고,
block도 되지 않기에 다른 작업을 자유롭게 수행할 수 있는 방식입니다.
작업의 결과를 신경 쓰지 않아도 되는 이유는 무엇일까요? 바로 callback이 있기 때문입니다. callback이란 어떤 작업이 끝난 뒤 실행될 함수를 뜻합니다. "실행이 다 끝나면 그 결과를 토대로 이 함수 좀 대신 실행시켜 줘~" 하는 느낌입니다. 이 callback을 통해 작업의 결과를 처리하기 때문에 작업을 직접 기다리지 않아도 됩니다.
비동기 논블로킹 I/O는 자원을 효율적으로 사용하고, 성능이 빠르다는 장점이 있습니다. 느린 I/O 작업을 처리하는 동안 다른 일을 하는 건 매우 효율적이겠죠!
하지만 단점으로는 디버깅이나 테스트가 어려울 수 있습니다. 그 이유는 프로그램 코드를 작성한 순서대로 실행되지 않기 때문입니다. 또한, 작업의 결과를 예측하는 게 어려울 수 있기 때문입니다.
이것도 Java 코드를 통해 살펴보시죠!
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.channels.CompletionHandler;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
public class Main {
public static void main(String[] args) throws IOException {
AsynchronousFileChannel channel = AsynchronousFileChannel.open(
Paths.get("sample.txt"), StandardOpenOption.READ
);
ByteBuffer buffer = ByteBuffer.allocate(100);
// 비동기 논블로킹 I/O 방식으로 파일 읽기
channel.read(buffer, 0, buffer, new CompletionHandler<>() {
// 콜백 전달
@Override
public void completed(Integer result, ByteBuffer attachment) {
attachment.flip();
byte[] data = new byte[attachment.limit()];
attachment.get(data);
System.out.println("파일 내용: " + new String(data));
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
System.out.println("읽기 실패: " + exc.getMessage());
}
});
System.out.println("파일 읽기 요청했다! block 안 됐지롱!"); // block 안 되기에 바로 실행 가능
// (메인 스레드가 먼저 종료되지 않도록 대기)
try { Thread.sleep(1000); } catch (InterruptedException e) {}
}
}
이를 실행하면 아래와 같이 출력됩니다!
block이 되지 않았기에 파일 읽기가 완료되기 전에
다음 코드인 `System.out.println("파일 읽기 요청했다! block 안 됐지롱!")`를 실행할 수 있습니다.
정리
지금껏 다뤘던 4가지 조합을 하나의 그림으로 정리하겠습니다!
이 4가지를 보지 않고도 설명할 수 있을 정도가 되면 잘 이해한 거라 할 수 있습니다!
비동기/논블로킹이 제일 좋은 거 아닌가?
4가지 중에서 어떤 게 제일 좋아 보이나요? 바로 비동기/논블로킹 방식일 겁니다. 이러한 원리를 성능이 좋기로 유명한 Node.js, Netty, NginX, Redis, Spring Webflux 등에서 적용되고 있고요!
그러면 비동기/논블로킹만 쓰면 되지 굳이 4가지를 다 알아야 하나 싶을 수 있습니다.
그래도 비동기/논블로킹에는 단점이 존재합니다. 작업의 흐름이 순차적이지 않기에 코드의 흐름을 파악하는 데 어려움이 있을 수 있습니다. 또, 여러 작업이 백그라운드에서 실행되는 경우가 많기에 테스트 코드를 작성하기도 만만치 않을 수 있습니다.
이는 개발자들로 하여금 유지보수 비용이나 추가 기능을 개발하는 비용을 높이게 됩니다. 컴퓨터 자원을 아끼는 것도 좋지만, 개발자의 자원을 아끼는 것도 중요합니다. 그렇기에 각 상황마다 적절한 방식을 택하는 게 현명해보입니다.
성능이 정말 중요한 상황이거나, 사용자의 요청이 동시다발적으로 들어오는 상황이라면 비동기/논블로킹이 좋을 수 있겠습니다. 하지만 이게 절대적으로 좋은 건 아닙니다! 전형적인 동기/블로킹 방식의 프로그램이 훨씬 더 나은 경우도 있습니다!
이와 관련한 글을 하나 추천드립니다!
[배민스토어] 우리만의 자유로운 WebFlux Practices
📍 정리
요약
- 동기 <-> 비동기: 작업의 완료 여부를 기다리느냐 안 기다리느냐
- 블로킹 <-> 논블로킹: 제어권을 소유 여부에 따라 block 되느냐 아니냐
- 동기/비동기 <-> 블로킹/논블로킹: 코드 실행 흐름을 보느냐, 스레드 상태를 보느냐
- 동기/비동기와 블로킹/논블로킹의 조합: 비동기/논블로킹이 무조건적으로 좋은 건 아니다!
레퍼런스
다음 읽을 글로 'Node.js에서 readFileSync를 쓰면 안 되는 이유'를 추천합니다!
Node.js에서 readFileSync를 쓰면 안 되는 이유 (feat. Event Loop)
안녕하세요! NewCodes입니다! Node.js에서`readFileSync`를 써보신 적이 있으신가요? `readFileSync`는 특별한 경우를 제외하고써서는 안 되는 함수입니다. `readFileSync`를 쓰면주방에 있는 셰프가 멈추는 것과
newcodes.tistory.com
동기 함수인 readFileSync를 사용했다가,
main thread가 block되어
성능을 놓치게 된 케이스에 대해서 짚어낸 글입니다.
이를 통해 동기 비동기 블로킹 논블로킹에 대한
이해도를 더욱 높일 수 있으실 겁니다!
이상 읽어주셔서 감사합니다!

'Computer Science > Operating System' 카테고리의 다른 글
1초에 80,000번 Network I/O를 하면 생기는 병목을 분석해보자! (0) | 2025.01.15 |
---|