[Java] 동기vs비동기 / 블로킹vs논블로킹 완벽 이해하기

슈퍼대디·2024년 12월 23일

CS면접대비

목록 보기
4/13

동기vs비동기 / 블로킹vs논블로킹

목차

  1. 개념 정리
  2. 실제 사례로 보는 차이점
  3. Java에서의 구현
  4. 실무 적용 사례
  5. 면접 예상 질문

1. 개념 정리

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

동기와 비동기는 '작업 완료 여부를 누가 신경쓰는가'의 관점입니다.

  • 동기: 작업 완료 여부를 요청한 쪽에서 확인
  • 비동기: 작업 완료 여부를 요청받은 쪽에서 알려줌

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

블로킹과 논블로킹은 '작업 제어권을 누가 가지고 있는가'의 관점입니다.

  • 블로킹: 작업이 완료될 때까지 제어권을 가져감
  • 논블로킹: 작업 완료와 관계없이 제어권을 바로 반환

네 가지 조합의 이해

  1. 동기 블로킹

    • 작업 완료를 caller가 확인
    • 작업 중 제어권을 가져감
    예시: 은행 창구 대기
    - 고객: 창구에서 업무 처리를 요청
    - 직원: 업무 처리 시작
    - 고객: 창구에서 기다림 (다른 일 못함)
    - 직원: 업무 처리 완료
    - 고객: 완료 확인 후 귀가
  2. 동기 논블로킹

    • 작업 완료를 caller가 확인
    • 작업 중 제어권을 유지
    예시: 식당 주문 후 진동벨을 보며 매장에서 쇼핑
    - 손님: 주문 후 진동벨 수령
    - 손님: 매장에서 쇼핑하면서 주기적으로 진동벨 확인
    - 직원: 음식 준비 중
    - 손님: 계속 쇼핑하며 벨 확인
    - 직원: 음식 완료, 진동벨 울림
    - 손님: 직접 확인 후 음식 수령
  3. 비동기 블로킹

    • 작업 완료를 callee가 알려줌
    • 작업 중 제어권을 가져감
    예시: 택배 배송 직접 기다리기
    - 고객: 택배 기사님께 문 앞에서 기다리겠다고 알림
    - 고객: 문 앞에서 대기 (다른 일 못함)
    - 택배 기사: 도착 시 알려줌
    - 고객: 택배 수령
  4. 비동기 논블로킹

    • 작업 완료를 callee가 알려줌
    • 작업 중 제어권을 유지
    예시: 택배 배송 알림 서비스
    - 고객: 택배 배송 요청
    - 고객: 일상적인 활동 수행
    - 택배 기사: 배송 완료 후 문자 발송
    - 고객: 문자 확인 후 택배 수령

2. 상황별 예시 사례로 보는 차이점

파일 입출력 예시

  1. 동기 블로킹 I/O
// 파일을 읽을 때까지 대기
try (FileInputStream fis = new FileInputStream("file.txt")) {
    byte[] data = new byte[1024];
    fis.read(data); // 이 줄에서 블로킹
    System.out.println(new String(data));
}
  1. 동기 논블로킹 I/O
// 파일 읽기 가능 여부 확인하며 다른 작업 수행
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();
    }
}
  1. 비동기 블로킹 I/O
// CompletableFuture를 사용하지만 get()으로 블로킹
CompletableFuture<String> future = readFileAsync("file.txt");
String content = future.get(); // 이 줄에서 블로킹
System.out.println(content);
  1. 비동기 논블로킹 I/O
// 콜백으로 처리
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();

3. Java에서의 구현

동기 처리 예시

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(); // 즉시 반환
    }
}

4. 실무 적용 사례

웹 애플리케이션에서의 활용

  1. 동기 블로킹 방식
@GetMapping("/sync-blocking")
public String syncBlocking() {
    // 외부 API 호출
    RestTemplate restTemplate = new RestTemplate();
    String result = restTemplate.getForObject(
        "http://api.example.com/data",
        String.class
    ); // 응답을 받을 때까지 블로킹
    
    return result;
}
  1. 비동기 논블로킹 방식
@GetMapping("/async-non-blocking")
public Mono<String> asyncNonBlocking() {
    // WebClient를 사용한 비동기 호출
    return WebClient.create()
        .get()
        .uri("http://api.example.com/data")
        .retrieve()
        .bodyToMono(String.class);
}

데이터베이스 작업

  1. 동기 블로킹 방식
@Service
public class SyncOrderService {
    public Order createOrder(OrderRequest request) {
        // 트랜잭션 내에서 순차적 처리
        Order order = orderRepository.save(request.toOrder());
        paymentService.process(order);
        notificationService.notify(order);
        return order;
    }
}
  1. 비동기 논블로킹 방식
@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;
            });
    }
}

5. Java의 비동기 프로그래밍

CompletableFuture를 사용한 비동기 처리

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);
}

리액티브 프로그래밍 (Project Reactor)

public class ReactiveExample {
    public Flux<String> processReactive(String input) {
        return Flux.just(input)
            .map(this::heavyProcessing)
            .publishOn(Schedulers.boundedElastic())
            .flatMap(this::saveToDatabase);
    }
}

6. Virtual Threads와 Project Loom

Java 21에서 정식으로 도입된 Virtual Threads는 비동기 프로그래밍의 복잡성을 줄이면서도 높은 동시성을 제공합니다.

Virtual Thread 사용 예시

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;
                });
            });
        }
    }
}

Virtual Threads vs 기존 비동기 방식

  1. 장점

    • 동기적 코드 스타일 유지
    • 디버깅 용이성
    • 스택 트레이스 가독성
    • 기존 도구들과의 호환성
  2. 고려사항

    • CPU 집약적 작업에는 부적합
    • 플랫폼 스레드가 여전히 필요한 상황 존재
    • 메모리 사용량 고려 필요

7. 면접 예상 질문

Q: 동기/비동기와 블로킹/논블로킹의 차이점을 설명해주세요.
A: 주요 차이점:

  1. 동기/비동기

    • 동기: 요청한 작업의 완료 여부를 직접 확인
    • 비동기: 요청한 작업의 완료를 콜백 등으로 전달받음
  2. 블로킹/논블로킹

    • 블로킹: 작업 완료까지 제어권이 없음
    • 논블로킹: 작업 완료와 무관하게 제어권 유지

Q: Virtual Thread가 기존의 비동기 프로그래밍 방식과 비교하여 가지는 장점은 무엇인가요?
A: Virtual Thread의 주요 장점:

  1. 코드 작성 및 유지보수

    • 동기 방식의 코드 스타일 유지
    • 복잡한 콜백이나 비동기 체인 제거
    • 명확한 에러 처리와 스택 트레이스
  2. 성능

    • 수백만 개의 동시 작업 처리 가능
    • 효율적인 시스템 리소스 사용
    • I/O 작업에서의 높은 성능

참고 자료

profile
성장하고싶은 Backend 개발자

0개의 댓글