본문 바로가기

Spring Boot/비동기 처리

비동기 처리 : @Async 활용

애너테이숀

 

 - @Async 애너테이션의 동작원리

 - 메서드 및 클래스 레벨에서 @Async 적용법

 - @Async 반환타입별 동작 차이

 - 비동기 코드, 테스트

 - 비동기 메서드의 실행 결과를 관리하는 다양한 방법

 


@Async 애너테이션 활용

Spring은 @Async 애너테이션을 통해 매우 간단하게 비동기 처리를 선언적으로(Declarative)으로 구현할 수 있음

@EnableAsync가 활성화된 상태에서 @Async가 붙은 메서드는 별도의 스레드에서 실행되며, 호출자는 즉시 반환됨

 

 

1. 동작원리

@Async는 내부적으로 AOP기반 프록시(Proxy)를 활용함

즉, 비동기 메서드를 호출할 때 프록시가 가로채서 해당 메서드를 TaskExecutor 스레드풀에 위임함

@Async 동작원리

 

@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 기반 스레드풀 실행