Spring Boot에서 @Async는 내부적으로 어떻게 작동하나요?

발행: (2026년 1월 10일 오후 03:10 GMT+9)
8 min read
원문: Dev.to

Source: Dev.to

소개 🚀

REST API를 호출하면서 다음과 같이 생각해 본 적이 있나요:

“왜 이 요청은 모든 작업이 끝날 때까지 차단되는 걸까?”

이제 이메일을 보내거나, 보고서를 생성하거나, 느린 서드‑파티 서비스를 호출한다고 상상해 보세요 — 사용자가 정말 기다려야 할까요?

바로 여기서 Spring Boot의 @Async 가 구원군이 됩니다.

간단히 말해, @Async는 Spring에게 이렇게 말하게 합니다:

“당신은 진행하고, 나는 이 작업을 백그라운드에서 처리할게.”

이 간단한 어노테이션 뒤에서는 Spring Boot가 프록시, 스레드 풀, 그리고 실행기를 사용해 코드를 비동기적으로 실행합니다 — 그리고 내부적으로 어떻게 동작하는지 이해하면 프로덕션 버그와 면접 함정을 피할 수 있습니다.

이 블로그에서 배우게 될 내용:

  • @Async가 실제로 배경에서 하는 일
  • Spring Boot가 비동기 메서드를 실행하는 방식
  • 두 개의 완전한, 엔드‑투‑엔드 Java 21 예제
  • 흔히 저지르는 실수와 모범 사례

핵심 개념 🧠

Spring Boot에서 @Async란?

@Async는 메서드가 다음을 할 수 있게 하는 Spring 애노테이션입니다:

  • 별도의 스레드에서 실행
  • 호출자에게 즉시 반환
  • **TaskExecutor**를 사용해 비동기적으로 로직을 실행

온라인으로 음식을 주문하는 것에 비유하면 🍔:

  1. 주문을 합니다 (API 호출)
  2. 레스토랑이 백그라운드에서 준비합니다
  3. 당신은 카운터에서 기다릴 필요가 없습니다

@Async가 내부적으로 작동하는 방식 (간단 설명)

내부적으로 Spring Boot는 **Spring AOP(프록시 기반 메커니즘)**을 사용합니다.

  1. Spring은 빈에 대한 프록시를 생성합니다.
  2. @Async 메서드가 호출되면 프록시가 호출을 가로챕니다.
  3. 프록시는 메서드를 **TaskExecutor**에 제출합니다.
  4. 실행자는 메서드를 별도의 스레드에서 실행합니다.
  5. 메인 스레드는 즉시 계속됩니다.

핵심 요점: @Async다른 Spring 관리 빈에서 메서드가 호출될 때만 작동합니다.

사용 사례 및 장점

✅ 최적 사용 사례

  • 이메일 전송
  • 느린 외부 API 호출
  • 백그라운드 처리
  • 이벤트 처리
  • 감사 로그

🎯 장점

  • 더 빠른 API 응답
  • 향상된 사용자 경험
  • 더 깔끔한 관심사 분리

End‑to‑End Setup (Java 21 + Spring Boot) ⚙️

우리는 다음을 구축합니다:

  • REST API
  • 비동기 서비스
  • 커스텀 스레드 풀
  • cURL 요청 + 응답

Example 1: Basic @Async Execution

1️⃣ Enable Async Support

@Configuration
@EnableAsync
public class AsyncConfig {
}

Spring에게 @Async 메서드를 찾도록 알려줍니다.

2️⃣ Async Service

@Service
public class NotificationService {

    @Async
    public void sendNotification() throws InterruptedException {
        // Simulate long‑running task
        Thread.sleep(3000);
        System.out.println("Notification sent by thread: " +
                           Thread.currentThread().getName());
    }
}

3️⃣ REST Controller

@RestController
@RequestMapping("/api")
public class NotificationController {

    private final NotificationService service;

    public NotificationController(NotificationService service) {
        this.service = service;
    }

    @GetMapping("/notify")
    public String triggerNotification() throws InterruptedException {
        service.sendNotification();
        return "Request accepted. Processing asynchronously.";
    }
}

4️⃣ cURL Request

curl -X GET http://localhost:8080/api/notify

✅ Response

Request accepted. Processing asynchronously.

API가 즉시 응답을 반환하고, 작업은 백그라운드에서 실행됩니다.

Why use this?

  • 더 나은 제어
  • 논블로킹 결과 처리
  • 깔끔한 비동기 조합

1️⃣ Async Service with Return Value

@Service
public class ReportService {

    @Async
    public CompletableFuture<String> generateReport() throws InterruptedException {
        Thread.sleep(2000);
        return CompletableFuture.completedFuture("Report generated successfully");
    }
}

2️⃣ REST Controller

@RestController
@RequestMapping("/api")
public class ReportController {

    private final ReportService service;

    public ReportController(ReportService service) {
        this.service = service;
    }

    @GetMapping("/report")
    public CompletableFuture<String> generate() throws InterruptedException {
        return service.generateReport();
    }
}

3️⃣ cURL Request

curl -X GET http://localhost:8080/api/report

✅ Response

Report generated successfully

요청 스레드는 일찍 해제되고, 작업은 비동기로 실행됩니다.

@Configuration
@EnableAsync
public class AsyncExecutorConfig {

    @Bean
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("async-worker-");
        executor.initialize();
        return executor;
    }
}

이 설정이 없으면 Spring은 SimpleAsyncTaskExecutor 로 폴백하게 되며, 이는 프로덕션 환경에 적합하지 않습니다.

모범 사례 ✅

  • 같은 클래스 안에서 @Async를 호출하지 마세요 – 자체 호출은 Spring 프록시를 우회합니다.
  • 항상 사용자 정의 executor를 정의하세요 – 무제한 스레드 생성을 방지합니다.
  • 반환값으로 CompletableFuture를 사용하세요 – 비동기 처리와 조합을 더 잘 지원합니다.
  • 비동기 로직을 멱등성예외 안전하게 유지하고; 비동기 메서드 내부에서 실패를 처리하세요.
  • 프로덕션 환경에서 스레드 풀 메트릭(활성 스레드, 대기열 크기)을 모니터링하세요.

Common Mistakes ❌

  • 잊어버림 @EnableAsync
  • 예상 private 메서드에서 비동기 동작
  • 차단 무거운 로직으로 비동기 스레드
  • 사용 CPU‑무거운 작업에 @Async
  • 가정 트랜잭션이 자동으로 전파된다고

Tips

  • 예외를 명시적으로 처리하세요 – async 예외는 자동으로 전파되지 않습니다.
  • 비동기 로직을 가볍게 유지하세요@Async는 메시지 큐를 대체하는 것이 아닙니다.

Conclusion 🧩

@Async in Spring Boot looks simple, but internally it relies on:

  • Spring AOP proxies
  • Task executors
  • Thread pools

Understanding these internals helps you:

  • Avoid subtle bugs
  • Write scalable applications
  • Impress interviewers 😉

행동 촉구 📣

💬 @Async에 대한 질문이나 겪은 비동기 버그가 있나요?

🧠 아래에 댓글을 달아 주세요 — 함께 이야기해요!

⭐ 더 많은 Java 프로그래밍Spring Boot 내부 콘텐츠를 원한다면 팔로우하세요.

Back to Blog

관련 글

더 보기 »