애너테이숀
- @Async 애너테이션의 동작원리
- 메서드 및 클래스 레벨에서 @Async 적용법
- @Async 반환타입별 동작 차이
- 비동기 코드, 테스트
- 비동기 메서드의 실행 결과를 관리하는 다양한 방법
@Async 애너테이션 활용
Spring은 @Async 애너테이션을 통해 매우 간단하게 비동기 처리를 선언적으로(Declarative)으로 구현할 수 있음
@EnableAsync가 활성화된 상태에서 @Async가 붙은 메서드는 별도의 스레드에서 실행되며, 호출자는 즉시 반환됨
1. 동작원리
@Async는 내부적으로 AOP기반 프록시(Proxy)를 활용함
즉, 비동기 메서드를 호출할 때 프록시가 가로채서 해당 메서드를 TaskExecutor 스레드풀에 위임함
@Async는 프록시 객체가 비동기 실행 로직을 대신 처리하고, 호출자는 메인 스레드로 바로 반환됨
2. 메서드 레벨 적용
@Async는 보통 Service 계층 메서드에 적용한다.
@Service
public class NotificationService {
@Async // 애너테이션
public void sendEmail(String to) {
System.out.println(Thread.currentThread().getName() + "이메일 전송중");
try {
Thread.sleep(2000);
} catch(InterruptedException e) {
e.PrintStackTrace();
}
System.out.println("전송 완료");
}
}
컨트롤러
@RestController
@RequiredArgsConstructor
public class NotificationController {
private final NotificationService service;
@GetMapping("/notify")
public String notifyUser() {
service.sendEmail("user.example.com");
return "요청이 접수되었습니다."; // 즉시 응답처리
}
}
3. 클래스 레벨 적용
놀랍게도 @Async는 클래스에도 선언할 수 있다.
이 경우 해당 클래스에 있는 모든 public 메서드가 비동기로 동작함
@Async // 가능
@Service
public class BulkService {
// 대충 퍼블릭 메서드들
}
* 실무에서는 특정 메서드 단위로 비동기를 지정하는 경우가 많음
왜냐하면, 클래스 전체에 비동기를 적용하면 예기치 않은 병렬 실행이 발생할 수 있기 때문
4. 지원 반환 타입
@Async에서 지원하는 세가지 주요 반환 타입
| 반환타입 | 설명 | 예시 |
| void | 단순 비동기 실행, 결과가 필요없는 경우 | 알림, 로그, 통계전송 등 |
| Future<?> 또는 ListenableFuture<?> |
비동기 결과를 나중에 조회가능 | 오래 걸리는 계산이나 외부 API 요청 |
| CompletableFuture<?> | 비동기 완료 후 후속 작업(thenApply 등) 수행 가능 | 체이닝, 비동기 결합 처리 |
1) void 반환 타입
가장 단순한 형태, 결과를 반환하지 않고 비동기처리
@Async
public void writeLog(String message) { // 반환타입 void
// 대충 로그
}
컨트롤러는 즉시 응답하고, 로그는 백그라운드 스레드에서 따로 처리된다
2) Future<?> 반환타입
Future를 반환하면 호출측에서 결과를 명시적으로 get()으로 가져올 수 있다고 함
@Async
public Future<String> fetchData() { // Future<?>
try {
Thread.sleep(2000);
} catch(InterruptedException e) {
e.printStackTrace();
}
return new AsyncResult<>("데이터 로드 완료");
}
// 컨트롤러에서
Future<String> result = service.fetchData();
System.out.println(result.get()); // 블로킹 방식으로 결과를 대기한다고 함
Future는 결과를 동기적으로 가져올 수 있지만, 완료 여부를 직접 확인해야한다고 한다.
3) CompletableFuture<?> 반환 타입
CompletableFuture는 비동기 결과를 체이닝 방식으로 다룰 수 있어서 실무에서 가장 많이 사용된다고 한다.
@Async
public CompletableFuture<String> loadData() {
try {
Thread.sleep(1000);
} catch(InterruptedException e) {
e.printStackTrace();
}
return CompletableFuture.completedFuture("데이터 로드 완료");
}
public void execute() {
service.loadData().thenApply(data -> data + "처리완료").thenAccept(System.out::println);
}
비동기 완료 후 thenApply, thenAccept 등을 통해 후속 작업을 자연스럽게 연결할 수 있음
위 예시의 경우, 같은 클래스 내부에서 loadData()를 호출하는 것으로 보일 수 있기 때문에 실제로 비동기 처리는 되지 않는다.
Self-Invocation 문제가 발생하기 때문
비동기로 호출시키려면 별도의 Bean에서 호출하거나, 프록시를 통해 호출해야만 한다.
밑에는 그 예시
@Service
public class AsyncWorker {
@Async
public CompletableFuture<String> loadData() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return CompletableFuture.completedFuture("데이터 로드 완료");
}
}
@Service
@RequiredArgsConstructor
public class DataService {
private final AsyncWorker asyncWorker;
public void execute() {
asyncWorker.loadData()
.thenApply(data -> data + " 처리 완료")
.thenAccept(System.out::println);
}
}
팁
@Async 활용시 주의사항
- 반드시 @EnableAsync가 선언되어 있어야만 @Async가 동작한다
- 자기 자신(this)의 내부 호출은 프록시를 거치지 않기 때문에 비동기로 동작하지 않는다
- 스레드풀(TaskExecutor)을 지정하지 않으면, SimpleAsyncTaskExecutor가 기본 사용된다
- CompletableFuture를 활용하면, 비동기 결과를 체이닝 형태로 처리할 수 있다
- 반환 타입이 void인 경우, 예외는 AsyncUncaughtExceptionHandler에서 처리해야한다
정리
| 구분 | 내용 |
| 핵심 애너테이션 | @Async, 비동기 실행 |
| 활성화 애너테이션 | @EnableAsync |
| 적용 위치 | 메서드 또는 클래스 레벨(메서드 권장) |
| 주요 반환타입 | void, Future<?>, CompletableFuture<?> |
| 실무 권장타입 | CompletableFuture<?>, 체이닝 및 예외 처리 용이 |
| 스레드 관리방식 | TaskExecutor 기반 스레드풀 실행 |
'Spring Boot > 비동기 처리' 카테고리의 다른 글
| 비동기 처리 : 비동기 처리 방식 (1) | 2025.11.02 |
|---|---|
| 비동기 처리 : 비동기 처리를 왜 해야하는가 (0) | 2025.11.01 |
| Spring Event : 분산 환경으로의 확장 가능성 (0) | 2025.10.29 |
| Spring Event : 이벤트 기반 비동기 처리 활용사례 (0) | 2025.10.29 |
| Spring Event : 비동기 이벤트 처리 구현 (0) | 2025.10.25 |