<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Completablefuture on kastori</title><link>http://blog.kastori.dev/tags/completablefuture/</link><description>Recent content in Completablefuture on kastori</description><generator>Hugo -- gohugo.io</generator><language>ko-kr</language><lastBuildDate>Tue, 19 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="http://blog.kastori.dev/tags/completablefuture/index.xml" rel="self" type="application/rss+xml"/><item><title>[Spring 완전 정복 #12] Spring @Async — 비동기 처리와 CompletableFuture 병렬 실행</title><link>http://blog.kastori.dev/tech/2026-05-19-spring-12-async/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>http://blog.kastori.dev/tech/2026-05-19-spring-12-async/</guid><description>&lt;h2 id="이메일-발송을-기다려야-할까"&gt;&lt;a href="#%ec%9d%b4%eb%a9%94%ec%9d%bc-%eb%b0%9c%ec%86%a1%ec%9d%84-%ea%b8%b0%eb%8b%a4%eb%a0%a4%ec%95%bc-%ed%95%a0%ea%b9%8c" class="header-anchor"&gt;&lt;/a&gt;이메일 발송을 기다려야 할까?
&lt;/h2&gt;&lt;p&gt;주문 완료 후 이메일 알림을 보내는 상황을 생각해보자. 이메일 발송 API가 외부 서비스를 호출하고 2초가 걸린다면, 사용자는 주문 완료 응답을 받기까지 2초를 기다려야 할까?&lt;/p&gt;
&lt;p&gt;주문 저장과 이메일 발송은 독립적인 작업이다. 이메일이 성공했는지 실패했는지를 주문 API 응답에 포함할 필요가 없다면 비동기로 처리하는 것이 맞다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;@Async&lt;/code&gt;는 메서드를 &lt;strong&gt;별도 스레드 풀에서 비동기 실행&lt;/strong&gt;한다. 호출자는 메서드 완료를 기다리지 않고 즉시 반환받는다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="기본-설정"&gt;&lt;a href="#%ea%b8%b0%eb%b3%b8-%ec%84%a4%ec%a0%95" class="header-anchor"&gt;&lt;/a&gt;기본 설정
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Configuration&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@EnableAsync&lt;/span&gt; &lt;span style="color:#75715e"&gt;// @Async 활성화&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;AsyncConfig&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Bean&lt;/span&gt;(name &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;mailExecutor&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; TaskExecutor &lt;span style="color:#a6e22e"&gt;mailExecutor&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ThreadPoolTaskExecutor executor &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; ThreadPoolTaskExecutor();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; executor.&lt;span style="color:#a6e22e"&gt;setCorePoolSize&lt;/span&gt;(5); &lt;span style="color:#75715e"&gt;// 항상 유지할 스레드 수&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; executor.&lt;span style="color:#a6e22e"&gt;setMaxPoolSize&lt;/span&gt;(20); &lt;span style="color:#75715e"&gt;// 최대 스레드 수&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; executor.&lt;span style="color:#a6e22e"&gt;setQueueCapacity&lt;/span&gt;(100); &lt;span style="color:#75715e"&gt;// 큐 대기 용량&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; executor.&lt;span style="color:#a6e22e"&gt;setThreadNamePrefix&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;mail-async-&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; executor.&lt;span style="color:#a6e22e"&gt;setRejectedExecutionHandler&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; ThreadPoolExecutor.&lt;span style="color:#a6e22e"&gt;CallerRunsPolicy&lt;/span&gt;()); &lt;span style="color:#75715e"&gt;// 큐 꽉 차면 호출 스레드가 직접 실행&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; executor.&lt;span style="color:#a6e22e"&gt;initialize&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; executor;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;스레드 풀 동작 방식:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;1. 스레드 수 &amp;lt; CorePoolSize → 새 스레드 생성
2. 스레드 수 &amp;gt;= CorePoolSize → 큐에 대기
3. 큐가 꽉 참 → MaxPoolSize까지 스레드 추가 생성
4. MaxPoolSize도 꽉 참 → RejectedExecutionHandler 실행
&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;
&lt;h2 id="async-기본-사용"&gt;&lt;a href="#async-%ea%b8%b0%eb%b3%b8-%ec%82%ac%ec%9a%a9" class="header-anchor"&gt;&lt;/a&gt;@Async 기본 사용
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Service&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@RequiredArgsConstructor&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;NotificationService&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Async&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;mailExecutor&amp;#34;&lt;/span&gt;) &lt;span style="color:#75715e"&gt;// 지정한 스레드 풀 사용&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;sendWelcomeEmail&lt;/span&gt;(String email) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 이 메서드는 별도 스레드에서 실행&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 호출자는 즉시 반환받음&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; emailClient.&lt;span style="color:#a6e22e"&gt;send&lt;/span&gt;(email, &lt;span style="color:#e6db74"&gt;&amp;#34;환영합니다!&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Service&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;UserService&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;registerUser&lt;/span&gt;(UserRequest request) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; User user &lt;span style="color:#f92672"&gt;=&lt;/span&gt; userRepository.&lt;span style="color:#a6e22e"&gt;save&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; User(request));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; notificationService.&lt;span style="color:#a6e22e"&gt;sendWelcomeEmail&lt;/span&gt;(user.&lt;span style="color:#a6e22e"&gt;getEmail&lt;/span&gt;()); &lt;span style="color:#75715e"&gt;// 기다리지 않음&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt;; &lt;span style="color:#75715e"&gt;// 이메일 발송 완료 안 돼도 여기 도달&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="completablefuture--비동기-결과-조합"&gt;&lt;a href="#completablefuture--%eb%b9%84%eb%8f%99%ea%b8%b0-%ea%b2%b0%ea%b3%bc-%ec%a1%b0%ed%95%a9" class="header-anchor"&gt;&lt;/a&gt;CompletableFuture — 비동기 결과 조합
&lt;/h2&gt;&lt;p&gt;대시보드처럼 여러 데이터를 합쳐서 보여줘야 할 때, 각 조회를 병렬로 실행하면 시간을 줄일 수 있다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Service&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;DashboardService&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Async&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; CompletableFuture&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;OrderStats&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;getOrderStats&lt;/span&gt;(Long userId) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; CompletableFuture.&lt;span style="color:#a6e22e"&gt;completedFuture&lt;/span&gt;(orderService.&lt;span style="color:#a6e22e"&gt;getStats&lt;/span&gt;(userId));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Async&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; CompletableFuture&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;PaymentStats&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;getPaymentStats&lt;/span&gt;(Long userId) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; CompletableFuture.&lt;span style="color:#a6e22e"&gt;completedFuture&lt;/span&gt;(paymentService.&lt;span style="color:#a6e22e"&gt;getStats&lt;/span&gt;(userId));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; DashboardDto &lt;span style="color:#a6e22e"&gt;getDashboard&lt;/span&gt;(Long userId) &lt;span style="color:#66d9ef"&gt;throws&lt;/span&gt; Exception {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; CompletableFuture&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;OrderStats&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; orderFuture &lt;span style="color:#f92672"&gt;=&lt;/span&gt; getOrderStats(userId);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; CompletableFuture&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;PaymentStats&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; paymentFuture &lt;span style="color:#f92672"&gt;=&lt;/span&gt; getPaymentStats(userId);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 둘 다 완료될 때까지 대기&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; CompletableFuture.&lt;span style="color:#a6e22e"&gt;allOf&lt;/span&gt;(orderFuture, paymentFuture).&lt;span style="color:#a6e22e"&gt;join&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; DashboardDto.&lt;span style="color:#a6e22e"&gt;of&lt;/span&gt;(orderFuture.&lt;span style="color:#a6e22e"&gt;get&lt;/span&gt;(), paymentFuture.&lt;span style="color:#a6e22e"&gt;get&lt;/span&gt;());
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 두 조회가 각각 1초씩 걸린다면: 순차 2초 → 병렬 ~1초&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="주의사항-3가지"&gt;&lt;a href="#%ec%a3%bc%ec%9d%98%ec%82%ac%ed%95%ad-3%ea%b0%80%ec%a7%80" class="header-anchor"&gt;&lt;/a&gt;주의사항 3가지
&lt;/h2&gt;&lt;h3 id="1-자기-호출--transactional과-같은-함정"&gt;&lt;a href="#1-%ec%9e%90%ea%b8%b0-%ed%98%b8%ec%b6%9c--transactional%ea%b3%bc-%ea%b0%99%ec%9d%80-%ed%95%a8%ec%a0%95" class="header-anchor"&gt;&lt;/a&gt;1. 자기 호출 — @Transactional과 같은 함정
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;@Async&lt;/code&gt;도 AOP 프록시로 동작한다. 같은 클래스 안에서 직접 호출하면 프록시를 거치지 않아 동기로 실행된다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Service&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;OrderService&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;createOrder&lt;/span&gt;(OrderRequest request) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sendNotification(request); &lt;span style="color:#75715e"&gt;// ❌ this.sendNotification() → 동기 실행&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Async&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;sendNotification&lt;/span&gt;(OrderRequest request) { ... }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;해결책은 &lt;code&gt;sendNotification()&lt;/code&gt;을 별도 Bean(&lt;code&gt;NotificationService&lt;/code&gt;)으로 분리하는 것이다.&lt;/p&gt;
&lt;h3 id="2-예외-처리--호출자에게-전파-안-됨"&gt;&lt;a href="#2-%ec%98%88%ec%99%b8-%ec%b2%98%eb%a6%ac--%ed%98%b8%ec%b6%9c%ec%9e%90%ec%97%90%ea%b2%8c-%ec%a0%84%ed%8c%8c-%ec%95%88-%eb%90%a8" class="header-anchor"&gt;&lt;/a&gt;2. 예외 처리 — 호출자에게 전파 안 됨
&lt;/h3&gt;&lt;p&gt;반환값이 없는 &lt;code&gt;@Async&lt;/code&gt; 메서드에서 예외가 발생해도 호출자에게 전파되지 않는다. &lt;code&gt;AsyncUncaughtExceptionHandler&lt;/code&gt;로 처리해야 한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Configuration&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@EnableAsync&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;AsyncConfig&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;implements&lt;/span&gt; AsyncConfigurer {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Override&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; AsyncUncaughtExceptionHandler &lt;span style="color:#a6e22e"&gt;getAsyncUncaughtExceptionHandler&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; (ex, method, params) &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; log.&lt;span style="color:#a6e22e"&gt;error&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;@Async 예외: method={}&amp;#34;&lt;/span&gt;, method.&lt;span style="color:#a6e22e"&gt;getName&lt;/span&gt;(), ex);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; alertService.&lt;span style="color:#a6e22e"&gt;sendAlert&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;비동기 작업 실패: &amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; method.&lt;span style="color:#a6e22e"&gt;getName&lt;/span&gt;());
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; };
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="3-threadlocal-전파-안-됨"&gt;&lt;a href="#3-threadlocal-%ec%a0%84%ed%8c%8c-%ec%95%88-%eb%90%a8" class="header-anchor"&gt;&lt;/a&gt;3. ThreadLocal 전파 안 됨
&lt;/h3&gt;&lt;p&gt;메인 스레드의 &lt;code&gt;SecurityContext&lt;/code&gt;나 &lt;code&gt;ThreadLocal&lt;/code&gt; 값이 &lt;code&gt;@Async&lt;/code&gt; 스레드에 자동으로 전파되지 않는다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Async&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;asyncMethod&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; SecurityContextHolder.&lt;span style="color:#a6e22e"&gt;getContext&lt;/span&gt;().&lt;span style="color:#a6e22e"&gt;getAuthentication&lt;/span&gt;(); &lt;span style="color:#75715e"&gt;// null 가능&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;보안 컨텍스트가 필요하다면 스레드 풀에 &lt;code&gt;DelegatingSecurityContextTaskDecorator&lt;/code&gt;를 설정해야 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="언제-쓸까"&gt;&lt;a href="#%ec%96%b8%ec%a0%9c-%ec%93%b8%ea%b9%8c" class="header-anchor"&gt;&lt;/a&gt;언제 쓸까
&lt;/h2&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;✅ 적합한 경우
 - 이메일/SMS/푸시 알림 발송
 - 감사 로그 기록
 - 캐시 비동기 갱신
 - 외부 API 호출 (응답 즉시 반환 불필요)

❌ 적합하지 않은 경우
 - 결과를 즉시 응답에 포함해야 할 때
 - 트랜잭션을 호출자와 공유해야 할 때
 - 실패 시 호출자가 반드시 알아야 할 때
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;@Async&lt;/code&gt;와 &lt;code&gt;@Transactional&lt;/code&gt;을 같이 쓰면 비동기 스레드는 호출자의 트랜잭션을 이어받지 않는다. 새로운 독립 트랜잭션으로 실행된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="마치며"&gt;&lt;a href="#%eb%a7%88%ec%b9%98%eb%a9%b0" class="header-anchor"&gt;&lt;/a&gt;마치며
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;@Async&lt;/code&gt;는 &amp;ldquo;결과를 기다릴 필요 없는 부가 작업&amp;quot;에 적합하다. 동작 원리는 AOP 프록시이므로 &lt;code&gt;@Transactional&lt;/code&gt;과 같은 자기 호출 함정이 있다. 예외는 &lt;code&gt;AsyncUncaughtExceptionHandler&lt;/code&gt;로 처리하고, ThreadLocal 전파가 필요하면 별도 설정이 필요하다.&lt;/p&gt;
&lt;p&gt;다음 편에서는 Spring Cache — &lt;code&gt;@Cacheable&lt;/code&gt;과 Redis 캐시 전략을 정리한다.&lt;/p&gt;</description></item></channel></rss>