ピンポイントタイム散布(PTS)でのバックエンド改善の軌跡 ~6. メンバー変数への再代入禁止 ~

こんにちは、農業系プロダクトの開発を担当している糸井です。

弊社ではピンポイントタイム散布(以下、PTS)という防除のデジタル化サービスを展開しております。

www.optim.co.jp

PTSシステムのバックエンドではSpringBootを使用しております。
立ち上げ期より、既存の農業系プロダクトの課題感をもとに改善を進めてきましたので、その軌跡をご紹介します。

前回までの記事はこちら


前回の記事では、CQRSに対する事例をご紹介しました。
今回はメンバー変数への再代入についてご紹介します。

6. メンバー変数への再代入禁止

きっかけは1つのバグチケットでした。 まずはバグの内容をコードブロック内で簡略化して解説します。

きっかけのバグ

package entity;

public class Entity1 {
  private String aaa;

  /**
   * メンバー変数に引数を代入するだけのただのPublicなSetterではなく、
   * メソッド内部でロジックを書いてメンバー変数に再代入している、つまり、破壊的な変更を行なっているメソッド
   */
  public void setAaa(Fuga fuga) {
    final var piyo = // 引数fugaを加工するロジック

    this.aaa = piyo; // メンバー変数への再代入
  }
}
package usecase_1;

@AllArgsConstructor
public class Usecase1 {
  private final HogeRepository hogeRepository;
  private final Usecase2 usecase2;

  /**
   * Propagationを指定していないので、デフォルトである Propagation.REQUIRED が適用される。
   * つまり、内部で読んでいる Usecase2.bbb() のTransactionは張られずに、Usecase1.aaa() と同一Transaction内で処理される。
   */
  @Transactional(rollbackFor = Exception.class)
  public void aaa() {
    var hoge = hogeRepository.getHoge(id);
    hoge.setAaa(aaa); // hoge に対して破壊的な変更を行なっている。

    usecase2.bbb(); // この処理の中でバグが発生した。
  }
}
package usecase_2;

@AllArgsConstructor
public class Usecase2 {
  private final HogeRepository hogeRepository;

  /**
   * Usecase2.bbb() のTransactionは張られずに、Usecase1.aaa() と同一Transaction内で処理される。
   */
  @Transactional(rollbackFor = Exception.class)
  public void bbb() {
    // Usecase1.aaa() と同じTransactionなので、MybatisにQuery結果がCacheされている。
    // Usecase1.aaa() の処理内で hoge に対して破壊的変更を行なっている。つまり、MybatisのCacheに破壊的変更が入っている。
    // そのため、以下の hoge は純粋なQuery結果ではなく、Usecase1.aaa() で行なった hoge.setAaa(aaa) の結果が格納されている。
    var hoge = hogeRepository.getHoge(id);

    ...省略 // hoge の aaa に予期しない値が格納されていたため、バグに繋がった

  }
}

実際のバグの原因としてはTransactionの張り方も不味かったですが、
このバグから学んだのは、インスタンスに対して極力破壊的変更は加えてはいけないということでした。


ということで改善を進めてきました。

メンバー変数への再代入禁止を目指す

DomainのEntityでメンバー変数に代入するのではなく、新たなインスタンスを生成して返却するように修正しました。
以下は修正後のサンプル実装です。

package entity;

public class Entity1 {
  private final String aaa; // final を付与し、再代入させない

  /**
   * 新たなインスタンスを生成して返却する
   */
  public Entity1 piyoPiyo(Fuga fuga) {
    final var piyo = // 引数fugaを加工するロジック

    return new Entity1(piyo);
  }
}

メンバー変数への再代入禁止を目指した結果

  1. MybatisがQuery結果をCacheする (大体のORMapperがそうだと思いますが) こと
  2. メンバー変数への再代入は避けるべきであること

この2点はかなり意識されるようになったと思います。

つづきはこちら (2025/4/04 追記)

ピンポイントタイム散布(PTS)でのバックエンド改善の軌跡 ~7. Entityのインスタンス生成ロジックを文脈で変える ~ - OPTiM TECH BLOG

最後に

OPTiMではチームで協力し、難しく大きな課題を楽しみながらチャレンジしていきたいというメンバーを大募集しております。 ご興味がありましたら、下記フォームよりご応募ください。

www.optim.co.jp