혼동하기 쉬운 블록킹/논블록킹과 동기/비동기 개념을 정리합니다.

개념 정의

블록킹 vs 논블록킹

구분 기준: 제어권을 바로 돌려주는가?

  • 블록킹: 작업 완료까지 제어권을 돌려주지 않음
  • 논블록킹: 작업 요청 후 바로 제어권 반환

카페 주문 비유:

  • 블록킹: 카운터에서 서서 기다림 (다른 일 못함)
  • 논블록킹: 진동벨 받고 자리로 (다른 일 가능)

동기 vs 비동기

구분 기준: 결과를 누가 확인하는가?

  • 동기: 요청한 쪽이 결과를 직접 확인
  • 비동기: 결과가 준비되면 콜백으로 알려줌

카페 주문 비유:

  • 동기: 내가 직접 “제 커피 됐나요?” 확인
  • 비동기: 직원이 “OO번 고객님~” 불러줌

핵심 구분

구분 질문 블록킹 논블록킹
제어권 기다리는 동안 다른 일 할 수 있어? 못함 (멈춤) 가능 (안 멈춤)
구분 질문 동기 비동기
결과 확인 결과를 내가 확인해? 알려줘? 내가 확인 알려줌

C 디바이스 I/O 예제

블록킹 I/O

int fd = open("/dev/device", O_RDONLY);
char buf[1024];

// 데이터 올 때까지 멈춤
int result = read(fd, buf, 1024);

// 여기 도달 = buf 채워짐 보장
printf("읽음: %s", buf);

동작:

  1. 프로세스가 커널에 read() 요청
  2. 데이터 없으면 프로세스 sleep
  3. 데이터 도착하면 커널이 프로세스 깨움
  4. 그제서야 다음 줄 실행

논블록킹 I/O + Polling

int fd = open("/dev/device", O_RDONLY | O_NONBLOCK);
char buf[1024];

while (1) {
    int result = read(fd, buf, 1024);  // 바로 리턴
    
    if (result > 0) {
        printf("읽음: %s", buf);
        break;
    } else if (result == -1 && errno == EAGAIN) {
        // 데이터 없음 → 다른 일 하고 다시 확인
        doOtherWork();
    }
}

동작:

  1. 프로세스가 커널에 read() 요청
  2. 데이터 없으면 즉시 -1 리턴
  3. 프로세스는 계속 실행
  4. 반복해서 확인 (polling)

논블록킹 I/O + select()

int fd = open("/dev/device", O_RDONLY | O_NONBLOCK);
fd_set readfds;

while (1) {
    FD_ZERO(&readfds);
    FD_SET(fd, &readfds);
    
    // 타임아웃 100ms
    struct timeval timeout = {0, 100000};
    
    int ready = select(fd + 1, &readfds, NULL, NULL, &timeout);
    
    if (ready > 0) {
        read(fd, buf, 1024);
        break;
    } else if (ready == 0) {
        doOtherWork();
    }
}

동작:

  1. select()로 데이터 준비 여부를 커널에 위임
  2. 효율적인 대기 - CPU가 계속 확인하는 대신 커널이 깨워줄 때까지 대기
  3. 타임아웃 시 다른 작업 가능

Java Spring 예제

동기 + 블록킹 (RestTemplate)

@GetMapping("/user")
public User getUser() {
    // 현재 스레드가 여기서 멈춤
    User user = restTemplate.getForObject(url, User.class);
    return user;
}

비동기 + 논블록킹 (WebClient)

@GetMapping("/user")
public Mono getUser() {
    // 요청만 하고 바로 반환
    return webClient.get()
        .uri(url)
        .retrieve()
        .bodyToMono(User.class);
}

I/O 방식별 비교

방식 대기 효율 사용 환경
블록킹 스레드 멈춤 단순함 일반 애플리케이션
논블록킹 + Polling 안 멈춤 (반복 체크) CPU 낭비 임베디드
논블록킹 + select/epoll 안 멈춤 (이벤트 대기) 높음 서버

조합별 특징

조합 빈도 예시
동기 + 블록킹 ✅ 흔함 JDBC, RestTemplate
비동기 + 논블록킹 ✅ 흔함 WebFlux, Node.js
동기 + 논블록킹 드묾 polling
비동기 + 블록킹 드묾 안티패턴

정리

  • 블록킹/논블록킹: 제어권 반환 시점
  • 동기/비동기: 결과 확인 주체
  • 대부분 동기+블록킹 또는 비동기+논블록킹 조합 사용
  • I/O 방식 선택은 성능과 복잡도의 트레이드오프

참고 비동기 + 블록킹 안티패턴

@GetMapping("/user")
public User getUser() {
    // 비동기로 시작했지만...
    CompletableFuture future = CompletableFuture.supplyAsync(() -> {
        return userService.findUser();
    });
    
    // get()으로 블록킹하면 비동기의 의미 없음
    return future.get();  // 여기서 블록킹!
}

비동기로 작업을 시작했지만 결과를 기다리느라 블록킹되어 비동기의 장점이 사라짐.