0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Spring `@Transactional`が`private`メソッドで動作しない理由を調べてみました - Spring AOP Proxyベースで理解する

0
Last updated at Posted at 2026-02-23
  • 執筆は自分で行い、翻訳にはAIを使用しました
  • 現在日本語を勉強しています。翻訳内容を確認しましたが、もし間違いや不自然な表現があれば教えてください
  • 内容についてのフィードバックも歓迎です

はじめに

コードを書いている最中にIDEを使っているなら、IDEの親切な機能によって次のような経験をすることがあります

image.png

@Transactionalでアノテーションが付与されたメソッドはオーバーライド可能である必要があります

image.png

@Transactionalの自己呼び出し(実質的にターゲットオブジェクト内のメソッドが同じオブジェクト内の別のメソッドを呼び出すこと)は、実行時に実際のトランザクションを発生させません

しかし実際に実行してみると、コンパイルも通るし、ランタイムエラーも発生しない。
ただし、トランザクションは動作しない。つまり、例外が発生してもロールバックされない。
IntelliJでは警告を表示してくれますが、なぜ使ってはいけないのか、なぜ動作しないのかについて調べてみました。

Spring @Transactionalの動作原理について

  • @TransactionalはSpring AOPベースのプロキシパターンで動作します
    そのため、これを理解するにはプロキシがどのように生成されるかを把握する必要があると思います
    Spring公式ドキュメントでも以下のように説明されています

You can apply the @Transactional annotation to an interface definition, a method on an interface, a class definition, or a method on a class. However, the mere presence of the @Transactional annotation is not enough to activate the transactional behavior. The @Transactional annotation is merely metadata that can be consumed by corresponding runtime infrastructure which uses that metadata to configure the appropriate beans with transactional behavior.

@Transactionalアノテーションはインターフェース、インターフェースのメソッド、クラス、クラスのメソッドに適用できます。ただし、@Transactionalアノテーションが存在するだけではトランザクションは動作しません。@Transactionalは単なるメタデータであり、ランタイムインフラがこのメタデータを読み取って対象のBeanにトランザクションの動作を設定します。

@Transactionalはそれ自体では何も動作しない単なるメタデータであり、実際にはランタイム時に生成されるプロキシが処理を担当します。

プロキシが生成される過程

  • SpringコンテナがBeanを登録する際、@Transactionalが付いたメソッドがあればプロキシオブジェクトを生成します
  • その後、外部から該当メソッドを呼び出すと、プロキシが呼び出しをインターセプトして以下の順序でトランザクションを処理します
  1. TransactionInterceptor.invoke()@Transactionalメソッドの呼び出しをインターセプトします
  2. AbstractPlatformTransactionManager.getTransaction()で情報を確認後、新しいトランザクションの生成または既存トランザクションへの参加を決定します
  3. DataSourceTransactionManager.doBegin()を通じてコネクションを取得し、DataSource.getConnection()を通じてトランザクションを開始します
  4. TransactionSynchronizationManager.bindResource()を通じて、同一スレッド内で同じコネクションの再利用を保証します
  5. ビジネスロジックを実行します(@Transactionalが付いたロジック)
  6. AbstractPlatformTransactionManagerでコミットまたはロールバックを確定します
  7. DataSourceTransactionManager.doCleanupAfterCompletion()でスレッドを解放し、コネクションプールに返却します

これをダイアグラムで表すと以下のようになります

  • つまり、プロキシが実際のメソッドの前後でトランザクションの開始と終了を処理する構造です

プロキシ方式:JDK Dynamic Proxy vs CGLIB

Spring Boot 2.x以降はデフォルトでCGLIBをプロキシとして使用します

If the target object to be proxied implements at least one interface, a JDK dynamic proxy is used, and all of the interfaces implemented by the target type are proxied. If the target object does not implement any interfaces, a CGLIB proxy is created which is a runtime-generated subclass of the target type.

プロキシ対象のオブジェクトが一つ以上のインターフェースを実装している場合はJDK Dynamic Proxyが使用されます。インターフェースを実装していない場合はCGLIBプロキシが生成され、これは対象タイプのランタイム生成サブクラスです。

これに加えて、CGLIBの制約事項も記載されています

If you want to force the use of CGLIB proxying, you can do so. However, you should consider the following issues:

  • final classes cannot be proxied, because they cannot be extended
  • final methods cannot be advised, because they cannot be overridden
  • private methods cannot be advised, because they cannot be overridden
  • Methods that are not visible – for example, package-private methods in a parent class from a different package – cannot be advised because they are effectively private

private methods cannot be advised, because they cannot be overridden

  • privateメソッドはオーバーライドできないため、アドバイスの適用が不可能です
    これがSpring公式ドキュメントで述べられている根本的な理由です。

  • CGLIBは対象クラスを継承してサブクラスとして生成します

  • privateメソッドはサブクラスから見えないためオーバーライドが不可能です

  • つまり、プロキシが該当メソッドの呼び出しをインターセプトできないため、プロキシを生成できません

  • JDK Dynamic Proxyもインターフェースベースであるため、privateメソッドはそもそもインターフェースに定義できず、当然プロキシも不可能です

まとめ

まとめると、

  • どのプロキシ方式を使用しても、privateメソッドはプロキシがインターセプトできません

結論:privateメソッドでの@Transactionalは動作しない

  • 多くの解説やIDEの警告が示す通り、privateメソッドではトランザクションは動作しません

image.png

簡単なテストコードで実験してみました

シンプルなドメインロジック

image.png

  1. publicメソッドにトランザクションを付けたコードでRuntimeExceptionを発生させると、ロールバックが正常に動作してオブジェクトは保存されないはずです
  2. privateメソッドにトランザクションを付けたコードでRuntimeExceptionを発生させると、ロールバックが動作しないためオブジェクトが存在するはずです

テストコードの結果

image.png

すべて成功しました。どちらにも@Transactionalを付けましたが、結果は全く異なりました。

ログ分析

実際のテスト時に発生したログを見ると、その差が明確にわかります。

publicメソッド

2026-02-24 02:18:30.077 [Test worker] [] [userId=anonymous] TRACE o.s.t.i.TransactionInterceptor - Getting transaction for [com.example.demo.blog.TestService.saveWithPublicTransaction]
2026-02-24 02:18:30.077 [Test worker] [] [userId=anonymous] TRACE o.s.t.i.TransactionInterceptor - Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
Hibernate: insert into hashtags (created_at,tag,updated_at,usage_count) values (?,?,?,?)
2026-02-24 02:18:30.079 [Test worker] [] [userId=anonymous] TRACE o.s.t.i.TransactionInterceptor - Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
2026-02-24 02:18:30.079 [Test worker] [] [userId=anonymous] TRACE o.s.t.i.TransactionInterceptor - Completing transaction for [com.example.demo.blog.TestService.saveWithPublicTransaction] after exception: java.lang.RuntimeException: Rollback test
2026-02-24 02:18:30.082 [Test worker] [] [userId=anonymous] TRACE o.s.t.i.TransactionInterceptor - No need to create transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findByTag]: This method is not transactional.
Hibernate: select h1_0.id,h1_0.created_at,h1_0.tag,h1_0.updated_at,h1_0.usage_count from hashtags h1_0 where h1_0.tag=?
2026-02-24 02:18:30.077 [Test worker] [] [userId=anonymous] TRACE o.s.t.i.TransactionInterceptor - Getting transaction for [com.example.demo.blog.TestService.saveWithPublicTransaction]
  • TestService(プロキシ)がトランザクションを開始しました
2026-02-24 02:18:30.077 [Test worker] [] [userId=anonymous] TRACE o.s.t.i.TransactionInterceptor - Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
Hibernate: insert into hashtags (created_at,tag,updated_at,usage_count) values (?,?,?,?)
2026-02-24 02:18:30.079 [Test worker] [] [userId=anonymous] TRACE o.s.t.i.TransactionInterceptor - Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
  • 新しいトランザクションを作成せず、既存のトランザクションに参加します
2026-02-24 02:18:30.079 [Test worker] [] [userId=anonymous] TRACE o.s.t.i.TransactionInterceptor - Completing transaction for [com.example.demo.blog.TestService.saveWithPublicTransaction] after exception: java.lang.RuntimeException: Rollback test
  • TransactionInterceptorが例外を検知し、サービスのトランザクション全体をロールバックしました
  • save()のinsertクエリも含まれているため、これも取り消されます
  • つまり、その後の照会時にはロールバックされているためデータが存在しません

privateメソッド

2026-02-24 02:18:29.957 [Test worker] [] [userId=anonymous] TRACE o.s.t.i.TransactionInterceptor - Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
Hibernate: insert into hashtags (created_at,tag,updated_at,usage_count) values (?,?,?,?)
2026-02-24 02:18:29.991 [Test worker] [] [userId=anonymous] TRACE o.s.t.i.TransactionInterceptor - Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
2026-02-24 02:18:30.009 [Test worker] [] [userId=anonymous] TRACE o.s.t.i.TransactionInterceptor - No need to create transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findByTag]: This method is not transactional.
Hibernate: select h1_0.id,h1_0.created_at,h1_0.tag,h1_0.updated_at,h1_0.usage_count from hashtags h1_0 where h1_0.tag=?
  • publicのテストで見られたTestServiceのログがありません。つまり、プロキシがインターセプトできませんでした
2026-02-24 02:18:29.957 [Test worker] [] [userId=anonymous] TRACE o.s.t.i.TransactionInterceptor - Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
  • 自らトランザクションを開いて
2026-02-24 02:18:29.991 [Test worker] [] [userId=anonymous] TRACE o.s.t.i.TransactionInterceptor - Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
  • save()がすぐにコミットします。ラップするトランザクションがないため、SimpleJpaRepository.save()自体のトランザクションとして動作します
  • そしてsave()findByTag()の間に例外関連のログがありません
  • RuntimeExceptionが発生しましたが、TransactionInterceptorはこれを検知できません
  • insertがコミットされてしまったため、データが残っています

Self-Invocationの問題

  • 次の内容についてはこちらをご参照いただけると幸いです。別の記事として書きました
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?