トランザクション設計で考えるSpring Boot実務

Spring Bootトランザクション設計で@Transactional、rollback、Service層を整理する図

トランザクション設計は業務処理の境界で考える

トランザクション設計で迷う場面は、Spring Boot実務ではかなり多いです。トランザクションアノテーションをどこに付けるか。rollbackされる例外は何か。Service層をどこで分けるか。こうした判断は、アノテーションを付けるだけでは決まりません。

結論から言うと、Spring Bootでは業務境界が重要です。「1つの業務処理として成功または失敗させたい範囲」を明確にします。

この記事では、Java/Spring Bootの実務でよく迷う点を整理します。トランザクションアノテーションの付け方、rollback、propagationを扱います。さらに、Service層の境界やレビューで見られやすいポイントも解説します。

Springの宣言的管理を理解する

まず、Spring Frameworkには一貫したトランザクション管理の仕組みがあります。公式のTransaction Managementでも説明されています。JTA、JDBC、Hibernate、JPAなどをまたいだモデルです。

実務では、トランザクションアノテーションを使うことが多いです。明示的にcommitやrollbackを書くより一般的です。Spring公式のDeclarative Transaction Managementでも説明されています。宣言的トランザクション管理という考え方です。

ただし、宣言的に扱えるからといって、設計を考えなくてよいわけではありません。むしろ、どのServiceメソッドをトランザクション境界にするかが重要になります。

トランザクションはService層の業務処理に付けるのが基本

次に、トランザクションアノテーションはService層に付けることが多いです。RepositoryではなくService層です。理由は、トランザクションの境界がDB操作単体ではなく、業務処理単位で決まるためです。

@Service
public class OrderService {

  @Transactional
  public void placeOrder(OrderRequest request) {
    Order order = orderRepository.save(request.toOrder());
    paymentRepository.save(Payment.pending(order));
    stockService.reserve(request.items());
  }
}

この例では、注文作成、支払い作成、在庫予約を1つの業務処理として扱います。途中で失敗した場合、全体をrollbackしたい範囲です。そのため、Serviceメソッドが自然な境界になります。

一方で、Repositoryメソッドごとに分けると問題が起きます。業務処理全体の一貫性を守りにくくなるためです。例えば、注文だけ保存され、支払い作成に失敗する状態が起きる可能性があります。

Spring Boot トランザクション設計ではrollback条件を理解する

特に誤解されやすいのがrollback条件です。Springのトランザクションアノテーションは、デフォルトでは非検査例外でrollbackします。RuntimeExceptionなどです。一方で、checked exceptionでは注意が必要です。自動rollbackされないケースがあります。

Spring公式のUsing @Transactionalでも、rollbackルールが説明されています。アノテーションの設定も確認できます。つまり、例外設計とDB更新の境界は関連します。

@Transactional(rollbackFor = ApplicationException.class)
public void approve(Long orderId) throws ApplicationException {
  Order order = orderRepository.findById(orderId)
      .orElseThrow(ApplicationException::new);

  order.approve();
}

ただし、rollbackForを機械的に付ければよいわけではありません。どの例外でDB更新を取り消すべきかを業務ルールとして決める必要があります。

例外の種類考え方確認ポイント
RuntimeExceptionデフォルトでrollbackされやすい業務例外として使いすぎていないか
checked exception設定しないとrollbackされない場合があるrollbackForが必要か
外部API失敗DB更新と一緒に戻せないことがある補償処理が必要か

特に、外部APIやメッセージ送信を含む処理では注意が必要です。DBはrollbackできます。一方で、外部サービスに送ったリクエストは取り消せない場合があります。

readOnlyは読み取り専用の意図を表す

検索処理では、readOnlyを使うことがあります。トランザクションを読み取り専用にする指定です。これは「この処理では更新しない」という意図を表します。

@Transactional(readOnly = true)
public OrderDetail getOrderDetail(Long orderId) {
  return orderRepository.findDetail(orderId);
}

ただし、readOnlyを付ければ必ず高速になるとは限りません。DB、JPAプロバイダ、設定によって効果は変わります。それでも、読み取り専用の意図をコード上に残せる点は実務上のメリットです。

一方で、readOnlyのメソッド内でEntityを変更する実装は避けるべきです。レビューでは、読み取り用Serviceと更新用Serviceを確認します。責務が混ざっていないかを見るためです。

propagationは例外的な用途から考える

Springのトランザクション伝播では、REQUIRED、REQUIRES_NEW、NESTEDなどを指定できます。公式のTransaction Propagationでも説明されています。それぞれ意味が異なります。

通常は、デフォルトのREQUIREDで十分なことが多いです。呼び出し元にトランザクションがあれば参加し、なければ新しく開始します。

一方で、REQUIRES_NEWは独立したトランザクションを作ります。監査ログだけは本処理が失敗しても残したい、といった場面で検討されます。

propagation使いどころ注意点
REQUIRED通常の業務処理外側のトランザクションに参加する
REQUIRES_NEW監査ログ、独立保存別トランザクションになり接続数にも注意
NESTED部分rollbackDBやTransactionManagerの対応が必要

ただし、REQUIRES_NEWを多用すると設計が読みにくくなります。さらに、接続プールへの負荷も増える可能性があります。そのため、使う場合は理由を明確にします。

外部APIを呼びすぎない

実務で注意したいのが、トランザクション内の外部API呼び出しです。DBロックを保持したまま外部APIの応答を待つと、処理時間が長くなります。

@Transactional
public void register(UserRequest request) {
  User user = userRepository.save(request.toUser());
  mailClient.sendWelcomeMail(user.email());
}

この例では、メール送信に時間がかかると、DBトランザクションも長く開いたままになります。また、メールは送れたのにDBだけrollbackされる、といった不整合も起こり得ます。

そのため、外部API、メール送信、メッセージ送信は分けることを検討します。トランザクション後の処理にする考え方です。イベント、キュー、補償処理などを使う設計も候補になります。

自己呼び出しに注意する

SpringのトランザクションアノテーションはAOPで実現されています。そのため、同じクラス内のメソッド呼び出しには注意が必要です。thisで呼び出すと、期待したトランザクションが適用されないことがあります。

public void importUsers(List<UserRequest> requests) {
  for (UserRequest request : requests) {
    saveOne(request);
  }
}

@Transactional
public void saveOne(UserRequest request) {
  userRepository.save(request.toUser());
}

このような自己呼び出しは、レビューで見落とされやすいポイントです。トランザクション境界を別Serviceに分ける方法があります。呼び出し構造を見直す対応も検討します。

Spring Boot トランザクション設計で避けたいアンチパターン

実務レビューでは、次のような設計を見つけたら注意します。

アンチパターン起きやすい問題見直すポイント
Repositoryごとにトランザクションを付ける業務処理全体の一貫性が見えにくいService層の業務単位で考える
rollback条件を理解せず例外を投げる想定外にcommitされるchecked exceptionとRuntimeExceptionを確認する
REQUIRES_NEWを多用する処理の一貫性が読みにくくなる独立させる理由を明確にする
長い処理をトランザクション内に入れるロック時間が長くなる外部APIや重い処理を分離する
自己呼び出しにトランザクションを期待するアノテーションが効かないことがある別Service化や呼び出し構造を見直す

もちろん、すべてを細かく分ければよいわけではありません。重要なのは、どこからどこまでを一貫して成功させたいのかを説明できることです。

実務で見るSpring Bootトランザクション設計のチェックリスト

  • 業務処理として成功または失敗させたい範囲が明確か?
  • トランザクションをService層の適切なメソッドに付けているか?
  • rollbackされる例外とされない例外を把握しているか?
  • readOnlyのメソッドで更新処理をしていないか?
  • REQUIRES_NEWを使う理由が明確か?
  • 外部APIやメール送信をトランザクション内で待っていないか?
  • 自己呼び出しでトランザクションが効くと誤解していないか?
  • 例外処理、ログ、監査、補償処理まで含めて設計しているか?

まずは、このチェックリストでトランザクション境界を見直します。そのうえで、例外設計や外部連携の扱いまで確認すると、障害時に説明しやすい設計になります。

Spring Bootトランザクション設計でよくある質問

Controllerに付けてもよいですか?

基本的にはService層に付ける方が自然です。ControllerはHTTPリクエストとレスポンスの入口です。一方で、トランザクションは業務処理の一貫性を守るためのものです。そのため、Service層で境界を表す方がレビューしやすくなります。

checked exceptionでもrollbackしたい場合は?

rollbackForを指定する選択肢があります。ただし、まずはその例外で本当にDB更新を戻すべきかを整理します。例外設計と業務ルールを確認したうえで、必要な場所にだけ指定します。

トランザクション設計でREQUIRES_NEWは監査ログに使うべきですか?

監査ログを本処理とは独立して残したい場合、候補になります。ただし、別トランザクションになるため設計意図を明確にする必要があります。接続プールや失敗時の扱いも確認します。

まとめ:Spring Bootトランザクション設計は業務境界を決める設計である

Spring Bootトランザクション設計では、トランザクションを付ける場所だけでは不十分です。業務処理の境界を見ます。rollback条件とpropagationも確認します。外部連携や自己呼び出しまで含めて設計する必要があります。

まずは、1つの業務処理として成功または失敗させたい範囲を決めます。次に、Service層にトランザクション境界を置きます。さらに、例外時のrollback方針を明確にします。この流れを守ると、レビューしやすく保守しやすい設計に近づきます。

Java/Spring Bootの実務では、この設計と例外処理がつながります。SQL設計や外部連携も切り離して考えることはできません。bluenaの採用情報では、Spring Boot経験を活かせる案件相談も行っています。今の経験を次の環境でどう伸ばすか、一度整理してみてください。

IaC INP PM PMO PMP UX Webディレクター インフラエンジニア キャリアチェンジ フロントエンドエンジニア