はじめに
この記事では、Spring の @Transactional がなぜ便利なのに、実務では分かりにくいと感じやすいのかを整理します。
@Transactional は Spring を使うとかなり早い段階で出てくるアノテーションです。
@Transactional
public void registerUser(RegisterUserRequest request) {
userRepository.save(...);
auditLogRepository.save(...);
}
見た目はかなりシンプルです。
しかし、実際には次のような疑問を生みやすいです。
- 本当にどこからどこまでがトランザクションなのか
- 例外を投げたのにロールバックされないのはなぜか
- 同じクラス内で呼んだら効かないのはなぜか
- private メソッドに付けても意味がないのはなぜか
このあたりが分かりにくい理由は、@Transactional が悪いからではなく、便利さの代わりに多くを隠しているからです。
先に結論
先に結論を書くと、@Transactional が分かりにくい理由は次の3つです。
- トランザクション制御がコードではなく AOP とプロキシに隠れている
- 例外や呼び出し方によって効く条件が変わる
- 見た目は短いが、背後に Spring 固有の前提がかなり多い
つまり @Transactional は、トランザクション制御を簡単に書ける代わりに、実行時の仕組みを見えにくくしています。
まず、何が便利なのか
@Transactional が便利なのは、Begin / Commit / Rollback を毎回書かなくてよいことです。
例えば素直に書くと、トランザクションは次のような形になります。
- トランザクションを開始する
- 処理を実行する
- 成功したらコミットする
- 失敗したらロールバックする
Spring では、この定型を次の一行に押し込めます。
@Transactional
public void createUser() {
userRepository.save(...);
}
この簡潔さはかなり強いです。
- 業務ロジックに集中しやすい
- 毎回同じボイラープレートを書かなくてよい
- サービスメソッドが「ここがトランザクション境界」と宣言しやすい
業務アプリで Service 層を中心に開発する場合、この恩恵はかなり大きいです。
便利さの正体は「隠してくれていること」にある
ただし、便利さの正体は「単純になったこと」ではありません。
実際には複雑なことを Spring が裏で引き受けているだけです。
@Transactional は、メソッドの前後にトランザクション処理を差し込む仕組みです。
イメージとしては次のようなことが起きています。
- メソッド呼び出しの直前でトランザクション開始
- メソッド本体を実行
- 正常終了ならコミット
- ロールバック対象の例外ならロールバック
ただし、これはソースコードに直接書かれていません。
Spring の AOP とプロキシが裏で差し込みます。
ここが最初の分かりにくさです。
コードには見えないのに、実行時には効いている
例えば次のコードだけを見ると、ただのメソッドです。
@Service
public class UserService {
@Transactional
public void register(RegisterUserRequest request) {
userRepository.save(...);
}
}
Java の文法としては、特別なことは起きていません。
でも実行時には、Spring がこのクラスをそのまま使わず、プロキシ経由で呼ばせます。
その結果、呼び出し時に横から処理が差し込まれます。
つまり、コードを読んで見える流れと、実行時の流れが完全には一致しません。
これが @Transactional の分かりにくさの中心です。
例外を投げたのにロールバックされないことがある
ここはかなりハマりやすいです。
Spring のデフォルトでは、RuntimeException と Error でロールバックされます。
一方で、検査例外はデフォルトではロールバック対象ではありません。
@Transactional
public void createUser() throws IOException {
userRepository.save(...);
throw new IOException("failed");
}
この場合、IOException は検査例外なので、デフォルト設定のままだとロールバックされずコミットされることがあります。
ここが分かりにくい理由は、見た目では「例外が出たら全部巻き戻りそう」に見えるからです。
しかし実際には、Spring のルールが入っています。
必要なら次のように明示します。
@Transactional(rollbackFor = Exception.class)
public void createUser() throws IOException {
userRepository.save(...);
throw new IOException("failed");
}
つまり @Transactional は単なるスイッチではなく、ロールバック条件まで含めて理解しないと安全に使えません。
同じクラス内から呼ぶと効かないことがある
これも有名な落とし穴です。
@Service
public class UserService {
@Transactional
public void register() {
this.saveAuditLog();
}
@Transactional
public void saveAuditLog() {
}
}
このとき register() の中から this.saveAuditLog() を呼んでも、saveAuditLog() 側に付けた @Transactional の設定は期待どおりには効きません。
理由は、Spring のプロキシを経由していないからです。
@Transactional が効くのは、Spring が管理するプロキシ経由でメソッドが呼ばれたときです。
同一クラス内の直接呼び出しは、ただの this 呼び出しなので割り込めません。
ここで注意したいのは、register() 自体のトランザクションが消えるわけではないことです。
register() に付いた @Transactional はそのまま効いており、saveAuditLog() の処理もそのトランザクション内で実行されます。
問題になるのは、saveAuditLog() 側に別の設定を持たせたい場合です。
@Transactional
public void register() {
this.saveAuditLog();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveAuditLog() {
}
この場合でも、saveAuditLog() を別トランザクションで動かしたいという設定は、同一クラス内の this 呼び出しでは新しく解釈されません。
この問題は、仕組みを知らないとかなり直感に反します。
private メソッドに付けても効かない
これも同じ理由です。
@Transactional
private void saveInternal() {
}
private メソッドは外からプロキシ経由で呼べません。
そのため、@Transactional を付けても意味がありません。
補足すると、Spring 6 以降ではクラスベースプロキシの場合、protected や package-private のメソッドもトランザクション対象になり得ます。
ただし interface ベースのプロキシでは、対象メソッドは public である必要があります。
実務では、Service の public メソッドに付ける、と覚えるのが一番安全です。
見た目だけでは分かりませんが、Spring がやっていることは「メソッド定義そのものを書き換える」ことではなく、「呼び出しの外側に割り込む」ことです。
だから、割り込めない呼び出し方では効きません。
便利なのに分かりにくいのは、宣言的だから
ここまでの話をまとめると、@Transactional は宣言的トランザクション管理です。
つまり、開発者は「どうやるか」ではなく「ここでやってほしい」と宣言します。
これは非常に便利です。
ただし、宣言的である以上、実際の動作はフレームワークが決めます。
この構造が次のトレードオフを生みます。
- 書くのは楽
- でも仕組みを知らないと誤解しやすい
Spring のアノテーション全般に言えることですが、記述量が減るほど、背後のルールへの依存は強くなります。
どこまでをトランザクション境界にするべきかも悩みやすい
@Transactional は便利なので、広く付けたくなります。
ただし、何でもかんでも付ければよいわけではありません。
例えば次のようなケースです。
- Controller に付ける
- private メソッド単位で細かく付ける
- 1つの Service 内でネストした意図を持たせる
これらは読み手にとって分かりにくくなりやすいです。
実務では、Service の public メソッドをトランザクション境界として扱うのが一番安定しやすいです。
理由は次のとおりです。
- 業務処理の単位と合わせやすい
- 呼び出し境界が明確
- AOP プロキシの前提とも噛み合う
つまり、@Transactional 自体の理解だけでなく、どこに付けるのが自然かまで含めて設計判断が必要です。
特に REQUIRES_NEW、readOnly、rollbackFor のような属性を使い始めると、単に「付ければ効く」ではなく、「どの呼び出し経路で、どの設定が有効になるか」を意識する必要があります。
Springの良さでもあり、怖さでもある
@Transactional は Spring の良さをかなり象徴しています。
- 横断的な関心事をアノテーションで隠せる
- 業務ロジックを短く書ける
- 定型処理をフレームワークが引き受けてくれる
一方で、そのまま Spring の怖さにもつながります。
- 見えない前提が増える
- 実行時挙動をフレームワーク知識に依存する
- 直感とずれる落とし穴がある
この意味で @Transactional は、Spring の便利さがどこから来ているのかを理解するのに非常に良い題材です。
まとめ
@Transactional が便利なのに分かりにくいのは、複雑さが消えたからではなく、Spring が裏に隠したからです。
- Begin / Commit / Rollback を自分で書かなくてよい
- その代わり、AOP とプロキシの仕組みを前提にする
- 例外の種類や呼び出し方で挙動が変わる
- 見た目の短さに対して、背後のルールはかなり多い
@Transactional を安全に使うには、アノテーションの書き方だけでなく、「なぜその書き方で効くのか」まで理解しておく必要があります。
Spring 全体を見るときも同じで、便利なアノテーションほど、裏側の仕組みを知っているかどうかで理解度が大きく変わります。