Spring Boot에서 @Async는 내부적으로 어떻게 작동하나요?
Source: Dev.to
소개 🚀
REST API를 호출하면서 다음과 같이 생각해 본 적이 있나요:
“왜 이 요청은 모든 작업이 끝날 때까지 차단되는 걸까?”
이제 이메일을 보내거나, 보고서를 생성하거나, 느린 서드‑파티 서비스를 호출한다고 상상해 보세요 — 사용자가 정말 기다려야 할까요?
바로 여기서 Spring Boot의 @Async 가 구원군이 됩니다.
간단히 말해, @Async는 Spring에게 이렇게 말하게 합니다:
“당신은 진행하고, 나는 이 작업을 백그라운드에서 처리할게.”
이 간단한 어노테이션 뒤에서는 Spring Boot가 프록시, 스레드 풀, 그리고 실행기를 사용해 코드를 비동기적으로 실행합니다 — 그리고 내부적으로 어떻게 동작하는지 이해하면 프로덕션 버그와 면접 함정을 피할 수 있습니다.
이 블로그에서 배우게 될 내용:
@Async가 실제로 배경에서 하는 일- Spring Boot가 비동기 메서드를 실행하는 방식
- 두 개의 완전한, 엔드‑투‑엔드 Java 21 예제
- 흔히 저지르는 실수와 모범 사례
핵심 개념 🧠
Spring Boot에서 @Async란?
@Async는 메서드가 다음을 할 수 있게 하는 Spring 애노테이션입니다:
- 별도의 스레드에서 실행
- 호출자에게 즉시 반환
- **
TaskExecutor**를 사용해 비동기적으로 로직을 실행
온라인으로 음식을 주문하는 것에 비유하면 🍔:
- 주문을 합니다 (API 호출)
- 레스토랑이 백그라운드에서 준비합니다
- 당신은 카운터에서 기다릴 필요가 없습니다
@Async가 내부적으로 작동하는 방식 (간단 설명)
내부적으로 Spring Boot는 **Spring AOP(프록시 기반 메커니즘)**을 사용합니다.
- Spring은 빈에 대한 프록시를 생성합니다.
@Async메서드가 호출되면 프록시가 호출을 가로챕니다.- 프록시는 메서드를 **
TaskExecutor**에 제출합니다. - 실행자는 메서드를 별도의 스레드에서 실행합니다.
- 메인 스레드는 즉시 계속됩니다.
핵심 요점:
@Async는 다른 Spring 관리 빈에서 메서드가 호출될 때만 작동합니다.
사용 사례 및 장점
✅ 최적 사용 사례
- 이메일 전송
- 느린 외부 API 호출
- 백그라운드 처리
- 이벤트 처리
- 감사 로그
🎯 장점
- 더 빠른 API 응답
- 향상된 사용자 경험
- 더 깔끔한 관심사 분리
End‑to‑End Setup (Java 21 + Spring Boot) ⚙️
우리는 다음을 구축합니다:
- REST API
- 비동기 서비스
- 커스텀 스레드 풀
- cURL 요청 + 응답
Example 1: Basic @Async Execution
1️⃣ Enable Async Support
@Configuration
@EnableAsync
public class AsyncConfig {
}
Spring에게
@Async메서드를 찾도록 알려줍니다.
2️⃣ Async Service
@Service
public class NotificationService {
@Async
public void sendNotification() throws InterruptedException {
// Simulate long‑running task
Thread.sleep(3000);
System.out.println("Notification sent by thread: " +
Thread.currentThread().getName());
}
}
3️⃣ REST Controller
@RestController
@RequestMapping("/api")
public class NotificationController {
private final NotificationService service;
public NotificationController(NotificationService service) {
this.service = service;
}
@GetMapping("/notify")
public String triggerNotification() throws InterruptedException {
service.sendNotification();
return "Request accepted. Processing asynchronously.";
}
}
4️⃣ cURL Request
curl -X GET http://localhost:8080/api/notify
✅ Response
Request accepted. Processing asynchronously.
API가 즉시 응답을 반환하고, 작업은 백그라운드에서 실행됩니다.
Example 2: @Async with CompletableFuture (Recommended)
Why use this?
- 더 나은 제어
- 논블로킹 결과 처리
- 깔끔한 비동기 조합
1️⃣ Async Service with Return Value
@Service
public class ReportService {
@Async
public CompletableFuture<String> generateReport() throws InterruptedException {
Thread.sleep(2000);
return CompletableFuture.completedFuture("Report generated successfully");
}
}
2️⃣ REST Controller
@RestController
@RequestMapping("/api")
public class ReportController {
private final ReportService service;
public ReportController(ReportService service) {
this.service = service;
}
@GetMapping("/report")
public CompletableFuture<String> generate() throws InterruptedException {
return service.generateReport();
}
}
3️⃣ cURL Request
curl -X GET http://localhost:8080/api/report
✅ Response
Report generated successfully
요청 스레드는 일찍 해제되고, 작업은 비동기로 실행됩니다.
Custom Thread Pool (Highly Recommended) 🧵
@Configuration
@EnableAsync
public class AsyncExecutorConfig {
@Bean
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("async-worker-");
executor.initialize();
return executor;
}
}
이 설정이 없으면 Spring은
SimpleAsyncTaskExecutor로 폴백하게 되며, 이는 프로덕션 환경에 적합하지 않습니다.
모범 사례 ✅
- 같은 클래스 안에서
@Async를 호출하지 마세요 – 자체 호출은 Spring 프록시를 우회합니다. - 항상 사용자 정의 executor를 정의하세요 – 무제한 스레드 생성을 방지합니다.
- 반환값으로
CompletableFuture를 사용하세요 – 비동기 처리와 조합을 더 잘 지원합니다. - 비동기 로직을 멱등성 및 예외 안전하게 유지하고; 비동기 메서드 내부에서 실패를 처리하세요.
- 프로덕션 환경에서 스레드 풀 메트릭(활성 스레드, 대기열 크기)을 모니터링하세요.
Common Mistakes ❌
- 잊어버림
@EnableAsync - 예상 private 메서드에서 비동기 동작
- 차단 무거운 로직으로 비동기 스레드
- 사용 CPU‑무거운 작업에
@Async - 가정 트랜잭션이 자동으로 전파된다고
Tips
- 예외를 명시적으로 처리하세요 – async 예외는 자동으로 전파되지 않습니다.
- 비동기 로직을 가볍게 유지하세요 –
@Async는 메시지 큐를 대체하는 것이 아닙니다.
Conclusion 🧩
@Async in Spring Boot looks simple, but internally it relies on:
- Spring AOP proxies
- Task executors
- Thread pools
Understanding these internals helps you:
- Avoid subtle bugs
- Write scalable applications
- Impress interviewers 😉
행동 촉구 📣
💬 @Async에 대한 질문이나 겪은 비동기 버그가 있나요?
🧠 아래에 댓글을 달아 주세요 — 함께 이야기해요!
⭐ 더 많은 Java 프로그래밍 및 Spring Boot 내부 콘텐츠를 원한다면 팔로우하세요.