비동기 처리(asynchronous processing)는 백엔드 개발에서 성능, 확장성, 응답성을 높이기 위한 핵심 기법 중 하나입니다. 이 글에서는 비동기 처리의 개념부터 사용 시기, 실무 이슈, 테스트 전략, 실패 대응까지 모두 정리합니다.
비동기란 작업을 요청한 후, 그 결과를 기다리지 않고 다른 작업을 수행할 수 있는 방식입니다. 이는 주로 블로킹 I/O(예: 데이터베이스 조회, 파일 읽기, 외부 API 호출 등)에서 응답을 기다리는 동안 리소스를 낭비하지 않도록 설계하는 기법입니다.
비유: 동기는 전화 통화, 비동기는 문자 메시지. 전자는 상대의 응답을 기다리며, 후자는 메시지만 보내고 기다리지 않습니다.
// 동기 방식
String result = slowService(); // 이 줄이 끝날 때까지 다음 코드 실행 안 됨
System.out.println(result);
// 비동기 방식
CompletableFuture.supplyAsync(() -> slowService())
.thenAccept(result -> System.out.println(result));
예시 설명: 위 예제는 시간이 오래 걸리는 서비스(slowService)를 동기와 비동기로 처리했을 때의 차이를 보여줍니다. 비동기 방식은 결과가 준비된 후 콜백 함수(thenAccept)를 통해 결과를 처리합니다.
Java 8부터 제공되며, 병렬 처리, 비동기 처리, 콜백 체이닝 등을 지원합니다.
@Service
public class AsyncService {
public CompletableFuture<String> loadData() {
return CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(2000); // I/O 지연 시뮬레이션
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
return "데이터 로드 완료";
});
}
}
Spring에서 기본 제공하는 방식으로, 간단히 어노테이션으로 메서드를 비동기화할 수 있습니다.
@Async
public CompletableFuture<String> asyncMethod() {
return CompletableFuture.completedFuture("비동기 응답");
}
주의사항: @Async는 스프링 컨텍스트에 빈으로 등록된 클래스에서만 정상 작동하며, 같은 클래스 내 self-invocation은 비동기로 작동하지 않습니다.
비동기 로직에서 예외가 발생하면 일반적인 try-catch로는 처리되지 않고 콜백 내에서 처리해야 합니다.
CompletableFuture.supplyAsync(() -> {
throw new RuntimeException("예외 발생");
}).exceptionally(ex -> {
System.err.println("예외 처리됨: " + ex.getMessage());
return null;
});
예시 설명: exceptionally를 사용하면 예외를 감지하고 기본값 반환 또는 로깅 등의 처리를 할 수 있습니다. 단, 발생한 예외는 정상적으로 전파되지 않으므로 로깅 누락에 유의해야 합니다.
비동기 작업의 실행 순서는 예측이 어렵습니다. 다음 작업이 이전 작업의 결과에 의존하는 경우 thenCompose 또는 thenApply를 사용해야 합니다.
CompletableFuture.supplyAsync(() -> "1단계")
.thenApply(stage1 -> stage1 + " → 2단계")
.thenAccept(System.out::println);
예시 설명: thenApply는 이전 단계의 결과를 가공하고, thenAccept는 최종 결과를 소비하는 단계입니다. thenCompose는 CompletableFuture 내부 중첩을 펼칠 때 사용합니다.
비동기 실행은 별도 스레드에서 진행되기 때문에 스택 트레이스가 단절되고, 요청 단위 추적이 어렵습니다. 이를 해결하기 위해 MDC (Mapped Diagnostic Context)
를 사용하거나 Spring Sleuth
, Zipkin
등 분산 추적 도구를 활용합니다.
비동기 작업이 몰릴 경우 기본 ThreadPool이 부족해지면 작업이 지연되거나 거부될 수 있습니다. 따라서 적절한 설정이 필요합니다.
@Bean
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(100);
executor.setQueueCapacity(50);
executor.initialize();
return executor;
}
설명: corePoolSize와 maxPoolSize는 기본 및 최대 스레드 수를 설정하며, queueCapacity는 대기열 길이를 의미합니다. 비즈니스에 따라 적절한 조정이 필요합니다.
CompletableFuture<String> future = asyncService.loadData();
String result = future.get(); // 블로킹
assertEquals("데이터 로드 완료", result);
설명: get() 메서드를 통해 테스트에서 결과가 반환될 때까지 기다릴 수 있습니다. 단, 타임아웃을 설정하지 않으면 테스트가 무한 대기할 수 있습니다.
Awaitility는 비동기 테스트를 명시적으로 기다릴 수 있게 해주는 Java 라이브러리입니다.
Awaitility.await()
.atMost(Duration.ofSeconds(5))
.untilAsserted(() -> assertTrue(future.isDone()));
장점: 시간 초과와 조건을 조합하여 안정적인 테스트 수행이 가능합니다.
when(mockService.asyncMethod()).thenReturn(CompletableFuture.completedFuture("결과"));
설명: 실제 비동기 로직 대신, 예측 가능한 CompletableFuture를 반환함으로써 테스트 안정성을 높입니다.
future.get(2, TimeUnit.SECONDS);
설명: 예상치 못한 지연이 전체 시스템 병목이 되는 것을 방지할 수 있습니다.
예외 발생 시 기본값을 반환하거나, 대체 경로로 처리합니다.
.exceptionally(ex -> "기본값");
Resilience4j 라이브러리를 통해 재시도, 서킷 브레이커 등을 쉽게 구성할 수 있습니다.
Retry retry = Retry.ofDefaults("myService");
Supplier<String> retryableSupplier = Retry.decorateSupplier(retry, () -> callService());
String result = retryableSupplier.get();
지속적인 오류 발생 시 호출 자체를 차단하여 시스템을 보호합니다.
Slack, Kibana, ELK Stack과 연동하여 실시간 경고 및 로그 분석이 가능하도록 구성합니다.
비동기 처리는 유용하지만, 모든 상황에 적합한 것은 아닙니다. 오히려 복잡성만 더해지는 경우도 많습니다.
비동기 처리의 효과를 극대화하고 유지보수를 쉽게 하기 위해 함께 고려하면 좋은 기술들을 소개합니다.
비동기 처리, 잘 쓰면 약이 되고 잘못 쓰면 독이 됩니다.
성능과 응답성을 높일 수 있는 강력한 도구이지만, 다음을 꼭 기억하세요:
비동기 처리, 그 자체보다 언제, 왜, 어떻게 쓸지가 핵심입니다. 여러분의 서비스에 알맞은 전략적 선택이 되길 바랍니다.
📌 참고 키워드: Java 비동기, CompletableFuture, Spring @Async, 비동기 예외 처리, 비동기 테스트, Resilience4j, Awaitility, Kafka, WebFlux
실무에서는 항상 "왜 비동기를 써야 하는가?"를 먼저 묻고, 목적에 맞게 단계적으로 적용하는 전략이 중요합니다.
초보 백엔드 개발자를 위한 HTTP 상태코드 (0) | 2025.06.01 |
---|---|
Spring Boot에서 CORS 오류 완벽 해결하기 – @CrossOrigin, WebMvcConfigurer, 예외 처리까지 (0) | 2025.06.01 |
Spring Boot에서 Redis 캐시 적용 전략과 실전 구현: 캐시 히트율 90% 달성하기 (0) | 2025.05.29 |
백엔드 개발자, 협업이 어렵다고요? 스몰토크부터 시작해봐요 (0) | 2025.05.27 |
AWS SQS 선택기: Kafka와 RabbitMQ를 제치고 선택한 이유 (0) | 2025.05.15 |