Spring Boot 비동기 작업 처리: @EnableAsync와 Future 패턴 적용

Spring Boot는 비동기 작업을 매우 효율적으로 지원합니다. 이번 글에서는 @EnableAsync@Async 어노테이션을 활용하여 비동기 기능을 활성화하고, 단방향(Fire-and-Forget) 패턴과 요청-응답(Request-Reply) 패턴을 구현하는 방법을 살펴봅니다.

비동기 기능 활성화

Spring Boot 환경에서 비동기 처리를 사용하려면 먼저 설정 클래스에 @EnableAsync 어노테이션을 추가하여 비동기 지원을 활성화해야 합니다.

@Configuration
@EnableAsync
public class AsyncThreadPoolConfig {
    // 비동기 스레드 풀 관련 추가 설정이 필요한 경우 이곳에 구현
}

이제 비즈니스 로직이 포함된 메서드에 @Async 어노테이션을 부착하기만 하면, 해당 메서드는 별도의 스레드에서 비동기적으로 실행됩니다.

단방향 메시지 전송 (Fire-and-Forget 패턴)

여러 서비스 호출 간에 논리적 의존성이 없고, 실행 순서가 중요하지 않은 경우 병렬 처리가 유리합니다. 단방향 서비스는 요청만 존재하고 응답을 기다리지 않으므로 비동기로 설계하기 매우 적합합니다. 호출을 시작하면 메인 스레드는 블록되지 않고 즉시 다음 로직으로 진행합니다.

@Service
public class NotificationService {

    @Async
    public void dispatchEmailNotification(String recipient) {
        System.out.println("이메일 전송 시작: " + recipient);
        long start = System.currentTimeMillis();
        try {
            Thread.sleep(1500); // 네트워크 지연 시뮬레이션
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        long end = System.currentTimeMillis();
        System.out.println("이메일 전송 완료. 소요 시간: " + (end - start) + "ms");
    }

    @Async
    public void dispatchSmsNotification(String phoneNumber) {
        System.out.println("SMS 전송 시작: " + phoneNumber);
        long start = System.currentTimeMillis();
        try {
            Thread.sleep(1500);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        long end = System.currentTimeMillis();
        System.out.println("SMS 전송 완료. 소요 시간: " + (end - start) + "ms");
    }
}

아래 테스트 코드를 실행하면, 비동기 메서드가 호출된 직후 메인 스레드가 대기하지 않고 즉시 반환되는 것을 확인할 수 있습니다.

@SpringBootTest
public class NotificationServiceTest {

    @Autowired
    private NotificationService notificationService;

    @Test
    public void testFireAndForget() {
        notificationService.dispatchEmailNotification("user@example.com");
        notificationService.dispatchSmsNotification("010-1234-5678");
        System.out.println("메인 스레드: 비동기 호출 후 즉시 반환됨");
    }
}

요청-응답 및 결과 집계 (Future 패턴)

비동기로 요청을 보내더라도, 여러 작업의 결과가 모두 취합된 이후에 다음 단계로 넘어가야 하는 경우가 있습니다. 이때는 Java의 CompletableFuture를 활용하여 각 작업의 완료 시점을 추적하고 결과를 수집할 수 있습니다.

@Service
public class DataAggregationService {

    @Async
    public CompletableFuture<Integer> fetchUserStatistics() {
        System.out.println("사용자 통계 데이터 수집 중...");
        simulateDelay(2000);
        return CompletableFuture.completedFuture(150);
    }

    @Async
    public CompletableFuture<Integer> fetchSalesStatistics() {
        System.out.println("판매 통계 데이터 수집 중...");
        simulateDelay(2000);
        return CompletableFuture.completedFuture(320);
    }

    private void simulateDelay(int millis) {
        long start = System.currentTimeMillis();
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        long end = System.currentTimeMillis();
        System.out.println("데이터 수집 소요 시간: " + (end - start) + "ms");
    }
}

다음 테스트는 두 개의 비동기 작업이 모두 완료될 때까지 기다린 뒤, 최종 결과를 집계하고 전체 소요 시간을 측정합니다. 기존 Future의 무한 루프 대기 방식 대신 CompletableFuture.allOf()를 사용하여 더욱 깔끔하게 동기화 포인트를 제어합니다.

@SpringBootTest
public class DataAggregationServiceTest {

    @Autowired
    private DataAggregationService aggregationService;

    @Test
    public void testRequestReplyWithFuture() throws Exception {
        long startTime = System.currentTimeMillis();

        CompletableFuture<Integer> userStats = aggregationService.fetchUserStatistics();
        CompletableFuture<Integer> salesStats = aggregationService.fetchSalesStatistics();

        // 두 작업이 모두 완료될 때까지 대기
        CompletableFuture.allOf(userStats, salesStats).join();

        long endTime = System.currentTimeMillis();
        int totalUsers = userStats.get();
        int totalSales = salesStats.get();
        
        System.out.println("통계 총합: " + (totalUsers + totalSales));
        System.out.println("전체 작업 소요 시간: " + (endTime - startTime) + "ms");
    }
}

태그: SpringBoot java Asynchronous EnableAsync CompletableFuture

6월 11일 16:34에 게시됨