본문으로 건너뛰기
김신건의 로그

Blocking (블로킹)

· 수정 · 📖 약 3분 · 1,077자/단어 #concurrency #programming-model #blocking-nonblocking #thread
blocking, block, 블로킹, 블락킹, blocking call, blocking I/O

정의

Blocking 은 호출이 결과가 준비될 때까지 호출 스레드를 멈춰두는 실행 방식. 멈춰 있는 동안 그 스레드는 다른 일을 할 수 없다. OS 가 스레드를 sleep 상태로 만들고 CPU 를 다른 스레드에게 넘겨준다 (대부분의 경우).

Non-Blocking 의 반대 개념. Synchronous 와 자주 같이 쓰이지만 별도의 차원.

시각화

Thread T 가 blocking I/O 를 호출하면 BLOCKED 상태로 들어가 CPU 를 양보한다. OS scheduler 는 그 사이 Thread U 에게 CPU 를 돌리고, I/O 가 끝나면 T 를 깨워 다시 CPU 를 할당한다. 호출자의 코드 흐름에서는 “한 줄짜리 sync 호출” 로 보이지만, 스레드 입장에서는 깊은 잠에 든 시간이 존재한다.

같은 흐름을 텍스트로 요약하면 다음과 같다.

Thread T:  [────── blocking call ──────][다음 작업]
           │                            │
           │ ← CPU 다른 스레드에 양보     │
           │   (block 동안 idle)         │

           작업 결과 도착, 깨어남

T 스레드는 결과가 도착할 때까지 어떤 코드도 실행하지 않는다. CPU 는 다른 스레드를 처리한다.

가장 흔한 예

파일 I/O (blocking)

String content = Files.readString(Path.of("data.txt"));
// 파일을 다 읽을 때까지 스레드 block

네트워크 I/O (blocking)

ServerSocket ss = new ServerSocket(8080);
Socket client = ss.accept();              // 연결이 올 때까지 block
String line = reader.readLine();          // 데이터 한 줄 올 때까지 block

락 대기 (blocking)

synchronized (obj) {                      // 다른 스레드가 락을 갖고 있으면 block
    ...
}

lock.lock();                              // ReentrantLock, 락을 얻을 때까지 block

잠 자기 (blocking)

Thread.sleep(1000);                       // 1초 block
TimeUnit.SECONDS.sleep(1);                // 같음

BlockingQueue (blocking)

BlockingQueue<Task> q = new LinkedBlockingQueue<>();
Task t = q.take();                        // 원소가 들어올 때까지 block
q.put(task);                              // 큐가 가득 차면 block

이름에 “Blocking” 이 명시적으로 들어있다.

blocking 의 코드 특징

  • 호출 후 즉시 결과 처리 가능, return 값을 바로 쓸 수 있다
  • stack trace 가 일직선, 디버깅이 쉽다
  • 에러는 그 자리에서 throw, try/catch 가 자연스럽다
try {
    String result = blockingCall();   // sync + blocking
    process(result);                  // 결과 즉시 처리
} catch (IOException e) {
    handle(e);                        // 같은 메서드에서 catch
}

blocking 의 비용

스레드가 block 됐다고 CPU 가 노는 건 아니다. OS 가 다른 스레드를 스케줄링한다. 하지만:

1. 스레드 자체가 비싸다

  • JVM 의 platform thread 는 OS 스레드와 1:1
  • 스레드 1개당 메모리 ~1MB (스택)
  • 100,000 스레드 → ~100GB RAM, 비현실적

따라서 blocking 코드 + 많은 동시 요청 은 스레드 풀로 제한해야 한다. 풀이 꽉 차면 요청들이 큐에서 기다린다.

2. context switch 가 잦다

스레드가 block 되면 OS scheduler 가 다른 스레드로 전환. 이 비용은 마이크로초 수준이지만 누적되면 무시 못 한다.

3. 응답성 손실

UI 스레드가 block 되면 화면이 멈춘다. Android 의 NetworkOnMainThreadException, Swing 의 EDT (Event Dispatch Thread) 보호가 모두 이 문제를 막기 위한 것.

blocking + Synchronous 가 가장 흔하다

String r = http.get(url);          // sync + blocking
  • sync: 호출자가 결과를 기다린다
  • blocking: 스레드가 멈춘다

대부분의 전통적 Java 코드 (HttpURLConnection, JDBC, JPA, file I/O) 가 이 모델.

blocking + Asynchronous 도 가능

new Thread(() -> {
    String r = http.get(url);         // 별도 스레드에서 blocking
    callback.accept(r);
}).start();

호출자는 즉시 풀려나지만, 새 스레드가 block. 자주 보는 안티패턴.

blocking 의 대안, NIO 와 비동기 I/O

전통적 Java I/O (java.io.*) 는 모두 blocking. Java NIO (java.nio.\*) 는 non-blocking I/O 를 지원한다.

// NIO non-blocking
ServerSocketChannel channel = ServerSocketChannel.open();
channel.configureBlocking(false);
SocketChannel client = channel.accept();   // null 즉시 반환 (연결 없으면)

이 모델로 Netty, Vert.x, Spring WebFlux 같은 high-throughput 서버가 가능.

자세히는 Non-Blocking 참고.

Virtual Threads, blocking 의 의미가 바뀌다

Java 21+ 의 Virtual Thread (JEP 444) 는 blocking 코드를 저비용으로 만든다.

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    executor.submit(() -> {
        String r = http.get(url);   // blocking call, 그러나 virtual thread 라 OK
        process(r);
    });
}

Virtual thread 는:

  • 스택을 힙에 저장 (KB 수준)
  • block 되면 carrier thread 에서 unmount → carrier 는 다른 virtual thread 처리
  • 결과 도착 시 carrier 에 다시 mount

수백만 동시 연결을 blocking sync 코드로 처리 가능. 사실상 “blocking 의 비용” 을 거의 0 으로 만든다.

blocking 의 장단점 요약

장점

  • 코드가 직관적, 위→아래
  • 디버깅, stack trace 일관
  • 에러 처리 자연스러움
  • 기존 코드 재사용 쉬움 (대부분의 JDK / 라이브러리가 blocking)

단점

  • 스레드 자원 소모 (platform thread 기준)
  • 동시 요청 수 한계
  • UI 스레드를 block 하면 응답성 손실

언제 blocking 이 적절한가

  • 순차 흐름이 본질 (DB 트랜잭션 안 여러 쿼리 순차)
  • CPU 바운드 작업 (계산 위주, 어차피 스레드가 일함)
  • 동시 요청 수가 적당 (수백 ~ 수천)
  • Virtual Thread 사용 환경 (Java 21+)
  • 단순함이 우선 (스크립트, 마이그레이션, 배치)

언제 Non-Blocking 이 더 나은가

  • 수만 ~ 수십만 동시 연결
  • I/O 가 매우 느리고 가변적 (외부 API 등)
  • 백프레셔 제어 필요 (스트림 처리)
  • 함수형 / reactive 패러다임 채택

참고

이 글의 용어 (5개)
[Java] ReentrantLockjava
정의 는 키워드와 같은 상호 배제 (mutual exclusion) 를 제공하는 클래스 기반 락. JSR-166 (Java 5) 에서 추가됐다. "재진입 (reentrant)" …
[Java] volatilejava
정의 은 Java 의 키워드. 필드에 붙이면 두 가지를 보장한다. 1. 가시성 (visibility): 한 스레드의 쓰기가 다른 모든 스레드에 즉시 보인다. CPU 캐시에 머무르…
Asynchronous (비동기)concurrency
정의 Asynchronous (비동기) 는 호출자가 결과를 즉시 기다리지 않고 다음 줄로 진행하는 실행 모델. 결과는 나중에 콜백, Future, Promise, 이벤트 등의 메…
Non-Blocking (논블로킹)concurrency
정의 Non-Blocking 은 호출이 결과를 기다리지 않고 즉시 반환 하는 실행 방식. 결과가 준비되지 않았다면 "아직 안 됐다" 라는 신호 ( , , , 등) 를 반환하고, …
Synchronous (동기)concurrency
정의 Synchronous (동기) 는 호출자가 결과가 나올 때까지 기다리는 실행 모델. 함수를 호출한 코드는 그 함수가 반환할 때까지 다음 줄로 진행하지 않는다. 이 정의는 J…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기