Spring Bootで非同期処理を完全理解!初心者向けのやさしい解説
新人
「先輩、Spring Bootの非同期処理って何なんですか?初心者向けの記事を読んでもピンとこなくて…」
先輩
「お、いいところに気がついたね。非同期処理は、処理を待たずに次の作業を進められる仕組みのことなんだ。Spring Bootでも簡単に実装できるんだよ。」
新人
「それって、普通の処理とどう違うんですか?同期処理って言葉もよく聞きますが…」
先輩
「その疑問は大事だね。じゃあ、まずは非同期処理とは何かを、Spring Bootの初心者向けにわかりやすく解説していこうか!」
1. 非同期処理とは何か
非同期処理とは、「ある処理をしている間に、他の処理も並行して進めることができる仕組み」のことです。Spring Bootでは、非同期処理を簡単に実装できる方法が用意されています。
たとえば、メール送信やファイル出力など、時間のかかる処理があるとします。これを非同期で実行すれば、ユーザーの待ち時間を減らせて、アプリケーションのレスポンスが良くなります。
Spring Bootでは、@Asyncというアノテーションを使って非同期処理を実装できます。次のように設定します。
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
public class AsyncService {
@Async
public void executeAsyncTask() {
System.out.println("非同期タスクを実行中…");
try {
Thread.sleep(3000); // 3秒待機(重い処理のイメージ)
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("非同期タスク完了!");
}
}
この例では、executeAsyncTaskというメソッドが非同期で実行されます。この処理を呼び出す側は、待つことなく次の処理に進めるため、アプリ全体の効率が上がるのです。
2. 同期処理との違い
同期処理とは、処理が終わるまで「待つ」仕組みです。すべての処理が順番に実行されるため、重い処理があると後続の処理が止まってしまいます。
実際の例を見てみましょう。以下は、Spring Bootの@Controllerクラスで同期的にサービスを呼び出すコードです。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class SyncController {
@Autowired
private AsyncService asyncService;
@GetMapping("/sync-task")
@ResponseBody
public String syncTask() {
asyncService.executeAsyncTask(); // この時点で処理が終わるまで待つ
return "処理完了しました(同期)";
}
}
このコードでは、非同期メソッドを呼び出しているように見えますが、実はSpring Bootで非同期処理を使うには、いくつかの前提が必要です。まず、非同期処理を有効にするための設定が必要です。
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
@Configuration
@EnableAsync
public class AsyncConfig {
}
@EnableAsyncを使うことで、Spring Bootに「非同期処理を有効にしてね」と伝えることができます。これを忘れると、@Asyncを付けても同期処理のままになります。
次に、同期処理と非同期処理の違いを実感できるように、ログ出力のタイミングを見てみましょう。
@GetMapping("/async-task")
@ResponseBody
public String asyncTask() {
asyncService.executeAsyncTask(); // 非同期で実行される
System.out.println("コントローラの処理は即座に完了");
return "処理開始(非同期)";
}
コントローラの処理は即座に完了
非同期タスクを実行中…
非同期タスク完了!
上記のように、「コントローラの処理は即座に完了」というログが先に出て、そのあとに非同期タスクが動いているのが分かります。これが非同期処理の最大の特徴です。
初心者のうちは、「なぜタスクが後で実行されるの?」と感じるかもしれませんが、Spring Bootが裏側でスレッドを分けて処理してくれていると考えると理解しやすいです。
3. @Asyncアノテーションの使い方と設定方法
Spring Bootで非同期処理を実装するには、@Asyncアノテーションを使うのが一般的です。初心者向けの観点から言えば、このアノテーションを付けるだけで、バックグラウンドで別のスレッドが自動的に処理をしてくれるようになります。
ただし、@Asyncを使う前に必要な設定があるので、そこを順番に解説していきましょう。
まず、非同期処理を有効にするには、設定クラスに@EnableAsyncを追加します。これはSpring Boot全体に「非同期処理を許可する」という宣言になります。
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
@Configuration
@EnableAsync
public class AsyncConfig {
}
次に、非同期処理を行うメソッドに@Asyncを付けます。対象のクラスには@Serviceや@Componentなどを付けて、Spring管理下にあることを明示する必要があります。
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
public class SampleAsyncService {
@Async
public void runTask() {
System.out.println("非同期処理の開始");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("非同期処理の終了");
}
}
このようにすることで、runTaskメソッドはバックグラウンドで実行され、呼び出し元のコントローラは待たずに次の処理に進みます。
4. 実行スレッドとバックグラウンド処理の関係
Spring Bootにおける非同期処理は、内部的にスレッド(処理の単位)を使って実現されています。つまり、@Asyncが付いたメソッドは、メインのスレッドとは別のスレッドで実行されます。
このような仕組みを理解することで、初心者でも「なぜすぐにレスポンスが返るのか」「なぜ処理が並列に進むのか」がイメージしやすくなります。
以下のコードを使って、ログの出力順を見てみましょう。
@Controller
public class ThreadDemoController {
@Autowired
private SampleAsyncService asyncService;
@GetMapping("/thread-demo")
@ResponseBody
public String handleRequest() {
System.out.println("コントローラ処理開始");
asyncService.runTask();
System.out.println("コントローラ処理終了");
return "非同期処理を開始しました";
}
}
コントローラ処理開始
コントローラ処理終了
非同期処理の開始
非同期処理の終了
このように、非同期処理はメインスレッドとは別に動いているため、コントローラの処理はすぐに完了します。これがSpring Bootのバックグラウンド処理の基本的な流れです。
非同期処理は便利ですが、複数のスレッドが同時に動くため、状態を共有するような処理には注意が必要です。たとえば、共有変数の扱いやデータベースとの整合性など、気をつけるべき点もあります。
5. 非同期処理のよくある活用例(メール送信、バッチ処理など)
Spring Bootの非同期処理は、現実のアプリケーション開発でも非常によく使われます。ここでは、初心者向けに代表的な活用パターンを紹介します。
メール送信
ユーザーが登録したあとに確認メールを送る場合、非同期にすれば画面の表示が遅くなりません。
@Service
public class MailService {
@Async
public void sendWelcomeMail(String email) {
System.out.println(email + " にメールを送信中...");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("メール送信完了!");
}
}
コントローラでこのメソッドを呼び出すとき、ユーザーにはすぐ「登録完了」などのレスポンスが返り、裏側でメール送信処理が動く仕組みになります。
バッチ処理
大量データの処理や定期的な集計処理なども、非同期処理との相性が良いです。特に、Web画面とは関係ない処理をバックグラウンドで走らせたい場合に有効です。
@Service
public class BatchService {
@Async
public void runDailyBatch() {
System.out.println("バッチ処理開始");
try {
Thread.sleep(5000); // データ集計のイメージ
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("バッチ処理完了");
}
}
このように、Spring Bootで非同期処理を使えば、システムの反応速度を損なうことなく重い処理を裏側で実行することができます。
ただし、バッチ処理やメール送信のように失敗したときに再試行したり、ログを出力したりする設計も大切です。これらは別の記事で詳しく扱う予定です。
6. 非同期処理の注意点(例外処理、タイムアウト、進捗管理)
Spring Bootで非同期処理を使う際には、いくつかの注意点があります。特に、初心者の方が見落としやすいポイントとして「例外処理」「タイムアウト」「進捗の把握」があります。
まず、非同期処理で例外が発生しても、通常の同期処理と違って呼び出し元には通知されません。void型の非同期メソッドで例外が発生すると、そのまま握りつぶされてしまいます。
これを防ぐために、Spring BootではAsyncUncaughtExceptionHandlerを使って非同期の例外をキャッチできます。
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import java.lang.reflect.Method;
@Configuration
public class AsyncConfig implements AsyncConfigurer {
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (throwable, method, objects) -> {
System.err.println("非同期処理で例外発生: " + throwable.getMessage());
};
}
}
次に、非同期処理が長時間かかる場合、タイムアウトを設定することも重要です。デフォルトでは無制限なので、意図しない長時間処理が発生する可能性があります。
非同期メソッドでFutureやCompletableFutureを返すようにし、get()にタイムアウトを指定することで制御可能です。
@Async
public CompletableFuture<String> longTask() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return CompletableFuture.completedFuture("完了");
}
呼び出し側でタイムアウトを設定する例:
CompletableFuture<String> result = asyncService.longTask();
String value = result.get(2, TimeUnit.SECONDS); // 2秒でタイムアウト
最後に、非同期処理では進捗の確認が難しいという課題もあります。進捗状況を可視化したい場合は、データベースや共有メモリ、ファイルなどに現在の状態を記録しながら処理を行う工夫が必要です。
7. 非同期処理のログ確認とデバッグの方法
Spring Bootの非同期処理で問題が発生したとき、初心者が一番困るのが「なぜうまく動かないのか分からない」という点です。そんなときには、まずログ出力を確認するのが基本です。
非同期メソッドには、開始時・途中・終了時にSystem.out.printlnやLoggerでログを出しておくと、流れがつかみやすくなります。
例として、Loggerを使ったログ出力の方法は以下の通りです。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
public class LoggingAsyncService {
private static final Logger logger = LoggerFactory.getLogger(LoggingAsyncService.class);
@Async
public void processTask() {
logger.info("非同期タスク開始");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
logger.error("タスク中断", e);
Thread.currentThread().interrupt();
}
logger.info("非同期タスク終了");
}
}
このようにログを出しておけば、タスクの進行状況やエラーの有無が明確になります。
また、非同期処理が起動しない場合のデバッグとして、以下の点を確認してください:
@Asyncがついているか- 対象クラスが
@Serviceや@Componentで登録されているか @EnableAsyncが設定されているか- メソッドが自己呼び出し(
this.method())になっていないか
これらをひとつひとつ確認することで、Spring Bootにおける非同期処理のデバッグもぐっとやりやすくなります。
8. Spring Bootで非同期処理を安全に運用するためのベストプラクティス
最後に、Spring Bootで非同期処理を安全に運用するための初心者向けベストプラクティスを紹介します。
1. スレッドプールの設定を行う
非同期処理の数が増えると、デフォルトのスレッド数では足りなくなる可能性があります。Spring Bootでは独自のスレッドプールを設定することで、安定した運用が可能になります。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
@Configuration
public class AsyncConfig {
@Bean(name = "customExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(3);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("AsyncThread-");
executor.initialize();
return executor;
}
}
そして、@Async("customExecutor")のように、明示的に使用するエグゼキュータを指定することで、管理しやすくなります。
2. 例外処理とログの設計
非同期処理でエラーが起きた場合に備えて、きちんとログ出力や通知設計を行っておくことが大切です。非同期の例外は見えにくいため、ログレベルや通知設定(Slackやメールなど)を整えておくと安心です。
3. 状態管理はスレッドセーフに
複数の非同期処理が同時に動くと、変数の競合が発生することがあります。状態を保持する必要がある場合は、スレッドセーフな仕組み(ConcurrentHashMapやAtomicIntegerなど)を使うようにしましょう。
4. 処理の完了を通知する仕組み
ユーザーに対して「処理が終わった」ことを伝えるためには、非同期処理の終了を検知して通知する仕組みが必要です。たとえば、データベースに完了フラグを更新し、画面側がポーリングする設計などが考えられます。
以上が、Spring Bootで非同期処理を安全かつ確実に運用するための基本的なベストプラクティスです。初心者の方も、これらのポイントを意識することで、実務でも使える堅牢なアプリケーションを作れるようになります。