📍 배경
2024.10.15 - [CS] - 동기vs비동기 , 블로킹vs논블로킹
예전에 포스팅한 CS 지식을 체화하기 위해 동기vs 비동기 , 블로킹 vs 논블로킹 실습 코드를 작성을 해보았습니다.
현재 제가 진행중인 그룹 스터디에서 주제를 정하고 각자 공부하는걸 발표하는 시간을 가졌었는데요.
저는 이때 실생활에서 볼 수 있는 동기 vs 비동기 , 블로킹 vs 논블로킹 시나리오를 생각하고 이를 java 코드로 구현해 보았습니다.
면접때 질문이 들어와도 곧바로 대답할 수 있는 수준이 되기 위해 쉽게 이해하기 쉬운 시나리오와 코드를 포스팅해보겠습니다
📍 동기 vs 비동기

`동기 vs 비동기` 로 만들어 본 시나리오의 상황은 카페에서 발생할 수 있는 상황들 입니다.
동기는 한 작업이 완료될 때까지 다음 작업은 대기한 후 결과값을 받아 순서대로 처리한다는 특징을 가지고 있죠.
그래서 카페에 4잔의 음료 주문이 들어왔을 때, 카페에 직원이 1명만 있다고 생각하고 주문이 들어온 순서대로 카페 메뉴를 제조한다는 시나리오를 가정했습니다.
비동기는 작업 완료를 기다리지 않고 다음 작업을 진행할 수 있으며, 여러 작업이 동시에 진행되어 완료 순서를 예측할 수 없다는 특징을 가지고 있죠.
여기서는 카페에 직원이 4명이 있다고 각자 음료를 맡아서 제조한다는 시나리오를 가정했습니다.
(⭐️ 여기서 중요 포인트는 직원 4명이라는 병렬실행 보다는' 작업 완료를 기다리지 않아도 된다' 는 점이 핵심입니다. 이 덕분에 여러 작업이 동시에 진행될 수 있는 것이죠!)
📁 동기 실습 코드
public void sync() {
long start = System.currentTimeMillis();
log.info("=== 🔄 동기 버전: 음료를 하나씩 순서대로 만듭니다 ===\n");
log.info("=== 알바생 한명 밖에 없음... ===\n");
cafeStaff.makeAmericano(); // 3초
cafeStaff.makeLatte(); // 4초
cafeStaff.makeAde(); // 2초
cafeStaff.makeFrappuccino(); // 5초
long duration = System.currentTimeMillis() - start;
log.info("\n=== 동기 실행 완료: {}ms (약 {}초) ===\n", duration, duration/1000);
}
동기는 음료를 제조하는 메소드를 호출하는 걸로 간단하게 구현이 가능합니다.
아래에 CafeStaff 클래스의 코드를 작성하였습니다.
public class CafeStaff {
/**
* [블로킹] 음료 만들기 - 3초 소요
* 호출하면 3초 동안 대기했다가 리턴됨
*/
public String makeCoffee() {
log.info("☕️ [직원] 음료 제조 시작...");
try {
Thread.sleep(3000); // 블로킹! 여기서 3초 대기
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
String result = "✅ [직원] 음료 제조 완료";
log.info(result);
return result;
}
/**
* [논블로킹] 음료 만들기 - 즉시 리턴
* 호출하면 바로 리턴되고, 백그라운드에서 3초 동안 작업 진행
*/
public CompletableFuture<String> makeCoffeeAsync() {
log.info("☕️ [직원] 음료 제조 시작... (백그라운드)");
return CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(3000); // 백그라운드에서 3초 작업
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
String result = "✅ [직원] 음료 제조 완료";
log.info(result);
return result;
});
}
/**
* 아메리카노 만들기 - 3초 소요
*/
public String makeAmericano() {
log.info("☕️ [아메리카노] 제조 시작...");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
String result = "✅ [아메리카노] 완성!";
log.info(result);
return result;
}
/**
* 라떼 만들기 - 4초 소요 (우유 스팀 시간 때문에 더 오래 걸림)
*/
public String makeLatte() {
log.info("🥛 [라떼] 제조 시작...");
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
String result = "✅ [라떼] 완성!";
log.info(result);
return result;
}
/**
* 에이드 만들기 - 2초 소요 (간단한 음료)
*/
public String makeAde() {
log.info("🍋 [에이드] 제조 시작...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
String result = "✅ [에이드] 완성!";
log.info(result);
return result;
}
/**
* 프라푸치노 만들기 - 5초 소요 (블렌더 사용으로 가장 오래 걸림)
*/
public String makeFrappuccino() {
log.info("🧊 [프라푸치노] 제조 시작...");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
String result = "✅ [프라푸치노] 완성!";
log.info(result);
return result;
}
}
각각 음료마다 제조 구현을 Thread.sleep으로 시간초 대기를 다르게 걸어놓았습니다.

테스트 코드로 동작한 결과 입니다
📁 비동기 실습 코드
public void async() {
long start = System.currentTimeMillis();
log.info("=== 비동기 버전: 모든 음료를 동시에 만들기 시작! ===\n");
log.info("=== 알바생 4명있음...(1사람당 음료 1개씩 맡음) ===\n");
// 모든 음료를 동시에 만들기 시작
CompletableFuture<String> americano = CompletableFuture.supplyAsync(() ->
cafeStaff.makeAmericano() // 3초
);
CompletableFuture<String> latte = CompletableFuture.supplyAsync(() ->
cafeStaff.makeLatte() // 4초
);
CompletableFuture<String> ade = CompletableFuture.supplyAsync(() ->
cafeStaff.makeAde() // 2초
);
CompletableFuture<String> frappuccino = CompletableFuture.supplyAsync(() ->
cafeStaff.makeFrappuccino() // 5초
);
// 모든 음료가 완료될 때까지 대기
CompletableFuture.allOf(americano, latte, ade, frappuccino).join();
long duration = System.currentTimeMillis() - start;
log.info("\n=== 비동기 실행 완료: {}ms (약 {}초) ===\n", duration, duration/1000);
}
비동기 실습 코드 구현은 CompletableFuture 클래스를 사용해서 구현을 했습니다.
CompletableFuture은 Java 8 에서 도입된 비동기 프로그래밍을 위한 클래스 입니다.
코드와 관련된 추가 설명은 아래와 같습니다.
- `supplyAsync()`: 메서드로 별도 스레드에서 작업을 실행합니다.
- `allOf()`: 모든 작업이 완료될 때까지 대기해주는 메서드 입니다.
- `join()`: 작업 완료까지 블로킹하며 결과를 반환해줍니다.
- CompletableFuture.allOf(americano, latte, ade, frappuccino); 만 작성할 경우 => ⚠️ CompletableFuture 객체만 생성될 뿐 실제로 대기하지 않습니다.
- 결과를 반환받기 위해서는 join(), get()이 필요하며, 백그라운드에서만 실행할 경우 runAsync()를 사용하면 됩니다.

테스트 코드로 동작한 결과 입니다
📍 블로킹 vs 논블로킹

`블로킹 vs 논블로킹` 또한 카페에서 발생할 수 있는 상황들로 시나리오를 가정했습니다.
블로킹은 caller가 callee를 호출 했을 때, callee의 작업이 완료될 때까지 caller의 작업이 대기상태 (Block)이 되는 것이죠.
이때 시나리오의 배경은 카페 직원 2명이 마감을 해야하는 상황인 겁니다. 직원 한 명은 초보고 또 다른 직원은 초보 직원을 교육시키는 매니저라고 합시다. 이때 매니저는 초보 직원이 일을 옆에서 봐줘야하는 상황 때문에 테이블을 닦으러 가지 못합니다. 음료가 나올 때까지 같이 봐주며 대기하고 이 시간 동안 테이블을 닦거나 다른 일을 할 수 없죠.
논블로킹은 caller가 callee를 호출 했을 때, 호출 즉시 리턴되어 callee의 작업 완료를 기다리지 않고 caller의 작업을 바로 이어서 수행 할 수 있습니다.
이때는 초보 직원도 업무에 익숙해져서 혼자서도 음료를 만들 수 있게 되었습니다. 매니저는 초보 직원에게 음료를 제조하라고 한 다음 테이블 닦으러 갈 수 있습니다.
📁 블로킹 실습 코드
public class BlockingNonBlockingCompare {
CafeStaff staff = new CafeStaff();
public void cleanTable() {
log.info("🧹 테이블 닦기");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
public int blocking() {
long start = System.currentTimeMillis();
log.info("=== 🔒 블로킹 방식 ===");
// 음료 만들기가 끝난 후에야 다른 일 가능
long timeLimit = 3000; // 3초 제한
long elapsed = System.currentTimeMillis() - start;
// 음료 만들기 - 블로킹! 3초 대기
staff.makeCoffee(); // 여기서 3초 대기 ⏸️
elapsed = System.currentTimeMillis() - start;
log.info("💭 [블로킹] 음료 만들기가 끝났으니 이제 다른 일을 할 수 있어요.");
int cleanedTableCnt = 0;
while (elapsed < timeLimit) {
cleanTable(); // 0.5초
elapsed = System.currentTimeMillis() - start;
cleanedTableCnt++;
}
log.info("✅ 완료한 작업 수: {}", cleanedTableCnt);
return cleanedTableCnt;
}
cleanTable() 메소드는 0.5초로 Thread.sleep(500)을 걸어놓았습니다.
커피 제조 소요시간이 3초이고 블로킹방식에서는 커피 제조가 끝난 후에야 테이블을 닦을 수 있습니다.
따라서 3초 제한 시간 내에 닦을 수 있는 테이블 수는 0개 입니다.
매니저는 초보 직원이 일을 봐줘야하는 상황 때문에 테이블을 닦으러 가지 못합니다.
📁 논블로킹 실습 코드
public int nonBlocking() {
long start = System.currentTimeMillis();
log.info("=== 🔓 논블로킹 방식 ===");
// 3초 동안 계속 다른 일 진행
long timeLimit = 3000; // 3초 제한
long elapsed = System.currentTimeMillis() - start;
// 음료 만들기 시작 - 논블로킹! 즉시 리턴 ⚡
log.info("☕ [직원] 음료 제조 시작... (백그라운드)");
CompletableFuture<String> makingCoffee = staff.makeCoffeeAsync();
log.info("💭 [논블로킹] 음료는 백그라운드에서 만들어지니 다른 일을 할 수 있어요!");
int cleanedTableCnt = 0;
while (elapsed < timeLimit) {
cleanTable();
cleanedTableCnt++;
elapsed = System.currentTimeMillis() - start;
}
makingCoffee.join();
log.info("✅ 완료한 작업 수: {}", cleanedTableCnt);
return cleanedTableCnt;
}
블로킹코드와 매우 유사합니다. 차이점이 있다면 논블로킹에서도 CompletableFuture 클래스를 사용한다는 점입니다.
public CompletableFuture<String> makeCoffeeAsync() {
log.info("☕️ [직원] 음료 제조 시작... (백그라운드)");
return CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(3000); // 백그라운드에서 3초 작업
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
String result = "✅ [직원] 음료 제조 완료";
log.info(result);
return result;
});
}
백그라운드에서 음료 제조를 수행하도록 makeCoffeeAysnc() 도 CompletableFuture 로 호출하여, 음료가 만들어지는 동안 다른 작업을 수행할 수 있도록 했습니다.
음료 제조는 3초가 걸리는데, 이 시간 동안 매니저는 테이블 닦기 작업을 반복합니다. 테이블 1개를 닦는데 0.5초가 걸리므로 3초 동안 총 6개의 테이블을 닦을 수 있습니다.
매니저는 초보 직원에게 음료를 제조하라고 한 다음 테이블 닦으러 갈 수 있습니다.
음료 제조와 테이블 닦기가 동시에 진행되므로 총 소요 시간은 3초이며, 같은 시간 동안 블로킹 방식보다 훨씬 많은 작업을 완료할 수 있습니다.

📍 배운점
이번 실습을 통해 동기/비동기와 블로킹/논블로킹의 차이를 명확히 이해할 수 있었습니다.
- 동기/비동기: 작업의 완료 시점과 결과 처리 방식의 차이
- 블로킹/논블로킹: 호출 후 제어권 반환 여부의 차이
또한, Java의 CompletableFuture을 처음으로 활용하여 비동기 논블로킹 프로그래밍을 구현할 수 있었습니다.
같은 3초라는 시간 동안 블로킹 방식은 0개, 논블로킹 방식은 6개의 테이블을 닦는 것을 보며 적절한 방식 선택이 얼마나 중요한지 체감할 수 있었습니다.
실제 카페 시나리오로 구현해보니 각 개념이 어떻게 다르게 동작하는지 직관적으로 이해하며 공부할 수 있는 시간이었습니다.
'CS' 카테고리의 다른 글
| MSA 환경에서 서비스별 Swagger UI를 GitHub Pages로 배포하기 (0) | 2025.09.22 |
|---|---|
| 동기vs비동기 , 블로킹vs논블로킹 (3) | 2024.10.15 |
