두 개념은 호출하는 쪽(call site)이 결과를 기다리느냐(동기) 아니면 기다리지 않느냐(비동기)를 구분
동기(Synchronous)
– 호출한 메서드가 작업 완료(또는 오류) → 결과 반환 전까지 호출 스레드가 대기
– 장점: 제어 흐름이 단순하고 디버깅하기 쉬움
– 단점: 긴 I/O나 연산 중 호출 스레드가 묶여 있어 전체 처리량이 떨어짐
비동기(Asynchronous)
– 호출한 메서드가 즉시 반환 → 별도 콜백, Future/Promise, Reactive 스트림 등을 통해 나중에 결과 수신
– 장점: 호출 스레드는 다른 작업 계속 수행 → 자원 활용 효율이 높음
– 단점: 콜백 헬, 흐름 제어 복잡도 증가
블로킹/논블로킹은 스레드 관점에서 자원이 사용 가능해질 때까지 대기하느냐를 말합
블로킹(Blocking I/O)
– I/O 함수 호출 시 데이터 준비가 안 돼 있으면 호출 스레드가 OS 레벨에서 멈춰 있다가(park) 준비되면 깨어나 처리
– 예: java.io.InputStream.read()
논블로킹(Non-Blocking I/O)
– I/O 함수가 즉시 반환 → 데이터가 준비되지 않았으면 즉시 0바이트나 EAGAIN 같은 상태를 알려줌
– 호출 스레드는 깨어 있고 다른 작업을 수행하거나 재시도 로직을 구현해야 함
– 예: Java NIO 채널(Channel).read(), select()/poll() 기반 구조
printAfterSeconds("Task A", 3000); // 3초 대기
printAfterSeconds("Task B", 2000); // Task A가 끝난 후 2초 대기
printAfterSeconds("Task C", 1000); // Task B가 끝난 후 1초 대기
Future<String> task = es.submit(() ->
returnValueAfterSeconds("Task", 10000));
while(!task.isDone()) {
// 다른 작업 수행
}
Future<String> task = es.submit(() ->
returnValueAfterSeconds("Task", 10000));
String result = task.get(); // 결과가 나올 때까지 블로킹
fs.readFile('/file.md', (err, data) => {
if (err) throw err;
console.log(data); // 파일 읽기 끝난 후 콜백 실행
});
console.log('다른 작업'); // 파일 읽는 동안에도 실행됨

블로킹 비용은 단순히 "시간"만 의미하지 않습니다.
CPU 관점: 블로킹 중인 스레드는 실제로 CPU를 점유하지 않지만, OS는 해당 스레드를 관리해야 하며, 많은 스레드가 블로킹되면 스케줄링·컨텍스트 스위칭 등 부가적인 컴퓨팅 자원이 소모됩니다.
메모리 관점: 블로킹 스레드마다 스택·상태를 보관해야 하므로, 많은 동시 요청이 들어오면 메모리 사용량이 급증합니다.
확장성: 블로킹 방식은 동시 접속이 많아질수록 서버 자원이 빠르게 고갈됩니다.
즉, 블로킹 비용은 단순 대기 시간뿐 아니라, 시스템 자원(메모리, 스레드 관리, 스케줄링 등) 전체에 영향을 미치는 "컴퓨팅 파워"의 낭비를 의미합니다.
콜백과 이벤트 루프는 비동기 논블로킹의 핵심 메커니즘입니다.
작업 요청 → 즉시 반환 → 완료 시 콜백 등록 → 이벤트 루프가 콜백 실행
이 과정에서 스레드가 대기하지 않으므로, 자원을 효율적으로 활용하고 높은 동시 처리가 가능합니다.
Spring 생태계에서 ‘반응형 스택(reactive stack)’은 Non-Blocking I/O와 Back-Pressure를 지원하는 라이브러리들을 모아, 비동기·이벤트 기반으로 데이터를 처리하는 기술 집합
Spring WebFlux: Spring 5.0부터 도입된 완전 Non-Blocking 웹 프레임워크로, Netty·Undertow·Servlet 3.1+ 같은 서버에서 구동되며 Reactive Streams Back-Pressure를 내장 지원
구독 지연(Laziness): Flux/Mono는 subscribe() 호출 전까지 어떤 연산도 수행하지 않습니다.
데이터 흐름: subscribe() 시 Subscription이 생성되고, 구독자가 request(n)로 소비량을 알리면 Publisher가 onNext()로 데이터를 흘려 보냅니다.
연산자(Operator): map, filter, flatMap 같은 연산자를 연결해 선언적으로 파이프라인을 정의합니다. 이들은 데이터 스트림 중간에서 값을 변환·조합·제어합니다.
종료 신호: 데이터 방출이 끝나면 onComplete(), 오류 발생 시 onError()로 스트림이 종료됩니다.
lux<Integer> flux = Flux.range(1, 5) // 1부터 5까지 발행
.filter(i -> i % 2 == 0) // 짝수 필터링
.map(i -> i * 10); // 값 변환
flux.subscribe(
data -> System.out.println(data), // onNext
err -> System.err.println(err), // onError
() -> System.out.println("완료"), // onComplete
sub -> sub.request(2) // 최초 2개 요청(백프레셔)
);
HTTP 프로토콜과 동시성
HTTP/1.1에서는 하나의 TCP 연결에서 한 번에 하나의 요청-응답만 처리할 수 있습니다. 여러 요청을 동시에 처리하려면 여러 TCP 연결이 필요합니다. 브라우저는 이를 위해 여러 연결을 병렬로 사용합니다.
HTTP/2에서는 하나의 연결에서 여러 요청을 동시에 스트림(stream) 단위로 처리하는 멀티플렉싱이 지원되어, 한 서버가 동시에 여러 요청을 처리할 수 있습니다.