동기와 비동기는 '작업 완료 여부를 누가 신경쓰는가'의 관점입니다.
블로킹과 논블로킹은 '작업 제어권을 누가 가지고 있는가'의 관점입니다.
동기 블로킹
예시: 은행 창구 대기
- 고객: 창구에서 업무 처리를 요청
- 직원: 업무 처리 시작
- 고객: 창구에서 기다림 (다른 일 못함)
- 직원: 업무 처리 완료
- 고객: 완료 확인 후 귀가
동기 논블로킹
예시: 식당 주문 후 진동벨을 보며 매장에서 쇼핑
- 손님: 주문 후 진동벨 수령
- 손님: 매장에서 쇼핑하면서 주기적으로 진동벨 확인
- 직원: 음식 준비 중
- 손님: 계속 쇼핑하며 벨 확인
- 직원: 음식 완료, 진동벨 울림
- 손님: 직접 확인 후 음식 수령
비동기 블로킹
예시: 택배 배송 직접 기다리기
- 고객: 택배 기사님께 문 앞에서 기다리겠다고 알림
- 고객: 문 앞에서 대기 (다른 일 못함)
- 택배 기사: 도착 시 알려줌
- 고객: 택배 수령
비동기 논블로킹
예시: 택배 배송 알림 서비스
- 고객: 택배 배송 요청
- 고객: 일상적인 활동 수행
- 택배 기사: 배송 완료 후 문자 발송
- 고객: 문자 확인 후 택배 수령
// 파일을 읽을 때까지 대기
try (FileInputStream fis = new FileInputStream("file.txt")) {
byte[] data = new byte[1024];
fis.read(data); // 이 줄에서 블로킹
System.out.println(new String(data));
}
// 파일 읽기 가능 여부 확인하며 다른 작업 수행
FileChannel channel = FileChannel.open(Paths.get("file.txt"));
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (buffer.hasRemaining()) {
int bytesRead = channel.read(buffer); // 즉시 반환
if (bytesRead == 0) {
// 다른 작업 수행
doSomethingElse();
}
}
// CompletableFuture를 사용하지만 get()으로 블로킹
CompletableFuture<String> future = readFileAsync("file.txt");
String content = future.get(); // 이 줄에서 블로킹
System.out.println(content);
// 콜백으로 처리
AsynchronousFileChannel.open(Paths.get("file.txt")).read(
ByteBuffer.allocate(1024), 0,
buffer,
new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer buffer) {
// 파일 읽기 완료 시 실행될 코드
System.out.println(new String(buffer.array()));
}
@Override
public void failed(Throwable exc, ByteBuffer buffer) {
exc.printStackTrace();
}
}
);
// 즉시 다른 작업 수행 가능
doSomethingElse();
public class SynchronousExample {
public String processOrder(String orderNumber) {
// 주문 조회
Order order = findOrder(orderNumber);
// 재고 확인
checkInventory(order);
// 결제 처리
processPayment(order);
// 배송 요청
requestDelivery(order);
return "주문 처리 완료";
}
}
public class AsynchronousExample {
public CompletableFuture<String> processOrderAsync(String orderNumber) {
return CompletableFuture
.supplyAsync(() -> findOrder(orderNumber))
.thenApplyAsync(this::checkInventory)
.thenApplyAsync(this::processPayment)
.thenApplyAsync(this::requestDelivery)
.thenApply(order -> "주문 처리 완료");
}
}
public class BlockingQueueExample {
private BlockingQueue<String> queue = new LinkedBlockingQueue<>();
public void produce(String data) {
try {
queue.put(data); // 큐가 가득 찼다면 블로킹
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
public String consume() {
try {
return queue.take(); // 큐가 비었다면 블로킹
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
}
}
}
public class NonBlockingQueueExample {
private ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();
public boolean produce(String data) {
return queue.offer(data); // 즉시 반환
}
public String consume() {
return queue.poll(); // 즉시 반환
}
}
@GetMapping("/sync-blocking")
public String syncBlocking() {
// 외부 API 호출
RestTemplate restTemplate = new RestTemplate();
String result = restTemplate.getForObject(
"http://api.example.com/data",
String.class
); // 응답을 받을 때까지 블로킹
return result;
}
@GetMapping("/async-non-blocking")
public Mono<String> asyncNonBlocking() {
// WebClient를 사용한 비동기 호출
return WebClient.create()
.get()
.uri("http://api.example.com/data")
.retrieve()
.bodyToMono(String.class);
}
@Service
public class SyncOrderService {
public Order createOrder(OrderRequest request) {
// 트랜잭션 내에서 순차적 처리
Order order = orderRepository.save(request.toOrder());
paymentService.process(order);
notificationService.notify(order);
return order;
}
}
@Service
public class AsyncOrderService {
public CompletableFuture<Order> createOrderAsync(OrderRequest request) {
return CompletableFuture
.supplyAsync(() -> orderRepository.save(request.toOrder()))
.thenApplyAsync(order -> {
paymentService.processAsync(order);
return order;
})
.thenApplyAsync(order -> {
notificationService.notifyAsync(order);
return order;
});
}
}
public class AsyncProcessing {
public CompletableFuture<String> processAsync(String input) {
return CompletableFuture.supplyAsync(() -> {
// 시간이 걸리는 작업
return heavyProcessing(input);
}).thenApply(result -> {
// 결과 변환
return transform(result);
}).thenAcceptAsync(result -> {
// 비동기 결과 처리
saveToDatabase(result);
});
}
}
public class CallbackExample {
public void processWithCallback(String input, AsyncCallback<String> callback) {
new Thread(() -> {
try {
String result = heavyProcessing(input);
callback.onSuccess(result);
} catch (Exception e) {
callback.onFailure(e);
}
}).start();
}
}
interface AsyncCallback<T> {
void onSuccess(T result);
void onFailure(Throwable throwable);
}
public class ReactiveExample {
public Flux<String> processReactive(String input) {
return Flux.just(input)
.map(this::heavyProcessing)
.publishOn(Schedulers.boundedElastic())
.flatMap(this::saveToDatabase);
}
}
Java 21에서 정식으로 도입된 Virtual Threads는 비동기 프로그래밍의 복잡성을 줄이면서도 높은 동시성을 제공합니다.
public class VirtualThreadExample {
public void processWithVirtualThread() {
try {
Thread.startVirtualThread(() -> {
// 시간이 걸리는 작업
heavyProcessing();
// I/O 작업
saveToDatabase();
});
} catch (Exception e) {
e.printStackTrace();
}
}
// ExecutorService 사용
public void processWithVirtualThreadPool() {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10_000).forEach(i -> {
executor.submit(() -> {
// 각각의 작업이 별도의 가상 스레드에서 실행
heavyProcessing();
return i;
});
});
}
}
}
장점
고려사항
Q: 동기/비동기와 블로킹/논블로킹의 차이점을 설명해주세요.
A: 주요 차이점:
동기/비동기
블로킹/논블로킹
Q: Virtual Thread가 기존의 비동기 프로그래밍 방식과 비교하여 가지는 장점은 무엇인가요?
A: Virtual Thread의 주요 장점:
코드 작성 및 유지보수
성능