[Spring] @Async를 활용한 비동기 처리
ㅁ 들어가며
내가 운영하는 서비스에 Request Timeout Exception이 발생하였다. 이 서비스는 전체 서비스의 중간 모듈이기 때문에 전체 서비스의 품질을 위해서는 Timeout의 시간을 2초를 넘길 수 없었다. 이를 해결하기 위해 전체 서비스에 장애로 전파될 수 있는 로직을 비동기로 처리하기로 하였다.
Spring Framework에서 제공하는 @Async 어노테이션은 메서드를 비동기적으로 실행할 수 있게 해주는 강력한 기능이다. 이를 통해 애플리케이션의 성능을 향상시키고 리소스를 효율적으로 사용할 수 있다.
ㅁ Webflux 비동처 처리 방식에 대한 나의 경험
@PostMapping("/1.0/noti")
public Mono<Object> Notification(
ServerHttpRequest request,
ServerHttpResponse response,
@RequestBody Noti notiBp
) {
notiService.webhook_deliver(request, response, notiBp).flatMap(n_res ->{
log.info("webhookRcv deliver. subscribe is done. n_res={}, brandId={}", n_res, notiBp.getBrandId());
return Mono.empty();
}).subscribe();
response.setStatusCode(HttpStatus.OK);
return Mono.just("Success");
}
ㅇ 위의 코드는 예전에 Webflux 형태로 작성했던 비동기 코드이다.
ㅇ 외부 연동된 서비스와의 동기화 처리를 위해 만든 로직으로 Webhook을 받아 처리한다.
ㅇ Webhook 시 동기화가 필요한 데이터의 Key만 전달 받고, 갱신 처리 시 레거시 쪽에 상세 데이터를 요청하여 내부 데이터를 갱신한다.
ㅇ 그런데 동기화 데이터가 많으면 처리 지연이 발생하고, 레거시 쪽에서 Timeout으로 실패처리된다.
ㅇ 재발송처리 로직이 있다면 여러 차례 반복적으로 같은 지연 현상이 발생할 수 있다.
ㅇ 동기화 요청에 대해서는 바로 Success로 응답을 주고, webhook_deliver 메서드는 다른 스레드에서 처리하도록 변경하였다.
ㅇ WebFlux는 높은 확장성과 성능이 필요한 복잡한 비동기 시나리오에 더 적합하고, @Async는 간단한 비동기 작업에 적합하고 구현이 쉬게 구성되어 있다.
ㅇ @Async와 WebFlux의 장단점을 표로 작성해 보았다.
ㅁ Webflux와 @Async 비동기 처리 방식 비교
WebFlux | @Async | |
프로그래밍 모델 | 리액티브 프로그래밍 모델을 사용 | 전통적인 명령형 프로그래밍 모델을 사용 |
동시성 처리 | 이벤트 루프 기반의 논블로킹 I/O를 사용 | 스레드 풀을 사용하여 작업을 비동기적으로 실행 |
확장성 | 적은 수의 스레드로 많은 동시 요청 처리 가능 | 스레드 풀 크기에 따라 제한됨 |
백프레셔 처리 | 내장된 백프레셔 메커니즘 제공 | 기본적으로 지원하지 않음 |
학습 곡선 | 리액티브 프로그래밍에 대한 이해 필요 | 비교적 간단하고 익숙한 방식 |
적합한 사용 사례 | 대량의 동시 연결, 실시간 데이터 스트리밍에 적합 | 단일 비동기 작업에 적합 |
성능 | 높은 동시성 환경에서 더 나은 성능 제공 | 중간 규모의 동시성에 적합 |
Backpressure?
리액티브 프로그래밍에서의 Backpressure은 Publisher가 끊임없이 emit하는 무수히 많은 데이터를 적절하게 제어하여 데이터 처리에 과부하가 걸리지 않도록 제어하는 것이다. 너무 많은 비동기 요청이 발생할 경우 대기하기 위한 전략으로 Exception을 발생, Drop 전략, Lastest 전략, Buffer 전략이 있다. 자세한 것은 https://devfunny.tistory.com/914 참조
ㅁ @Async 기본 설정
@EnableAsync 어노테이션 추가
@EnableAsync
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
애플리케이션 클래스나 설정 클래스에 @EnableAsync를 추가해 비동기 기능을 활성화한다.
@Service
public class EmailService {
@Async
public void sendEmail(String to, String subject, String content) {
// 이메일 전송 로직
}
}
비동기로 실행할 메서드에 @Async 추가
ㅁ @Async 동작 원리
ㅇ @Async는 Spring AOP를 기반으로 동작한다.
ㅇ 메서드 호출 시 프록시 객체가 생성되어 별도의 스레드에서 해당 메서드를 실행한다.
ㅁ 주의사항
ㅇ public 메서드에만 적용 가능하다.
ㅇ self-invocation(같은 클래스 내 메서드 호출)에서는 동작하지 않는다.
ㅁ 커스텀 Executor 설정
기본적으로 @Async는 SimpleAsyncTaskExecutor를 사용하지만, 성능 향상을 위해 ThreadPoolTaskExecutor를 사용하는 것이 좋다.
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(25);
executor.setThreadNamePrefix("PetericaAsync-");
executor.initialize();
return executor;
}
}
ㅁ 반환값 처리
void가 아닌 반환값이 필요한 경우 Future나 CompletableFuture를 사용할 수 있다.
@Async
public CompletableFuture<String> asyncMethodWithReturnType() {
// 비동기 작업 수행
return CompletableFuture.completedFuture("작업 완료");
}
ㅁ 예외 처리
@Async 메서드에서 발생한 예외는 별도로 처리해야 한다. AsyncUncaughtExceptionHandler를 구현하여 예외 처리 로직을 정의할 수 있다. 리턴 타입이 Future이 아닌 Void이면 예외가 호출 메서드까지 전파가 되지 않는다. 그래서 AsyncUncaughtExceptionHandler를 구현한 클래스를 생성하고 AsyncConfigurer 인터페이스의 getAsyncUncaughtExceptionHandler 메서드를 오버라이딩 해주어야 한다.
ㅁ 마무리
@Async를 활용한 비동기 처리는 Spring 애플리케이션의 성능을 크게 향상시킬 수 있다. 하지만 올바른 설정과 사용 방법을 숙지해야 하며, 과도한 사용은 오히려 성능 저하를 초래할 수 있으므로 주의가 필요하다. 적절한 상황에서 @Async를 활용하면 효율적이고 반응성 높은 애플리케이션을 구현할 수 있다.
ㅁ 함께 보면 좋은 사이트
ㅇ [Spring] @Async를 이용한 비동기 처리에 대해