비동기 처리는 요청한 작업의 완료를 기다리지 않고 다음 작업을 수행하는 방식입니다. Spring Boot에서는 다음과 같은 상황에서 주로 사용됩니다:
Spring Boot에서는 크게 세 가지 방식으로 비동기 처리를 구현할 수 있습니다.
@Async는 다음과 같은 상황에서 가장 적합합니다:
단순한 비동기 처리가 필요한 경우
기존 동기 메서드를 비동기로 전환할 때
@Service
public class NotificationService {
@Async
public void sendNotification(String userId) {
// 간단한 알림 발송
}
@Async
public CompletableFuture<Boolean> generateReport(Long reportId) {
// 시간이 걸리는 보고서 생성
return CompletableFuture.completedFuture(true);
}
}
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(30);
executor.setQueueCapacity(50);
executor.setThreadNamePrefix("Async-");
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new SimpleAsyncUncaughtExceptionHandler();
}
}
@EnableAsync만 설정하고 Executor를 따로 설정하지 않으면, Spring은 기본적으로 SimpleAsyncTaskExecutor를 사용합니다.
이 SimpleAsyncTaskExecutor의 특징은:
따라서 프로덕션 환경에서는 위험할 수 있습니다:
1. 리소스 낭비 (계속 새로운 스레드 생성)
2. 스레드 수 제한이 없어서 시스템 부하 위험
3. 성능 저하 (스레드 생성/제거 오버헤드)
그래서 실제 서비스에서는 ThreadPoolTaskExecutor 설정을 통해 스레드 풀을 명시적으로 설정하는 것이 권장됩니다.
AsyncTaskExecutor는 다음과 같은 상황에서 유용합니다:
세밀한 스레드 풀 제어가 필요한 경우
대량의 비동기 작업을 처리할 때
@Configuration
public class ExecutorConfig {
@Bean("highPriorityExecutor") // 구현체 등록
public AsyncTaskExecutor highPriorityExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setThreadNamePrefix("high-priority-");
executor.initialize();
return executor;
}
}
@Service
@RequiredArgsConstructor
public class BatchProcessService {
// AsyncTaskExecutor 인터페이스를 사용
private final AsyncTaskExecutor highPriorityExecutor;
private final AsyncTaskExecutor lowPriorityExecutor;
public void processTasks(List<Task> tasks) {
tasks.forEach(task -> {
if (task.isHighPriority()) {
highPriorityExecutor.execute(() -> processTask(task));
} else {
lowPriorityExecutor.execute(() -> processTask(task));
}
});
}
}
CompletableFuture는 다음과 같은 상황에서 강점을 발휘합니다:
복잡한 비동기 작업 흐름이 필요한 경우
세밀한 예외 처리와 fallback이 필요한 경우
@Service // 스프링의 서비스 계층 컴포넌트임을 나타내는 어노테이션
public class OrderProcessingService {
// 비동기 주문 처리를 위한 메서드, CompletableFuture를 사용하여 비동기 실행 체인 구성
public CompletableFuture<OrderResult> processOrder(Long orderId) {
return validateOrder(orderId) // Step 1: 주문 유효성 검증 (비동기)
.thenCompose(valid -> { // 검증 결과를 받아 다음 단계 결정
if (!valid) return CompletableFuture.failedFuture(new OrderValidationException());
return checkInventory(orderId); // Step 2: 재고 상태 확인 (비동기)
})
.thenCompose(inStock -> { // 재고 확인 결과를 받아 다음 단계 결정
if (!inStock) return CompletableFuture.failedFuture(new OutOfStockException());
return processPayment(orderId); // Step 3: 결제 진행 (비동기)
})
.thenCompose(paid -> { // 결제 결과를 받아 다음 단계 결정
if (!paid) return CompletableFuture.failedFuture(new PaymentFailedException());
return updateInventory(orderId); // Step 4: 재고 수량 갱신 (비동기)
})
.exceptionally(ex -> { // 에러 처리 로직
// 각 단계별 예외 상황에 따른 적절한 주문 상태 반환
if (ex instanceof OrderValidationException) {
return new OrderResult(OrderStatus.INVALID); // 주문 유효성 검증 실패
}
if (ex instanceof OutOfStockException) {
return new OrderResult(OrderStatus.OUT_OF_STOCK); // 재고 부족
}
return new OrderResult(OrderStatus.FAILED); // 기타 실패
});
}
}
@Async
AsyncTaskExecutor
CompletableFuture
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new AsyncUncaughtExceptionHandler() {
@Override
public void handleUncaughtException(Throwable ex, Method method, Object... params) {
log.error("Async method {} threw exception:", method.getName(), ex);
// 추가 에러 처리 로직
}
};
}
}
Future<String> future = asyncMethod();
String result = future.get(); // 블로킹 호출
try {
Future<User> future = userService.findUser(id);
User user = future.get(5, TimeUnit.SECONDS);
} catch (ExecutionException e) {
// 비즈니스 예외 처리
} catch (TimeoutException e) {
// 타임아웃 처리
}
ListenableFuture<String> future = asyncMethod();
future.addCallback(
result -> log.info("Success: {}", result),
ex -> log.error("Error", ex)
);
ListenableFuture<String> future = asyncMethod();
future.addCallback(
new ListenableFutureCallback<String>() {
@Override
public void onSuccess(String result) {
log.info("Success: {}", result);
}
@Override
public void onFailure(Throwable ex) {
if (ex instanceof CustomException) {
log.error("Custom error occurred: {}", ex.getMessage());
} else {
log.error("Unexpected error", ex);
}
}
}
);
thenApply() // 값 변환
thenCompose() // Future 조합
thenCombine() // 두 Future 결과 조합
allOf() // 여러 Future 동시 실행
CompletableFuture<String> future = asyncMethod()
.thenApply(result -> result + " processed")
.thenCombine(anotherFuture, (r1, r2) -> r1 + r2)
.exceptionally(ex -> "Error: " + ex.getMessage());
CompletableFuture<User> future = userService.findUser(id)
.exceptionally(ex -> {
log.error("Error finding user", ex);
return null;
})
.handle((user, ex) -> {
if (ex != null) {
return new User(); // 기본값 반환
}
return user;
});
@Service
@RequiredArgsConstructor
public class OrderService {
private final ApplicationEventPublisher eventPublisher;
@Transactional
public void processOrder(Order order) {
orderRepository.save(order);
eventPublisher.publishEvent(new OrderProcessedEvent(order.getId()));
}
@EventListener
@Async
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void handleOrderProcessed(OrderProcessedEvent event) {
Order order = orderRepository.findById(event.getOrderId()).orElseThrow();
// 별도 트랜잭션에서 처리
}
}
@Service
@RequiredArgsConstructor
public class OrderService {
private final TransactionTemplate transactionTemplate;
@Async
public CompletableFuture<Order> processOrderAsync(Long orderId) {
return CompletableFuture.supplyAsync(() ->
transactionTemplate.execute(status -> {
Order order = orderRepository.findById(orderId).orElseThrow();
order.process();
return orderRepository.save(order);
})
);
}
}
Spring Boot의 비동기 처리는 애플리케이션의 성능을 크게 향상시킬 수 있는 강력한 기능입니다. 하지만 제대로 사용하기 위해서는 각 방식의 특징과 주의사항을 잘 이해하고 있어야 합니다.
특히 트랜잭션과 함께 사용할 때는 더욱 신중한 접근이 필요합니다. 이 글에서 다룬 내용들을 참고하여 여러분의 프로젝트에 맞는 최적의 방식을 선택하시기 바랍니다.