Spring 13: 비동기 처리
@Async와 CompletableFuture 활용
1. 비동기 처리 개념
1.1 동기 vs 비동기
동기 처리 (Synchronous)
동기 처리는 작업이 순차적으로 실행되며, 하나의 작업이 완료될 때까지 다음 작업이 대기하는 방식입니다. 코드의 실행 순서가 예측 가능하고 이해하기 쉽지만, I/O 작업이나 시간이 오래 걸리는 작업에서 전체 시스템의 성능이 저하될 수 있습니다.
// 동기 처리 예제
@Service
public class SynchronousService {
public void processOrder(Order order) {
// 1. 주문 검증 (100ms)
validateOrder(order);
// 2. 재고 확인 (200ms)
checkInventory(order);
// 3. 결제 처리 (500ms)
processPayment(order);
// 4. 이메일 발송 (300ms)
sendEmail(order);
// 총 처리 시간: 1100ms
log.info("주문 처리 완료: {}", order.getId());
}
private void validateOrder(Order order) {
// 주문 검증 로직
try {
Thread.sleep(100); // 시뮬레이션
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private void checkInventory(Order order) {
// 재고 확인 로직
try {
Thread.sleep(200);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private void processPayment(Order order) {
// 결제 처리 로직
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private void sendEmail(Order order) {
// 이메일 발송 로직
try {
Thread.sleep(300);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}비동기 처리 (Asynchronous)
비동기 처리는 작업을 병렬로 실행하거나 백그라운드에서 처리하여 메인 스레드가 블로킹되지 않도록 하는 방식입니다. 시스템의 처리량과 응답성을 크게 향상시킬 수 있지만, 코드의 복잡성이 증가하고 예외 처리가 어려워질 수 있습니다.
// 비동기 처리 예제
@Service
public class AsynchronousService {
@Async
public CompletableFuture<Void> processOrderAsync(Order order) {
// 1. 주문 검증 (필수, 동기)
validateOrder(order);
// 2. 병렬 처리 가능한 작업들
CompletableFuture<Void> inventoryCheck = checkInventoryAsync(order);
CompletableFuture<Void> paymentProcess = processPaymentAsync(order);
CompletableFuture<Void> emailSend = sendEmailAsync(order);
// 모든 비동기 작업 완료 대기
return CompletableFuture.allOf(inventoryCheck, paymentProcess, emailSend)
.thenRun(() -> log.info("주문 처리 완료: {}", order.getId()));
// 총 처리 시간: 약 500ms (가장 오래 걸리는 작업 기준)
}
@Async
public CompletableFuture<Void> checkInventoryAsync(Order order) {
return CompletableFuture.runAsync(() -> {
try {
Thread.sleep(200);
log.info("재고 확인 완료: {}", order.getId());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
@Async
public CompletableFuture<Void> processPaymentAsync(Order order) {
return CompletableFuture.runAsync(() -> {
try {
Thread.sleep(500);
log.info("결제 처리 완료: {}", order.getId());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
@Async
public CompletableFuture<Void> sendEmailAsync(Order order) {
return CompletableFuture.runAsync(() -> {
try {
Thread.sleep(300);
log.info("이메일 발송 완료: {}", order.getId());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
}1.2 비동기 처리의 필요성
성능 향상
I/O 바운드 작업(데이터베이스 조회, 외부 API 호출, 파일 읽기/쓰기)에서 CPU가 대기하는 시간을 줄여 전체적인 처리량을 향상시킵니다.
성능 비교 예제
사용자 경험 개선
웹 애플리케이션에서 사용자 요청에 대한 응답 시간을 단축하여 더 나은 사용자 경험을 제공합니다.
@RestController
public class UserController {
@Autowired
private UserService userService;
// 동기 처리 - 응답까지 3초 소요
@GetMapping("/users/{id}/profile-sync")
public ResponseEntity<UserProfile> getUserProfileSync(@PathVariable Long id) {
UserProfile profile = userService.getUserProfileSync(id);
return ResponseEntity.ok(profile);
}
// 비동기 처리 - 즉시 응답, 백그라운드에서 처리
@GetMapping("/users/{id}/profile-async")
public CompletableFuture<ResponseEntity<UserProfile>> getUserProfileAsync(@PathVariable Long id) {
return userService.getUserProfileAsync(id)
.thenApply(ResponseEntity::ok);
}
}리소스 효율성
스레드 풀을 효율적으로 활용하여 시스템 리소스를 최적화하고, 더 많은 동시 요청을 처리할 수 있습니다.
1.3 비동기 처리 사용 사례
1. 이메일 및 알림 발송
사용자 등록, 주문 완료, 비밀번호 재설정 등의 이벤트 발생 시 이메일이나 푸시 알림을 비동기로 발송합니다.
@Service
public class NotificationService {
@Async
public CompletableFuture<Void> sendWelcomeEmail(User user) {
return CompletableFuture.runAsync(() -> {
// 이메일 템플릿 생성
EmailTemplate template = createWelcomeTemplate(user);
// 외부 이메일 서비스 호출
emailProvider.send(template);
log.info("환영 이메일 발송 완료: {}", user.getEmail());
});
}
@Async
public CompletableFuture<Void> sendOrderConfirmation(Order order) {
return CompletableFuture.runAsync(() -> {
// 주문 확인 이메일 발송
EmailTemplate template = createOrderTemplate(order);
emailProvider.send(template);
// SMS 알림도 함께 발송
smsProvider.send(order.getUser().getPhone(),
"주문이 완료되었습니다. 주문번호: " + order.getId());
log.info("주문 확인 알림 발송 완료: {}", order.getId());
});
}
}2. 데이터 처리 및 분석
대용량 데이터 처리, 로그 분석, 리포트 생성 등의 시간이 오래 걸리는 작업을 백그라운드에서 처리합니다.
@Service
public class DataProcessingService {
@Async
public CompletableFuture<ProcessingResult> processLargeDataset(Dataset dataset) {
return CompletableFuture.supplyAsync(() -> {
log.info("데이터 처리 시작: {} records", dataset.getSize());
ProcessingResult result = new ProcessingResult();
// 청크 단위로 데이터 처리
List<DataChunk> chunks = dataset.splitIntoChunks(1000);
for (DataChunk chunk : chunks) {
ProcessingResult chunkResult = processChunk(chunk);
result.merge(chunkResult);
// 진행률 업데이트
updateProgress(dataset.getId(), result.getProcessedCount(), dataset.getSize());
}
log.info("데이터 처리 완료: {}", dataset.getId());
return result;
});
}
@Async
public CompletableFuture<Report> generateMonthlyReport(int year, int month) {
return CompletableFuture.supplyAsync(() -> {
// 월간 리포트 생성 (시간 소요 작업)
Report report = new Report();
// 각종 통계 데이터 수집
report.setSalesData(collectSalesData(year, month));
report.setUserData(collectUserData(year, month));
report.setPerformanceData(collectPerformanceData(year, month));
// 리포트 파일 생성
String filePath = generateReportFile(report);
report.setFilePath(filePath);
return report;
});
}
}3. 외부 API 호출
결제 처리, 소셜 로그인, 지도 서비스 등 외부 API를 호출할 때 네트워크 지연을 비동기로 처리합니다.
@Service
public class ExternalApiService {
@Async
public CompletableFuture<PaymentResult> processPayment(PaymentRequest request) {
return CompletableFuture.supplyAsync(() -> {
try {
// 외부 결제 API 호출
PaymentResponse response = paymentGateway.charge(request);
if (response.isSuccess()) {
// 결제 성공 처리
return PaymentResult.success(response.getTransactionId());
} else {
// 결제 실패 처리
return PaymentResult.failure(response.getErrorMessage());
}
} catch (Exception e) {
log.error("결제 처리 중 오류 발생", e);
return PaymentResult.error(e.getMessage());
}
});
}
@Async
public CompletableFuture<List<Product>> fetchProductsFromMultipleSources(String category) {
// 여러 외부 소스에서 상품 정보를 병렬로 가져오기
CompletableFuture<List<Product>> source1 = CompletableFuture.supplyAsync(() ->
externalApi1.getProducts(category));
CompletableFuture<List<Product>> source2 = CompletableFuture.supplyAsync(() ->
externalApi2.getProducts(category));
CompletableFuture<List<Product>> source3 = CompletableFuture.supplyAsync(() ->
externalApi3.getProducts(category));
return CompletableFuture.allOf(source1, source2, source3)
.thenApply(v -> {
List<Product> allProducts = new ArrayList<>();
allProducts.addAll(source1.join());
allProducts.addAll(source2.join());
allProducts.addAll(source3.join());
return allProducts;
});
}
}4. 이벤트 처리
시스템 내에서 발생하는 다양한 이벤트를 비동기로 처리하여 이벤트 발생자와 처리자를 분리합니다.
@Component
public class EventHandler {
@EventListener
@Async
public void handleUserRegistration(UserRegisteredEvent event) {
User user = event.getUser();
// 여러 후속 작업을 비동기로 처리
CompletableFuture.allOf(
sendWelcomeEmail(user),
createUserProfile(user),
setupDefaultSettings(user),
logUserActivity(user)
).thenRun(() -> {
log.info("사용자 등록 후속 처리 완료: {}", user.getId());
});
}
@EventListener
@Async
public void handleOrderPlaced(OrderPlacedEvent event) {
Order order = event.getOrder();
// 주문 관련 비동기 처리
CompletableFuture.allOf(
updateInventory(order),
sendOrderConfirmation(order),
notifyWarehouse(order),
updateAnalytics(order)
).thenRun(() -> {
log.info("주문 처리 완료: {}", order.getId());
});
}
private CompletableFuture<Void> sendWelcomeEmail(User user) {
return CompletableFuture.runAsync(() -> {
// 환영 이메일 발송 로직
});
}
private CompletableFuture<Void> updateInventory(Order order) {
return CompletableFuture.runAsync(() -> {
// 재고 업데이트 로직
});
}
}💡 핵심 포인트
- • 비동기 처리는 I/O 바운드 작업에서 특히 효과적입니다
- • CPU 집약적인 작업에서는 오히려 성능이 저하될 수 있습니다
- • 적절한 스레드 풀 설정이 성능에 큰 영향을 미칩니다
- • 예외 처리와 디버깅이 동기 처리보다 복잡합니다
@EnableAsync 설정
Spring에서 비동기 처리를 활성화하기 위한 설정입니다.
@EnableAsync
@Configuration
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(500);
executor.initialize();
return executor;
}
}@Async 어노테이션
메서드를 비동기로 실행하도록 지정하는 어노테이션입니다.
@Service
public class EmailService {
@Async
public void sendEmail(String to, String subject, String body) {
// 이메일 발송 로직
System.out.println("Sending email to: " + to);
}
@Async
public CompletableFuture<String> processData(String data) {
// 데이터 처리 로직
return CompletableFuture.completedFuture("Processed: " + data);
}
}CompletableFuture 활용
Java 8의 CompletableFuture를 사용하여 복잡한 비동기 작업을 처리할 수 있습니다.
@Service
public class DataService {
public CompletableFuture<String> fetchUserData(Long userId) {
return CompletableFuture.supplyAsync(() -> {
// 사용자 데이터 조회
return "User data for " + userId;
});
}
public CompletableFuture<String> combineData() {
CompletableFuture<String> future1 = fetchUserData(1L);
CompletableFuture<String> future2 = fetchUserData(2L);
return future1.thenCombine(future2, (data1, data2) ->
data1 + " and " + data2);
}
}예외 처리와 모니터링
비동기 작업에서의 예외 처리와 성능 모니터링 방법을 알아봅니다.
주의사항
- 예외 처리 전략 수립
- 스레드 풀 크기 조정
- 메모리 사용량 모니터링
6. 실전 비동기 활용
6.1 이메일 발송 서비스
비동기 이메일 발송
@Service
@RequiredArgsConstructor
@Slf4j
public class AsyncEmailService {
private final JavaMailSender mailSender;
private final TemplateEngine templateEngine;
@Async("emailExecutor")
public CompletableFuture<Boolean> sendWelcomeEmail(User user) {
try {
Context context = new Context();
context.setVariable("userName", user.getName());
context.setVariable("activationLink", generateActivationLink(user));
String htmlContent = templateEngine.process("welcome-email", context);
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
helper.setTo(user.getEmail());
helper.setSubject("환영합니다! 회원가입이 완료되었습니다.");
helper.setText(htmlContent, true);
mailSender.send(message);
log.info("Welcome email sent to: {}", user.getEmail());
return CompletableFuture.completedFuture(true);
} catch (Exception e) {
log.error("Failed to send welcome email to: {}", user.getEmail(), e);
return CompletableFuture.completedFuture(false);
}
}
@Async("emailExecutor")
public CompletableFuture<Boolean> sendOrderConfirmation(Order order) {
try {
Context context = new Context();
context.setVariable("order", order);
context.setVariable("items", order.getOrderItems());
context.setVariable("totalAmount", order.getTotalAmount());
String htmlContent = templateEngine.process("order-confirmation", context);
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
helper.setTo(order.getMember().getEmail());
helper.setSubject("주문이 완료되었습니다. [주문번호: " + order.getOrderNumber() + "]");
helper.setText(htmlContent, true);
mailSender.send(message);
log.info("Order confirmation sent for order: {}", order.getOrderNumber());
return CompletableFuture.completedFuture(true);
} catch (Exception e) {
log.error("Failed to send order confirmation: {}", order.getOrderNumber(), e);
return CompletableFuture.completedFuture(false);
}
}
@Async("emailExecutor")
public void sendBulkEmails(List<EmailRequest> requests) {
log.info("Starting bulk email send: {} emails", requests.size());
AtomicInteger successCount = new AtomicInteger(0);
AtomicInteger failCount = new AtomicInteger(0);
requests.parallelStream().forEach(request -> {
try {
sendSimpleEmail(request);
successCount.incrementAndGet();
} catch (Exception e) {
failCount.incrementAndGet();
log.error("Failed to send email to: {}", request.getTo(), e);
}
});
log.info("Bulk email completed. Success: {}, Failed: {}",
successCount.get(), failCount.get());
}
}6.2 이벤트 기반 비동기 처리
Spring Event와 비동기 처리
// 이벤트 정의
@Getter
@AllArgsConstructor
public class OrderCreatedEvent {
private final Order order;
private final LocalDateTime occurredAt;
public static OrderCreatedEvent of(Order order) {
return new OrderCreatedEvent(order, LocalDateTime.now());
}
}
@Getter
@AllArgsConstructor
public class PaymentCompletedEvent {
private final Payment payment;
private final Order order;
private final LocalDateTime occurredAt;
}
// 이벤트 발행
@Service
@RequiredArgsConstructor
@Transactional
public class OrderService {
private final OrderRepository orderRepository;
private final ApplicationEventPublisher eventPublisher;
public Order createOrder(OrderCreateRequest request) {
Order order = Order.create(request);
Order savedOrder = orderRepository.save(order);
// 이벤트 발행 (비동기 리스너가 처리)
eventPublisher.publishEvent(OrderCreatedEvent.of(savedOrder));
return savedOrder;
}
}
// 비동기 이벤트 리스너
@Component
@RequiredArgsConstructor
@Slf4j
public class OrderEventListener {
private final EmailService emailService;
private final InventoryService inventoryService;
private final NotificationService notificationService;
@Async("eventExecutor")
@EventListener
public void handleOrderCreated(OrderCreatedEvent event) {
Order order = event.getOrder();
log.info("Processing OrderCreatedEvent for order: {}", order.getId());
// 비동기로 여러 작업 수행
CompletableFuture.allOf(
emailService.sendOrderConfirmation(order),
inventoryService.reserveStock(order),
notificationService.notifyAdmin(order)
).exceptionally(ex -> {
log.error("Error processing order event", ex);
return null;
});
}
@Async("eventExecutor")
@EventListener
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handlePaymentCompleted(PaymentCompletedEvent event) {
// 트랜잭션 커밋 후 실행
log.info("Payment completed for order: {}", event.getOrder().getId());
emailService.sendPaymentReceipt(event.getPayment());
notificationService.notifyShipping(event.getOrder());
}
}6.3 대량 데이터 처리
비동기 배치 처리
@Service
@RequiredArgsConstructor
@Slf4j
public class AsyncBatchService {
private final ProductRepository productRepository;
@Async("batchExecutor")
public CompletableFuture<BatchResult> updatePricesAsync(
List<PriceUpdateRequest> requests) {
log.info("Starting async price update for {} products", requests.size());
long startTime = System.currentTimeMillis();
AtomicInteger successCount = new AtomicInteger(0);
AtomicInteger failCount = new AtomicInteger(0);
List<String> errors = new CopyOnWriteArrayList<>();
// 청크 단위로 분할 처리
int chunkSize = 100;
List<List<PriceUpdateRequest>> chunks = partition(requests, chunkSize);
List<CompletableFuture<Void>> futures = chunks.stream()
.map(chunk -> CompletableFuture.runAsync(() -> {
processChunk(chunk, successCount, failCount, errors);
}))
.toList();
// 모든 청크 처리 완료 대기
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
long duration = System.currentTimeMillis() - startTime;
BatchResult result = BatchResult.builder()
.totalCount(requests.size())
.successCount(successCount.get())
.failCount(failCount.get())
.errors(errors)
.durationMs(duration)
.build();
log.info("Batch completed: {}", result);
return CompletableFuture.completedFuture(result);
}
@Transactional
protected void processChunk(List<PriceUpdateRequest> chunk,
AtomicInteger successCount,
AtomicInteger failCount,
List<String> errors) {
for (PriceUpdateRequest request : chunk) {
try {
Product product = productRepository.findById(request.getProductId())
.orElseThrow(() -> new EntityNotFoundException("Product not found"));
product.updatePrice(request.getNewPrice());
successCount.incrementAndGet();
} catch (Exception e) {
failCount.incrementAndGet();
errors.add(request.getProductId() + ": " + e.getMessage());
}
}
}
private <T> List<List<T>> partition(List<T> list, int size) {
List<List<T>> partitions = new ArrayList<>();
for (int i = 0; i < list.size(); i += size) {
partitions.add(list.subList(i, Math.min(i + size, list.size())));
}
return partitions;
}
}6.4 외부 API 병렬 호출
여러 외부 서비스 동시 호출
@Service
@RequiredArgsConstructor
@Slf4j
public class ProductAggregationService {
private final InventoryClient inventoryClient;
private final PricingClient pricingClient;
private final ReviewClient reviewClient;
private final RecommendationClient recommendationClient;
public ProductDetailResponse getProductDetail(Long productId) {
long startTime = System.currentTimeMillis();
// 4개의 외부 API를 병렬로 호출
CompletableFuture<InventoryInfo> inventoryFuture =
CompletableFuture.supplyAsync(() -> inventoryClient.getInventory(productId));
CompletableFuture<PricingInfo> pricingFuture =
CompletableFuture.supplyAsync(() -> pricingClient.getPricing(productId));
CompletableFuture<List<Review>> reviewsFuture =
CompletableFuture.supplyAsync(() -> reviewClient.getReviews(productId));
CompletableFuture<List<Product>> recommendationsFuture =
CompletableFuture.supplyAsync(() -> recommendationClient.getRecommendations(productId));
// 모든 결과 조합
try {
CompletableFuture.allOf(
inventoryFuture, pricingFuture, reviewsFuture, recommendationsFuture
).get(5, TimeUnit.SECONDS);
ProductDetailResponse response = ProductDetailResponse.builder()
.inventory(inventoryFuture.get())
.pricing(pricingFuture.get())
.reviews(reviewsFuture.get())
.recommendations(recommendationsFuture.get())
.build();
log.info("Product detail aggregated in {}ms",
System.currentTimeMillis() - startTime);
return response;
} catch (TimeoutException e) {
log.warn("Timeout while fetching product details");
return getPartialResponse(inventoryFuture, pricingFuture,
reviewsFuture, recommendationsFuture);
} catch (Exception e) {
throw new ServiceException("Failed to aggregate product details", e);
}
}
private ProductDetailResponse getPartialResponse(
CompletableFuture<InventoryInfo> inventoryFuture,
CompletableFuture<PricingInfo> pricingFuture,
CompletableFuture<List<Review>> reviewsFuture,
CompletableFuture<List<Product>> recommendationsFuture) {
return ProductDetailResponse.builder()
.inventory(getOrDefault(inventoryFuture, InventoryInfo.empty()))
.pricing(getOrDefault(pricingFuture, PricingInfo.empty()))
.reviews(getOrDefault(reviewsFuture, List.of()))
.recommendations(getOrDefault(recommendationsFuture, List.of()))
.partial(true)
.build();
}
private <T> T getOrDefault(CompletableFuture<T> future, T defaultValue) {
try {
return future.getNow(defaultValue);
} catch (Exception e) {
return defaultValue;
}
}
}- • 이메일/SMS 발송 - 사용자 응답 지연 방지
- • 외부 API 호출 - 병렬 처리로 응답 시간 단축
- • 대량 데이터 처리 - 배치 작업 비동기화
- • 로깅/감사 - 메인 로직과 분리
- • 알림/푸시 - 실시간 알림 처리
7. 정리 및 베스트 프랙티스
7.1 비동기 설계 가이드
| 상황 | 권장 방식 | 이유 |
|---|---|---|
| 결과 불필요 | void 반환 | Fire-and-forget 패턴 |
| 단일 결과 필요 | CompletableFuture | 결과 조합, 예외 처리 용이 |
| 여러 작업 병렬 | allOf/anyOf | 동시 실행, 결과 집계 |
| 이벤트 기반 | @EventListener + @Async | 느슨한 결합, 확장성 |
| 대량 처리 | 청크 분할 + 병렬 | 메모리 효율, 처리량 향상 |
7.2 ThreadPool 설정 가이드
용도별 ThreadPool 설정
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
// CPU 바운드 작업용 (계산 집약적)
@Bean("cpuExecutor")
public Executor cpuBoundExecutor() {
int coreCount = Runtime.getRuntime().availableProcessors();
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(coreCount);
executor.setMaxPoolSize(coreCount);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("cpu-");
executor.initialize();
return executor;
}
// I/O 바운드 작업용 (네트워크, 파일)
@Bean("ioExecutor")
public Executor ioBoundExecutor() {
int coreCount = Runtime.getRuntime().availableProcessors();
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(coreCount * 2);
executor.setMaxPoolSize(coreCount * 4);
executor.setQueueCapacity(500);
executor.setThreadNamePrefix("io-");
executor.setRejectedExecutionHandler(new CallerRunsPolicy());
executor.initialize();
return executor;
}
// 이메일 발송용 (제한된 동시성)
@Bean("emailExecutor")
public Executor emailExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(1000);
executor.setThreadNamePrefix("email-");
executor.initialize();
return executor;
}
// 이벤트 처리용
@Bean("eventExecutor")
public Executor eventExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(200);
executor.setThreadNamePrefix("event-");
executor.initialize();
return executor;
}
}| 설정 | CPU 바운드 | I/O 바운드 |
|---|---|---|
| corePoolSize | CPU 코어 수 | CPU 코어 수 × 2 |
| maxPoolSize | CPU 코어 수 | CPU 코어 수 × 4 |
| queueCapacity | 작게 (100) | 크게 (500+) |
7.3 주의사항
- 같은 클래스 내부 호출: @Async가 동작하지 않음 (프록시 우회)
- 트랜잭션: 비동기 메서드는 별도 트랜잭션에서 실행
- 예외 처리: void 반환 시 예외가 무시될 수 있음
- SecurityContext: 기본적으로 전파되지 않음
- ThreadLocal: 새 스레드에서 접근 불가
문제 해결 코드
// 1. 같은 클래스 내부 호출 문제 해결
@Service
@RequiredArgsConstructor
public class OrderService {
private final AsyncOrderService asyncOrderService; // 별도 빈 주입
public void processOrder(Order order) {
// 내부 호출 대신 별도 빈 사용
asyncOrderService.sendNotificationAsync(order);
}
}
@Service
public class AsyncOrderService {
@Async
public void sendNotificationAsync(Order order) {
// 비동기 처리
}
}
// 2. SecurityContext 전파
@Configuration
@EnableAsync
public class AsyncSecurityConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.initialize();
// SecurityContext 전파 설정
return new DelegatingSecurityContextAsyncTaskExecutor(executor);
}
}
// 3. 트랜잭션 이벤트 리스너
@Component
public class OrderEventListener {
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleOrderCreated(OrderCreatedEvent event) {
// 트랜잭션 커밋 후 비동기 실행
}
}7.4 모니터링
ThreadPool 모니터링
@Component
@RequiredArgsConstructor
@Slf4j
public class ThreadPoolMonitor {
private final Map<String, ThreadPoolTaskExecutor> executors;
private final MeterRegistry meterRegistry;
@Scheduled(fixedRate = 60000)
public void logThreadPoolStatus() {
executors.forEach((name, executor) -> {
int activeCount = executor.getActiveCount();
int poolSize = executor.getPoolSize();
int queueSize = executor.getThreadPoolExecutor().getQueue().size();
log.info("ThreadPool [{}] - Active: {}, Pool: {}, Queue: {}",
name, activeCount, poolSize, queueSize);
// 메트릭 기록
meterRegistry.gauge("threadpool.active",
Tags.of("name", name), activeCount);
meterRegistry.gauge("threadpool.queue.size",
Tags.of("name", name), queueSize);
});
}
}7.5 체크리스트
- ☐ @EnableAsync 설정 확인
- ☐ 용도별 ThreadPool 분리
- ☐ 적절한 풀 사이즈 설정
- ☐ 예외 처리 핸들러 구현
- ☐ 타임아웃 설정
- ☐ 모니터링 구성
- ☐ 테스트 코드 작성
- 비동기는 응답 시간 단축과 처리량 향상에 효과적
- CompletableFuture로 결과 조합과 예외 처리
- 용도별 ThreadPool 분리로 리소스 격리
- 이벤트 기반 아키텍처로 느슨한 결합
- 모니터링으로 병목 지점 파악