728x90
반응형
프로그래밍에서 자주 마주치는 개념인 '동기/비동기'와 '블로킹/논블로킹'은 종종 혼동되거나 같은 의미로 잘못 이해되곤 합니다. 이 글에서는 이 두 개념의 차이점과 각각의 특징을 명확히 알아보겠습니다.
1. 핵심 개념 정리
Sync vs Async (동기 vs 비동기)
동기(Synchronous)와 비동기(Asynchronous)는 프로그램이 작업을 처리하는 방식에 차이를 둡니다.
- 동기(Synchronous): 작업이 순차적으로 진행됩니다. 하나의 작업이 끝난 후에야 다음 작업을 시작할 수 있습니다. 즉, 현재 작업이 완료될 때까지 다른 작업을 기다려야 합니다.
public void syncExample() { task1(); // task1이 끝날 때까지 task2는 시작되지 않음 task2(); }
- 비동기(Asynchronous): 작업을 시작한 후, 해당 작업이 완료되기를 기다리지 않고 다른 작업을 병렬로 처리할 수 있습니다. 즉, 작업이 끝나는 시간을 기다리지 않고 다른 일을 할 수 있습니다.
public void asyncExample() { CompletableFuture.runAsync(() -> task1()); // task1을 비동기로 실행 task2(); // task2는 task1이 끝날 때까지 기다리지 않고 실행 }
Block vs Non-block (블로킹 vs 논블로킹)
블로킹(Blocking)과 논블로킹(Non-blocking)은 주로 I/O 작업이나 네트워크 요청에서 동작 방식에 차이를 둡니다.
- 블로킹(Blocking): 작업을 요청한 후, 해당 작업이 완료될 때까지 코드 실행이 멈춥니다. 예를 들어, 파일을 읽거나 네트워크 요청을 할 때, 요청이 끝날 때까지 다른 코드가 실행되지 않습니다.
public void blockingExample() { readFile(); // 파일 읽기를 완료할 때까지 기다림 System.out.println("File read complete."); }
- 논블로킹(Non-blocking): 요청을 보낸 후, 해당 작업이 완료되지 않아도 다른 작업을 계속해서 실행할 수 있습니다. I/O 작업이나 네트워크 요청이 끝날 때까지 기다리지 않고 다른 처리를 할 수 있습니다.
public void nonBlockingExample() { readFileAsync(); // 파일 읽기를 비동기로 실행 System.out.println("File read started, but not waiting."); }
2. 네 가지 조합의 이해
2.1 동기-블로킹 (Synchronous-Blocking)
// 전형적인 동기-블로킹 예제
public String readFile() {
return Files.readString(Path.of("file.txt")); // 파일을 읽을 때까지 대기
}
- 특징:
- 작업이 완료될 때까지 대기
- 다른 작업을 수행할 수 없음
- 사용 사례:
- 간단한 파일 입출력
- JDBC를 사용한 데이터베이스 연동
2.2 동기-논블로킹 (Synchronous-Non-blocking)
// 동기-논블로킹 예제
while (!futureResult.isDone()) {
// 다른 작업을 수행할 수 있음
doSomethingElse();
}
- 특징:
- 작업 완료를 기다리지만, 그 동안 다른 일을 할 수 있음
- 주기적으로 작업 완료 여부를 확인
- 사용 사례:
- Future/Promise 패턴
- NIO의 Selector
2.3 비동기-블로킹 (Asynchronous-Blocking)
// 비동기-블로킹 예제
CompletableFuture.supplyAsync(() -> {
return blockingOperation(); // 블로킹 발생
}).thenAccept(result -> {
processResult(result);
});
- 특징:
- 비동기로 작업을 시작하지만, 내부적으로 블로킹이 발생
- 일반적으로 피해야 할 패턴
- 사용 사례:
- 블로킹 API를 비동기 프레임워크에서 사용할 때
2.4 비동기-논블로킹 (Asynchronous-Non-blocking)
// 비동기-논블로킹 예제
webClient.get()
.uri("/api/data")
.retrieve()
.bodyToMono(String.class)
.subscribe(data -> {
processData(data); // 데이터가 준비되면 처리
});
- 특징:
- 작업을 시작하고 바로 반환
- 작업이 완료되면 콜백이나 이벤트로 처리
- 사용 사례:
- Spring WebFlux
- Node.js의 비동기 IO
- Reactive Programming
3. 실제 사용 예시
3.1 파일 처리 예시
// 동기-블로킹
String content = Files.readString(Path.of("file.txt"));
// 비동기-논블로킹
AsyncFileChannel.open(Path.of("file.txt"))
.read(buffer, 0)
.thenAccept(bytesRead -> {
// 처리 로직
});
3.2 네트워크 요청 예시
// 동기-블로킹 (RestTemplate)
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
// 비동기-논블로킹 (WebClient)
webClient.get()
.uri(url)
.retrieve()
.bodyToMono(String.class)
.subscribe(response -> {
// 처리 로직
});
4. 선택 기준
4.1 동기-블로킹을 선택하는 경우
- 간단한 로직이나 빠른 처리가 필요할 때
- 순차적인 처리가 중요할 때
- 코드의 가독성이 중요할 때
4.2 비동기-논블로킹을 선택하는 경우
- 높은 동시성이 필요할 때
- IO 작업이 많은 경우
- 확장성이 중요한 경우
5. 성능 고려사항
5.1 리소스 사용
- 동기-블로킹: 스레드 당 하나의 요청 처리
- 비동기-논블로킹: 적은 수의 스레드로 많은 요청 처리 가능
5.2 메모리 사용
- 동기-블로킹: 스레드 스택 메모리 사용
- 비동기-논블로킹: 이벤트 루프와 콜백으로 메모리 효율적 사용
마치며
Sync/Async와 Block/Non-block은 서로 다른 개념이지만, 함께 사용될 때 프로그램의 효율성에 큰 영향을 미칩니다. 비동기와 논블로킹을 잘 활용하면, 멀티스레드 환경에서 더 빠르고 효율적인 시스템을 구축할 수 있습니다. 이 두 개념을 잘 이해하고, 상황에 맞게 적용하는 것이 중요합니다.
728x90
반응형