T_era
동기/비동기와 블로킹/논블로킹 완전 공략 본문
📚 목차
개념 이해
🤔 동기(Synchronous) vs 비동기(Asynchronous)
핵심 내용: "작업의 순서와 결과 처리는 어떻게 될까?"
동기 (Synchronous)
- 정의: 각 작업의 시작과 끝이 일치하는 것
- 특징: 함수를 호출하고 그 결과를 받은 후 다음 작업을 시작
- 흐름: A → B → C (순차적 실행)
// 동기 방식 예제
public void synchronousExample() {
String result1 = doTask1(); // Task1 완료까지 대기
String result2 = doTask2(); // Task2 완료까지 대기
String result3 = doTask3(); // Task3 완료까지 대기
System.out.println("모든 작업 완료: " + result1 + ", " + result2 + ", " + result3);
}
비동기 (Asynchronous)
- 정의: 각 작업의 시작과 끝이 일치하지 않는 것
- 특징: 함수를 호출하고 결과와 상관없이 남은 작업을 수행
- 흐름: A, B, C 동시 시작 → 각각 완료 시점이 다름
// 비동기 방식 예제
public void asynchronousExample() {
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> doTask1());
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> doTask2());
CompletableFuture<String> future3 = CompletableFuture.supplyAsync(() -> doTask3());
// 모든 작업이 동시에 실행되고, 각각 완료되는 시점이 다름
System.out.println("작업들이 백그라운드에서 실행 중...");
}
🤔 블로킹(Blocking) vs 논블로킹(Non-blocking)
핵심 내용: "작업이 진행되는 동안 나는 어떤 상태일까?"
블로킹 (Blocking)
- 정의: 어떤 작업을 호출한 후 그 작업이 완료되기를 기다림
- 특징: 제어권이 호출된 함수에게 넘어감
- 상태: 대기 상태 (CPU 자원 낭비 가능)
// 블로킹 예제
public void blockingExample() {
System.out.println("파일 읽기 시작...");
String content = Files.readString(Path.of("large-file.txt")); // 여기서 멈춤
System.out.println("파일 읽기 완료: " + content.length() + "자");
// 파일 읽기가 완료될 때까지 다음 줄 실행 안됨
}
논블로킹 (Non-blocking)
- 정의: 어떤 작업을 호출한 후 그 작업이 완료되는 것을 기다리지 않고 내 작업을 진행
- 특징: 제어권이 호출자에게 유지됨
- 상태: 다른 작업 수행 가능
// 논블로킹 예제
public void nonBlockingExample() {
System.out.println("파일 읽기 시작...");
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
try {
return Files.readString(Path.of("large-file.txt"));
} catch (IOException e) {
throw new RuntimeException(e);
}
});
System.out.println("파일 읽기가 백그라운드에서 실행 중...");
System.out.println("다른 작업들을 계속 수행할 수 있습니다!");
// 나중에 결과가 필요할 때
String content = future.join();
System.out.println("파일 읽기 완료: " + content.length() + "자");
}
4가지 조합 패턴
1️⃣ 동기 + 블로킹 (Synchronous + Blocking)
가장 익숙한 형태의 프로그래밍
public class SyncBlockingExample {
public void syncBlocking() {
System.out.println("=== 동기-블로킹 예제 ===");
try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))) {
System.out.print("입력하세요: ");
// 사용자가 입력할 때까지 대기(블로킹)
String input = reader.readLine();
// 데이터를 받으면 그 다음 작업 실행(동기)
System.out.println("입력된 값: " + input);
processInput(input);
} catch (IOException e) {
e.printStackTrace();
}
}
private void processInput(String input) {
System.out.println("입력 처리 중: " + input.toUpperCase());
}
}
🤔 질문: 이 방식의 장단점은 무엇일까?
- 장점: 코드가 직관적이고 이해하기 쉬움
- 단점: I/O 작업 중 CPU가 놀고 있음 (자원 낭비)
2️⃣ 동기 + 논블로킹 (Synchronous + Non-blocking)
폴링(Polling) 방식
public class SyncNonBlockingExample {
public void syncNonBlocking() {
System.out.println("=== 동기-논블로킹 예제 ===");
try (FileChannel channel = FileChannel.open(Path.of("large-file.txt"), StandardOpenOption.READ)) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true) {
int bytesRead = channel.read(buffer); // 논블로킹: 결과 즉시 반환
if (bytesRead > 0) {
// 데이터를 읽었음
System.out.println("읽은 바이트: " + bytesRead);
buffer.flip();
// 데이터 처리
processData(buffer);
buffer.clear();
} else if (bytesRead == 0) {
System.out.println("데이터 준비 중...");
// 다른 작업 수행 (동기)
doOtherWork();
// 잠시 대기 후 다시 시도
Thread.sleep(100);
} else {
// EOF (-1) - 파일 끝
System.out.println("파일 읽기 완료");
break;
}
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
private void processData(ByteBuffer buffer) {
// 데이터 처리 로직
System.out.println("데이터 처리 중...");
}
private void doOtherWork() {
System.out.println("다른 작업 수행 중...");
}
}
🤔 질문: 폴링 방식의 문제점은 무엇일까?
- 문제점: CPU 자원을 계속 사용하여 폴링 (비효율적)
- 해결책: 적절한 대기 시간 설정 필요
3️⃣ 비동기 + 블로킹 (Asynchronous + Blocking)
병렬 작업 후 대기
public class AsyncBlockingExample {
public void asyncBlocking() {
System.out.println("=== 비동기-블로킹 예제 ===");
// 여러 일을 시작 (비동기)
CompletableFuture<String> job1 = startJob("피자 주문", 3000);
CompletableFuture<String> job2 = startJob("치킨 주문", 2000);
CompletableFuture<String> job3 = startJob("음료 주문", 1000);
System.out.println("모든 주문이 진행 중입니다...");
try {
// 모든 일이 끝날 때까지 대기 (블로킹)
CompletableFuture.allOf(job1, job2, job3).get();
System.out.println("모든 음식이 도착했습니다!");
System.out.println("결과: " + job1.get() + ", " + job2.get() + ", " + job3.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
private CompletableFuture<String> startJob(String jobName, int delay) {
return CompletableFuture.supplyAsync(() -> {
try {
System.out.println(jobName + " 시작...");
Thread.sleep(delay);
System.out.println(jobName + " 완료!");
return jobName + " 완료";
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
}
}
🤔 질문: 이 방식은 언제 유용할까?
- 유용한 경우: 여러 독립적인 작업을 병렬로 실행하고 모든 결과가 필요한 경우
- 주의사항: 하나라도 실패하면 전체가 블로킹될 수 있음
4️⃣ 비동기 + 논블로킹 (Asynchronous + Non-blocking)
가장 효율적인 방식
public class AsyncNonBlockingExample {
public void asyncNonBlocking() {
System.out.println("=== 비동기-논블로킹 예제 ===");
// 여러 일을 시작 (비동기)
startJob("피자 주문", 3000).thenAccept(result -> handleResult(result));
startJob("치킨 주문", 2000).thenAccept(result -> handleResult(result));
startJob("음료 주문", 1000).thenAccept(result -> handleResult(result));
System.out.println("모든 주문 완료! 다른 일을 계속 합니다...");
// 다른 작업 계속 수행 (논블로킹)
for (int i = 1; i <= 10; i++) {
System.out.println("다른 작업 " + i + "번 수행 중...");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
System.out.println("프로그램 종료");
}
private CompletableFuture<String> startJob(String jobName, int delay) {
return CompletableFuture.supplyAsync(() -> {
try {
System.out.println(jobName + " 시작...");
Thread.sleep(delay);
System.out.println(jobName + " 완료!");
return jobName + " 완료";
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
}
// 작업이 끝나는 대로 즉시 호출됨 (논블로킹)
private void handleResult(String result) {
System.out.println("🎉 [즉시처리] " + result + " - 바로 고객에게 전달!");
}
}
🤔 질문: 이 방식이 가장 효율적인 이유는?
- 이유: CPU 자원을 최대한 활용하면서도 응답성을 유지
- 장점: 높은 처리량과 낮은 지연시간
실제 사용 예제
🌐 Spring Boot에서의 비동기 처리
@RestController
@RequestMapping("/api/async")
public class AsyncController {
@Autowired
private AsyncService asyncService;
// 비동기 + 블로킹 예제
@GetMapping("/users")
public CompletableFuture<List<User>> getUsersAsync() {
return asyncService.getUsersAsync();
}
// 동기 + 블로킹 예제 (기존 방식)
@GetMapping("/users-sync")
public List<User> getUsersSync() {
return asyncService.getUsersSync();
}
// 여러 작업을 병렬로 처리
@GetMapping("/dashboard")
public CompletableFuture<DashboardData> getDashboardData() {
CompletableFuture<List<User>> usersFuture = asyncService.getUsersAsync();
CompletableFuture<List<Order>> ordersFuture = asyncService.getOrdersAsync();
CompletableFuture<Statistics> statsFuture = asyncService.getStatisticsAsync();
return CompletableFuture.allOf(usersFuture, ordersFuture, statsFuture)
.thenApply(v -> new DashboardData(
usersFuture.join(),
ordersFuture.join(),
statsFuture.join()
));
}
}
🔄 WebFlux와 리액티브 프로그래밍
@RestController
@RequestMapping("/api/reactive")
public class ReactiveController {
@Autowired
private ReactiveService reactiveService;
// 리액티브 스트림 (논블로킹)
@GetMapping("/users")
public Flux<User> getUsersReactive() {
return reactiveService.getUsersFlux();
}
// 백프레셔 지원
@GetMapping("/users/stream")
public Flux<User> streamUsers() {
return reactiveService.getUsersStream()
.onBackpressureBuffer(1000) // 백프레셔 처리
.delayElements(Duration.ofMillis(100)); // 속도 제어
}
}
문제점과 해결방안
🚨 주요 문제점들
1. 콜백 지옥 (Callback Hell)
// 문제가 있는 코드
public void callbackHell() {
asyncTask1().thenAccept(result1 -> {
asyncTask2(result1).thenAccept(result2 -> {
asyncTask3(result2).thenAccept(result3 -> {
asyncTask4(result3).thenAccept(result4 -> {
System.out.println("최종 결과: " + result4);
});
});
});
});
}
해결방안: CompletableFuture 체이닝
public void cleanAsyncChain() {
asyncTask1()
.thenCompose(this::asyncTask2)
.thenCompose(this::asyncTask3)
.thenCompose(this::asyncTask4)
.thenAccept(result -> System.out.println("최종 결과: " + result))
.exceptionally(throwable -> {
System.err.println("에러 발생: " + throwable.getMessage());
return null;
});
}
2. 스레드 풀 관리
@Configuration
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("Async-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new SimpleAsyncUncaughtExceptionHandler();
}
}
3. 타임아웃 처리
public CompletableFuture<String> withTimeout() {
return CompletableFuture.supplyAsync(() -> {
// 긴 작업
try {
Thread.sleep(5000);
return "작업 완료";
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).orTimeout(3, TimeUnit.SECONDS) // 3초 타임아웃
.exceptionally(throwable -> {
if (throwable instanceof TimeoutException) {
return "타임아웃 발생";
}
return "에러 발생: " + throwable.getMessage();
});
}
🎯 성능 최적화 팁
- 적절한 스레드 풀 크기 설정
- CPU 바운드: CPU 코어 수 + 1
- I/O 바운드: CPU 코어 수 * 2
- 백프레셔 처리
- 무한 스트림 방지
- 메모리 사용량 제어
- 에러 처리
- Circuit Breaker 패턴 적용
- 재시도 로직 구현
Spring Boot에서의 활용
📦 의존성 추가
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'io.projectreactor:reactor-core'
}
⚙️ 설정
@Configuration
@EnableAsync
public class AsyncConfiguration {
@Bean
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(500);
executor.setThreadNamePrefix("AsyncTask-");
executor.initialize();
return executor;
}
}
🔧 실제 서비스 구현
@Service
public class UserService {
@Async
public CompletableFuture<List<User>> getUsersAsync() {
// 비동기로 사용자 목록 조회
return CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(1000); // DB 조회 시뮬레이션
return Arrays.asList(
new User(1L, "김철수"),
new User(2L, "이영희")
);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
}
public Flux<User> getUsersReactive() {
return Flux.fromIterable(Arrays.asList(
new User(1L, "김철수"),
new User(2L, "이영희")
)).delayElements(Duration.ofMillis(100));
}
}
📝 정리
🎯 언제 어떤 방식을 사용할까?
| 상황 | 권장 방식 | 이유 |
|---|---|---|
| 간단한 순차 작업 | 동기 + 블로킹 | 코드가 직관적이고 이해하기 쉬움 |
| I/O 작업이 많은 경우 | 비동기 + 논블로킹 | CPU 자원을 효율적으로 활용 |
| 여러 독립 작업 | 비동기 + 블로킹 | 병렬 처리로 성능 향상 |
| 실시간 스트리밍 | 리액티브 스트림 | 백프레셔와 메모리 효율성 |
🤔 마지막 질문들
- Q: 항상 비동기 + 논블로킹이 최선인가?
- A: 아니요. 상황에 따라 적절한 방식을 선택해야 한다.
- Q: 동시성 문제는 어떻게 해결하나요?
- A: synchronized, volatile, Atomic 클래스, Lock 등을 사용한다.
- Q: 디버깅이 어려운데 어떻게 하나요?
- A: 로깅, 메트릭, 분산 추적 도구를 활용한다.
'이론 > 백엔드 개념정리' 카테고리의 다른 글
| 네트워크 7계층, TCP, 오버헤드 – 개발자가 꼭 알아야 할 네트워크 기초 (0) | 2025.06.24 |
|---|---|
| Repository에서 예외 처리: 단일 책임 원칙(SRP) 위배인가? (0) | 2025.05.23 |
| Service계층에서 서로 다른 Repository를 가져오는건 SRP를 위배한 방법일까? (0) | 2025.05.23 |
| MVC 패턴의 한계점 (0) | 2025.05.05 |
| MVC (Model-View-Controller) 패턴 개요 (0) | 2025.05.05 |