Java 기반 동기/비동기, 블로킹/논블로킹 정리

2026. 3. 23. 00:05·기술 학습

서로 다른 관심사

동기/비동기와 블로킹/논블로킹은 흔히 혼용되지만, 실제로는 서로 다른 관심사를 다루는 별개의 축이다.
이 구분을 명확히 하는것이 모든 논의의 바탕이 되므로 명확히 짚어보자.

 

동기(Synchronous) vs 비동기(Asynchronous) 

작업 완료를 누가 확인하는가?

  • 동기(Synchronous)
    • 호출자(Caller)가 직접 작업의 완료 여부를 확인하거나 기다린다. 결과를 호출자가 내놓으라고 끌어오는 Pull 방식이다.
  • 비동기(Asynchronous)
    • 피호출자(Callee)가 작업이 완료되면 호출자에게 알려준다. 콜백(callback), 이벤트(event) 등을 통해
      결과를 내보내는 Push 방식이다.

즉, 동기/비동기의 핵심은 완료 통지의 주체가 누구인가이다.

블로킹 (Blocking) vs 논블로킹 (Non-Blocking)

제어권이 즉시 반환되는가?

  • 블로킹 (Blocking)
    • 호출된 함수가 작업을 완료할 때까지 호출자에게 제어권을 돌려주지 않는다. 호출자의 스레드는 그자리에서 멈추고 대기한다.
  • 논블로킹 (Non-Blocking)
    • 호출된 함수가 즉시 제어권을 반환한다. 작업이 완료되지 않았더라도 호출자는 다음 코드를 실행할 수 있다.

즉, 블로킹/논블로킹의 핵심은 호출자의 스레드가 멈추느냐 아니냐이다.


4가지 조합 - Java 코드 예시

  블로킹(Blocking) 논블로킹(Non-Blocking)
동기(Sync) ① Sync + Blocking ② Sync + Non-Blocking
비동기(Async) ③ Async + Blocking ④ Async + Non-Blocking

 

1. 동기 + 블로킹

  • 호출자가 결과를 직접 확인하고 (동기), 결과가 올 때까지 스레드가 멈춤. (블로킹)
  • 일반적인 Java 개발에서 볼 수 있는 형태이다. 대부분의 I/O 호출이 여기에 해당한다.

예시

1)
// JDBC 호출 — 전형적인 동기 + 블로킹
String name = jdbcTemplate.queryForObject(
    "SELECT name FROM users WHERE id = ?",
    String.class, userId
);
// DB가 결과를 반환할 때까지 이 스레드는 여기서 멈춘다.
System.out.println(name);

2)
// RestTemplate — 동기 + 블로킹 HTTP 호출
RestTemplate restTemplate = new RestTemplate();
String result = restTemplate.getForObject(
    "https://api.example.com/data", String.class
);
// 응답이 올 때까지 스레드가 블로킹된다.

3)
// CompletableFuture.get() — 비동기로 시작했지만, get()에서 동기 + 블로킹이 된다
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> heavyWork());
String result = future.get(); // 호출자가 직접 결과를 가져오고(동기), 완료까지 멈춘다(블로킹)

 

특징

  • 코드가 순차적이므로 가독성이 높고 디버깅이 쉽다.
  • 요청 하나당 스레드 하나가 점유되므로, 동시 요청이 많아지면 스레드 풀이 고갈될 수 있다.
  • Spring MVC + JDBC 기반의 전통적인 서버 아키텍처가 이 모델이다.

적합한 상황

  • 요청량이 예측 가능하고, 개발 생산성과 코드 유지보수성을 우선시하는 경우에 적합하다.

2. 동기 + 논블로킹 (Synchronous + Non-Blocking)

  • 호출자가 직접 결과를 확인하고 (동기), 제어권은 즉시 반환됨. (논블로킹)
  • 호출자는 반환된 제어권으로 다른일도 하고 주기적으로 응답 완료 여부를 확인 (Polling) 한다.

예시

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> heavyWork());

// isDone()은 즉시 반환된다 (논블로킹)
// 호출자가 직접 완료 여부를 반복 확인한다 (동기 — polling)
while (!future.isDone()) {
    doSomethingElse(); // 제어권이 있으므로 다른 작업 가능
}
String result = future.get(); // 이미 완료되었으므로 즉시 반환

 

특징

  • 호출자가 주기적으로 확인해야 하므로 CPU 타임(싸이클)을 소모한다. (Busy Wating 방식)
  • 호출자가 완료를 직접 확인하는 만큼 호출자의 흐름안에서 결과를 처리할 수 있다.
  • 실무에서 실제 사용하는 경우보다는 비동기-논블로킹을 가기 위한 단계로 볼 수 있다.

의문점 - 제어권이 호출자에게 반환되었는데, 피호출자의 작업은 어떻게 진행될 수 있을까?

더보기

바로 스레드가 분리되기 때문이다.

 

supplyAsync()는 기본적으로 ForkJoinPool.commonPool()의 워커 스레드에서 작업을 실행한다.

호출자의 스레드와 작업 스레드는 서로 다르기 때문에, 호출자가 제어권을 돌려받아도 작업은 다른 스레드에서 동시에 진행된다.

 

System.out.println("Caller: " + Thread.currentThread().getName());
// 출력: Caller: main

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    System.out.println("Worker: " + Thread.currentThread().getName());
    // 출력: Worker: ForkJoinPool.commonPool-worker-1
    return heavyWork();
});

3. 비동기 + 블로킹 (Asynchronous + Blocking)

  • 피호출자가 작업완료를 알려주고 (비동기), 제어권은 반환되지 않아 호출자의 스레드가 멈춰있다. (블로킹)
  • 의도적인 설계보다는 비동기로 전환하던 중 블로킹 지점이 일부 남아있을 때 자연스럽게 발생하는 경우이다. (webflux + JDBC)

예시 1 (Spring WebFlux + JDBC Blocking)

Mono.fromCallable(() -> {
        // 리액티브 파이프라인 안에서 블로킹 JDBC 호출이 발생
        return jdbcTemplate.queryForObject(
            "SELECT name FROM users WHERE id = ?",
            String.class, userId
        );
    })
    .subscribeOn(Schedulers.boundedElastic())
    .subscribe(name -> sendResponse(name)); // 콜백으로 결과 수신 (비동기)
  • 위 코드를 분석해보면
    • 비동기인가? -> subscribe에 콜백을 등록하고, 파이프라인이 완료되면 알려주는 push 방식이다.
      호출자가 결과를 직접 끌어오지 않으므로 비동기다.
    • 블로킹인가? -> 파이프라인 내부에서 jdbcTemplate 호출이 실행되는 순간, 그 작업을 실행하는 스레드는 DB 응답이 올 때까지 멈춘다. JDBC 자체가 블로킹 API이기 때문이다.
      호출자가 멈추므로 블로킹이다.

예시 2 (WebClient + JDBC)

// 비동기 + 논블로킹으로 의도한 코드
WebClient webClient = WebClient.create();

Mono<String> result = webClient.get()
    .uri("https://api.example.com/data")
    .retrieve()
    .bodyToMono(String.class)
    .flatMap(data -> {
        // ⚠️ 여기서 블로킹 JDBC 호출이 발생!
        String saved = jdbcTemplate.queryForObject(
            "INSERT INTO logs(data) VALUES(?) RETURNING id",
            String.class, data
        );
        return Mono.just(saved);
    });

result.subscribe(id -> log.info("저장 완료: {}", id));
  • 위 코드를 분석해보면
    • 1. 요청 수신 → Netty 이벤트 루프 스레드(reactor-http-nio-1)가 처리 시작
    • 2. webClient.get()...bodyToMono()
      • 외부 API에 HTTP 요청 전송 (A)
      • 응답을 기다리는 동안 이벤트 루프 스레드는 해방됨
      • 다른 요청(B, C, D...)를 처리할 수 있다.
    • 3. 외부 API 응답 도착 (A)
      • 이벤트 루프 스레드가 깨어나서 flatMap 진입
    • 4. flatMap 내에서 jdbc.queryForObject() 호출 (블로킹!)
      • JDBC 블로킹 API - DB가 응답할 때까지 현재 스레드가 멈춘다!
      • 이 스레드는 Netty 이벤트 루프 스레드 (중요, Netty는 CPU 코어수만큼의 스레드로만 처리함)
      • DB 응답을 대기하는 수십~수백 ms 동안 이 루프에 할당된 모든 요청이 멈춘다.

특징

  • 비동기 방식을 도입했으나 병목 지점에서 블로킹이 발생하여 비동기의 장점이 상쇄된다.
  • 시스템 전체를 논블로킹으로 전환하지 않으면 제대로된 리액티브가 되지 못한다.

R2DBC의 등장 배경

더보기

즉, WebFlux를 도입하면 HTTP 요청 처리는 비동기 + 논블로킹이 된다.

하지만 DB 접근 계층에서 JDBC를 사용하면, 결국 그 지점에서 스레드가 블로킹된다.

비동기의 핵심 장점인 "적은 스레드로 높은 처리량"을 온전히 누릴 수 없는 것이다.

 

이 문제를 해결하기 위해 등장한 것이 R2DBC(Reactive Relational Database Connectivity)다.
R2DBC는 관계형 데이터베이스에 대한 완전한 논블로킹 API를 제공하여, WebFlux와 결합했을 때 요청 처리 전 구간에서
비동기 + 논블로킹을 달성할 수 있게 해준다.

Netty의 이벤트 루프 스레드 방식

더보기

Netty는 기본적으로 CPU 코어 수만큼의 이벤트 루프 스레드로 전체 요청을 처리한다.

예를 들어 4코어 서버에서는 reactor-http-nio-1 ~ reactor-http-nio-4, 총 4개의 스레드가 모든 동시 요청을 나눠 처리하는 구조이다.

 

이 중 하나의 스레드가 JDBC 호출로 100ms만 블로킹되어도, 그 스레드에 할당된 모든 커넥션의 요청처리가 100ms동안 완전히 멈추게 되는것이다. 4개중 한개의 스레드가 막히면 전체 처리량의 25%가 줄어드는 셈이된다.

4. 비동기 + 논블로킹 (Asynchronous + Non-Blocking)

  • 작업 완료는 피호출자가 알려주고 (비동기), 호출자의 스레드도 즉시 해방된다. (논블로킹)
  • 가장 효율적인 방식이다. 호출자는 요청만 보내고 즉시 다른일을 할 수 있고, 피호출자의 작업이 완료되면 콜백이나 이벤트를 통해 
    확인하게 된다.

예시

// 1. CompletableFuture — thenAccept()는 콜백을 등록하고 즉시 반환된다
CompletableFuture
    .supplyAsync(() -> callExternalApi())    // 별도 스레드에서 실행 (논블로킹)
    .thenApply(response -> parse(response))  // 완료 시 자동으로 체이닝
    .thenAccept(result -> save(result));      // 피호출자가 완료를 알려줌 (비동기)
System.out.println("요청 전송 완료 — 결과를 기다리지 않고 다음 로직 실행");

// 2. Spring WebFlux — 완전한 비동기 + 논블로킹 (WebFlux + R2DBC)
@GetMapping("/users/{id}")
public Mono<User> getUser(@PathVariable Long id) {
    return userRepository.findById(id);  // R2DBC — 논블로킹 DB 호출
    // 반환 즉시 스레드 해방, 결과가 준비되면 Reactor가 응답을 push
}

// 3. Java 11+ HttpClient — 비동기 + 논블로킹 HTTP 호출
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.example.com/data"))
    .build();

client
    .sendAsync(request, HttpResponse.BodyHandlers.ofString())
    .thenAccept(response -> {
        System.out.println("Status: " + response.statusCode());
    });
// sendAsync()는 즉시 반환되고, 응답이 오면 thenAccept()로 콜백이 실행된다

 

특징

  • 적은 수의 스레드로 많은 요청을 처리할 수 있다. 스레드가 I/O 대기로 낭비되지 않는다.
  • 코드 복잡도가 올라간다. 콜백 체이닝, 에러 전파, 디버깅이 동기식 코드보다 어려워진다.

마무리 정리

4가지 조합 간략 요약

  완료 확인 제어권 반환 예시 스레드 효율
동기 + 블로킹 호출자가 직접 확인 완료까지 미반환 JDBC, RestTemplate, future.get() 낮음 (스레드 점유)
동기 + 논블로킹 호출자가 직접 확인 (polling) 즉시 반환 future.isDone() While 루프 보통 (busy-wait)
비동기 + 블로킹 피호출자가 알려줌 중간에 블로킹 발생 WebFlux 요청 내 JDBC (블로킹) 낮음 (블로킹 병목)
비동기 + 논블로킹 피호출자가 알려줌 즉시 반환 thenAccept(), WebFlux + R2DBC 높음

헷갈리는 내용 정리

한줄 정리

  • 동기/비동기는 - 완료를 누가 알려주는가!
  • 블로킹/논블로킹은 - 호출자 스레드를 멈추는가!

CompletableFuture를 사용하면 항상 비동기 + 논블로킹인가?

-> No! CompletableFuture를 어떻게 사용하냐에 따라 동기/비동기 블로킹/논블로킹이 다르다.

  • future.get(): 결과 내놔! Polling 이므로 동기이다. get()으로 응답 내놔 하는동안 호출자 스레드가 블로킹된다.
  • while(future.isDone().not()): 결과 확인! Polling 이므로 동기이다. 단, while 내에서 다른 작업을 할 수 있는 논블로킹이다.
  • future.thenAccept(callback): 결과를 확인하지 않고 thenAccept로 결과를 콜백받으므로 비동기이다. 그동안 다른 작업을 할 수 있으므로 논블로킹이다.

 

반응형
저작자표시 비영리 변경금지 (새창열림)

'기술 학습' 카테고리의 다른 글

MSA에서 CORS 문제를 해결하는 4가지 전략  (0) 2026.03.27
TCP/IP 체크섬(Checksum) 내부 동작 원리  (0) 2026.03.23
ANSI Isolation Level vs MySQL Isolation Level: 같은 이름, 다른 보장  (0) 2026.03.15
MSA에서 ACID의 의미  (0) 2026.03.10
객체지향 설계 원칙 - SOLID  (0) 2026.03.08
'기술 학습' 카테고리의 다른 글
  • MSA에서 CORS 문제를 해결하는 4가지 전략
  • TCP/IP 체크섬(Checksum) 내부 동작 원리
  • ANSI Isolation Level vs MySQL Isolation Level: 같은 이름, 다른 보장
  • MSA에서 ACID의 의미
구름뭉치
구름뭉치
구름의 개발일기장
    반응형
  • 구름뭉치
    구름 개발일기장
    구름뭉치
  • 전체
    오늘
    어제
    • ALL (290)
      • 프로젝트 (23)
        • 토스페이먼츠 PG 연동 시리즈 (12)
        • JWT 방식 인증&인가 시리즈 (6)
        • 스우미 웹 애플리케이션 프로젝트 (1)
        • 스프링부트 기본 보일러 플레이트 구축 시리즈 (2)
        • 마이크로서비스 아키텍쳐 시리즈 (1)
      • 스프링 (43)
        • 스프링부트 API 설계 정리 (8)
        • 스프링부트 RestAPI 프로젝트 (18)
        • 스프링부트 WebSocket 적용기 (3)
        • 스프링 JPA 정리 시리즈 (5)
        • 스프링 MVC (5)
        • 스프링 배치 (2)
        • 토비의 스프링 정리 (2)
      • 기술 학습 (6)
        • 아파치 카프카 (9)
        • 클린 코드 (4)
        • 디자인 패턴의 아름다움 (2)
        • 모던 자바 인 액션 (7)
        • JVM 스레드 딥다이브 (7)
      • Web (25)
        • 정리글 (20)
        • GraphQL 정리글 (2)
        • Jenkins 정리글 (3)
      • 취업 (6)
      • CS (77)
        • 네트워크 전공 수업 정리 (11)
        • OSI 7계층 정리 (12)
        • 운영체제 정리 (19)
        • 데이터베이스 정리 (5)
        • MySql 정리 (17)
        • GoF의 Design Pattern 정리 (12)
      • 알고리즘 (70)
        • 백준 (56)
        • 프로그래머스 (12)
        • 알고리즘 정리본 (1)
      • 기초 지식 정리 (2)
      • 일상 (8)
  • 블로그 메뉴

    • 홈
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    마우스 패드
    mx master s3 for mac
    키보드 손목 받침대
    레이저
    동유럽
    마우스
    크로아티아
    부다페스트
    류블라냐
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
구름뭉치
Java 기반 동기/비동기, 블로킹/논블로킹 정리
상단으로

티스토리툴바