본문 바로가기

Spring Boot/비동기 처리

비동기 처리 : 비동기 처리 방식

@EnableAsync, TaskExecutor, 크아악

 

 - Spring 비동기 처리의 내부 구조, 동작 원리

 - @EnableAsync, 설정법에 대해

 - TaskExecutor와 스레드 관리 구조 이해

 - Spring이 비동기 메서드를 어떤 방식으로 실행하는가

 - 테스트

 


비동기 처리 방식

@Async 애너테이션을 기반으로 한 고수준 비동기 처리 메커니즘

Java에서 제공해주는 CompletableFuture와 달리, Spring은 스레드 관리, 예외 처리, 트랜잭션 분리를 자동으로 지원하여 더욱 쉽게 비동기 로직을 구현할 수 있다.

하지만.. Spring은 프레임워크다. 잊지말자. Spring을 벗겨내면 결국 Java의 CompletableFuture 등을 통해서도 구현 가능함

 

 

1. 비동기 처리 아키텍처

Spring에서 비동기 처리를 활성화하면, 내부적으로 프록시(Proxy) 객체가 생성되어 비동기 메서드를 별도의 스레드에서 실행함

@Async 애너테이션이 붙은 메서드가 호출될때의 전체 흐름

 

클라이언트의 호출 - > @Async 메서드가 호출됨 - > Spring 프록시가 생성됨 - > TaskExecutor을 전달?

-> 스레드풀 내부에서 비동기 실행 - > 결과를 반환함 : Future/CompletableFuture

 

 

1) ☆핵심 구성 요소☆

@Async 메서드를 비동기로 실행하도록 표시함
@EnableAsync 비동기 기능 활성화(프록시 생성)
TaskExecutor 비동기 스레드를 관리하는 실행기
ThreadPoolTaskExecutor 실무에서 가장 많이 사용하는 스레드풀 구현체
AsyncUncaughtExceptionHandler 비동기 스레드의 예외 처리 담당

 

 

2. @EnableAsync 활성화

비동기 기능을 사용하려면, 먼저 @EnableAsync를 통해 Spring 컨테이너에 비동기 기능을 등록해야함

@Configuration
@EnableAsync // 이걸 통해서 비동기 처리 기능을 활성화한다
public class AsyncConfig {

}

 

@EnableAsync 애너테이션은 내부적으로 AsyncAnnotationBeanPostProcessor를 등록하여, @Async가 붙은 메서드를 프록시로 감싸고 비동기 스레드 풀에 위임할 수 있게 만듬

* @EnableAsync는 SpringBoot에서 자동 활성화되지 않는다. 반드시 명시적으로 추가해줘야함

 

 

3. 비동기 실행의 기본 흐름

@Async가 호출될 때의 내부 동작

 

클라이언트가 컨트롤러에게 요청 전송 - > 컨트롤러가 서비스에서 @Async 메서드를 호출, 이후 프록시 처리로 즉시 반환

이하는 프록시 처리로 즉시 반환하는 과정..이라고 본다, 비동기니까 다른 로직이 수행됨과 동시에 다른 스레드풀에서 실행되니까

- > @Async 서비스가 TaskExecutor에 비동기 작업을 위임함 - > TaskExecutor가 스레드 풀에서 작업을 수행함

- > TaskExecutor가 @Async 서비스에게 작업 완료를 알림

 

* @Async 메서드는 호출 즉시 반환되며, 별도의 스레드에서 실제 작업이 수행됨

* 스레드풀(TaskExecutor)을 통해 병렬로 실행되므로, 메인 스레드는 블로킹되지 않는다.(논블로킹인가?)

 

 

4. 기본 스레드 관리 구조

Spring은 기본적으로 SimpleAsyncTaskExecutor를 사용하지만, 실무에서는 대부분 ThreadPoolTaskExecutor로 커스터마이징해서 사용한다고 한다.

 

기본 TaskExecutor 등록 구조

src\main\java\com.example.async

 - config\AsyncConfig.java

 - service\AsyncService.java

 - controller\AsyncController.java

 

예제

AsyncConfig.java

@Configuration
@EnableAsync
public class AsyncConfig {
	@Bean(name = "taskExecutor")
    public Executor taskExecutor() {
    	ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(4); // 기본 스레드 갯수
        executor.setMaxPoolSize(8); // 최대 스레드의 갯수임
        executor.setQueueCapacity(100); // 대기열의 크기, 큐
        executor.setThreadNamePrefix("AsyncExecutor-"); // 스레드 이름 접두사
        executor.initialize();
        return executor;
    }
}

 

AsyncService.java

@Service
public class AsyncService {
	
    @Async("taskExecutor") // 지정된 Executor를 자용하게끔 함
    public void sendEmail(String to) {
    	System.out.println(Thread.currentThread().getName() + "이메일 발송 중" + to);
        
        try {
        	Thread.sleep(1500); // 처리 시간 시뮬레이션
        } catch(InterruptedException e) {
        	e.PrintStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "이메일 발송 완료" + to);
    }
}

 

AsyncController.java

@RestController
@RequiredArgsConstructor
public class AsyncController {
	private final AsyncService asyncService;
    
    @GetMapping("/email");
    public String sendEmail() {
    	asyncService.sendEmail("b1uffer@example.com");
        return "이메일 접수 완료!"; // 즉시 반환됨
    }
}

 

* 컨트롤러는 즉시 응답을 사용자에게 반환해주고, 비동기 메서드는 별도의 스레드에서 실행된다.

 


비동기 환경 구성할 때 주의사항

 - @EnableAsync를 반드시 추가해줘야 비동기 기능이 활성화된다.

 - @Async는 public 메서드에서만 적용되며, 같은 클래스 내의 자기 호출(Self-invocation)에서는 동작하지 않는다.

 - Executor 설정시, 스레드풀 크기를 과도하게 설정하면 오히려 성능 저하가 발생할 수 있음

 - 예외 처리를 위해 AsyncUncaughtExceptionHandler를 별도로 구성해주는게 좋다고 한다.

 - 트랜잭션 전파가 필요한 경우, 비동기 실행 전에 트랜잭션을 완료해줘야 한다고 한다. 

 


정리

구분 내용
비동기 실행의 원리 프록시 객체가 메서드 호출을 TaskExecutor에 위임해서 별도의 스레드에서 실행함
핵심 설정 @EnableAsync, @Async, TaskExecutor
기본 스레드풀 SimpleAsyncTaskExecutor(기본), ThreadPoolTaskExecutor(실무)
주의사항 자기호출시 비동기 미작동, 예외 전파 불가, 스레드풀 과도 설정 주의