業務でSpring Bootを半年間使ってみて思ったこと

はじめに

この記事はLINE Advent Calendar 2018の12日目の記事です。

こんにちは。LINEのファミリーサービスの 1つであるLINEキャリアの開発を担当している黒澤です。
新卒として4月に 入社して、5月中旬にLINEキャリア に配属され、すでに約半年が過ぎました。
業務ではサーバーサイドを担当しており、WebアプリケーションフレームワークにはSpring Bootを使用しています。 
そこで今回は 、約半年間 業務でSpring Bootを使用して感じたことについて、色々と書いていきたいと思います。

初めに結論から述べますと

  • DIコンテナが優秀
  • AOPできるのがすごい
  • 他のWebアプリケーションフレームワークより柔軟に実装できる場合がある
  • つまり Spring 最強

という所感でした!

本記事はSpring Bootユーザ向けというよりは、Spring Bootを知らない方や、フレームワームの選定に悩んでいる方向けの記事となります。また、本記事内の説明は、Advent Calender向けにかなり省いたものとなっています。詳しくは公式ドキュメントなどをご参照ください。

Spring Boot とは

この記事を読んでいる人の中で Spring Boot についてよく知らない方もいるかもしれません。

Spring BootとそのベースとなっているSpring Frameworkについて簡単に説明します。

Spring Framework は Java プラットフォーム向けのオープンソースアプリケーションフレームワークです。 Webアプリケーションに限らず、広範囲のJavaアプリケーションを開発できます。

Spring Frameworkはもともと DI ( Dependency Injection ) コンテナから始まった経緯があり、 現在でもこのフレームワークのコアになっている機能が DI です。
この DI コンテナから始まり、数々の機能を備え、現在ではWebアプリケーションフレームワークとして広く使われ、 コミュニティーも盛んです。

Spring Frameworkを簡単に扱えるように便利にしてくれたものがSpring Bootと思っていただければ大丈夫です。
モジュール間のバージョンをよいように決めてくれたり、 Spring FrameworkではXMLで記述する必要があった設定を Java のコードとして記載できたり、 TomcatなどのWebアプリケーションコンテナを内蔵して手早くアプリケーションが動くようにしてくれます。

この Spring Framework と Spring Boot について今回紹介したいのが

  • Dependency Injection
  • Aspect Oriented Programming
  • Test

です。非常に魅力的な機能なので是非読み進めていって欲しいです。順に説明します。

Dependency Injection

前節の Spring の説明で DI という言葉が出てきましたが、馴染みのない人もいるかと思います。
Spring を知るにあたって DI を理解することがファーストステップであるといっても過言ではないくらい Spring にとって大事な要素です。
DI とは Dependency Injection (つまり依存性の注入) の略で、クラス間にある依存関係を、コードに書くのではなく、実行時に解決していくデザインパターンのことです。

クラス間の依存性が高く、柔軟性に欠けてしまうコードを書いてしまうことはありませんか。
その理由の一つに、クラスが他のクラスをコード上で静的に参照している場合があります。

例えば production と development 環境で別のロジックを使いたい場合に以下のように設計してしまうと、 環境を判定する条件分岐のコードを都度書かなければいけません。

class ProductionLogic {
  void execute() {
    ...
  }
}

class DevelopmentLogic {
  void execute() {
    ...
  }
}

class Hoge {
  void hoge() {
    ...
    if (isDevelopment) {
      new DevelopmentLogic().execute();
    } else {
      new ProductionLogic().execute();
    }
  }
}

そこでインターフェースを共通化させます。

interface Logic {
  void execute();
}

class ProductionLogic implements Logic{
  void execute() {
    ...
  }
}

class DevelopmentLogic implements Logic {
  void execute() {
    ...
  }
}

class Hoge {
  void hoge() {
    ...
    Logic logic = isDevelopment ? new DevelopmentLogic() : new ProductionLogic();
    logic.execute();
  }
}

少しすっきりしましたが、HogeがProductionLogicとDevelopmentLogic両方の環境用のコードに依存していることは変わりません。

静的に参照するのではなく、Hogeの外部から動的に依存オブジェクトを渡してあげる形に設計します。 これにより依存性が薄まり、柔軟な切り替えが可能になります。

class Hoge {
  private Logic logic;

  public Hoge(Logic logic) {
    this.logic = logic
  }
  void hoge() {
    ...
    logic.execute();
  }
}

しかしこのときに、誰が HogeにLogic実装クラスのオブジェクトを渡してくれるのでしょうか。
それをやってくれるのがDIコンテナの仕事というわけです。

オブジェクトインスタンスの生成を管理し、それを使用するオブジェクトに渡してくれることで面倒ごとを解決してくれます。

具体的なやり方としてはこのようにアノテーションを付けて、Spring DIコンテナにコンポーネントとして管理してもらいつつ、 依存するコンポーネントの適切な実装を結びつけてもらいます。

@Component
class Hoge {
  private Logic logic;

  @Autowired
  public Hoge(Logic logic) {
    this.logic = logic
  }
  void hoge() {
    ...
    logic.execute();
  }
}

柔軟に依存性を注入できるようになるため、例えば環境の違いにより一部だけ別のロジックに変えるといった事が簡単にできるようになります。

Aspect Oriented Programming

アスペクト指向プログラミング AOP は Aspect Oriented Programming の略で、 オブジェクト指向プログラミング OOP(Object Oriented Programming)同様にプログラミング手法の1つです。

OOP ではオブジェクトに機能を持たせ、相互に利用していく形になりますが、 OOP だけでは、共通機能を持たせるようなときに、共通処理ライブラリに対する依存性が高いコードになってしまったり、 いちいちボイラープレートなコードを書いて、コピペコードが増殖し、保守性を下げたりしてしまいがちです。

例えば、ログ処理や、トランザクションの開始と終了処理をすべてのビジネスロジックコードごとに書くのは大変ですよね。 こういったあらゆる面に出てくる共通処理(としてやりたいこと)を横断的な関心と呼びます。

そこで、共通処理をアスペクトとして書き、それを対象のビジネスロジックに編み込めば、依存性が下がり変更修正のコストも低くなります。

OOP だけのコード

@Slf4j
@Service
class HogeService {
  @Autowired
  private TransactionService transactionService;

  public void save(Hoge hoge) {
    log.info("start");
    try {
      transactionService.start();
      // hoge を save する処理
      transactionService.commit();      
    } catch(Exception ex) {
      transactionService.rollback();
      throw ex;
    } finally {
      log.info("end");      
    }
  }
}

AOP を用いたコード

@Aspect
@Component
@Slf4j
@Service
public class ServiceAspect {
  @Autowired
  private TransactionService transactionService;
    
  @Around("@within(Transactional)")
  public Object around(ProceedingJoinPoint pjp) throws Throwable {
    Object ret = null;
    log.info("start");
    try {
      transactionService.start();
      ret = pjp.proceed();
      transactionService.commit();      
    } catch(Exception ex) {
      transactionService.rollback();
      throw ex;
    } finally {
      log.info("end");      
    }
    return ret;
  }
}
@Service
class HogeService {
  @Transactional
  public void save(Hoge hoge) {
    // hoge を save する処理
  }
}

以上により、新しく FugaService の save を作るときも @Transactional を加えるだけで Transaction 処理を書かなくて済むようになります。

Spring Bootでは AspectJ をベースとした AOP フレームワークを持っているため AOP を簡単に実現できます。 トランザクション処理のようにSpring Bootにそれを行うためのアスペクトが用意されていて、 適切なアノテーションを付けるだけでやってくれるものもありますし、上記のログ処理のように、自分でアスペクトを書くこともできます。

Test

DI コンテナによって依存性の注入を柔軟に行うことができますので、Spring Boot でのテストコードはとてもカジュアルに書くことができます。
以下のようなコードがあるとします。

class HogeConfiguration() {
  @Bean
  @Primary // デフォルトではProductionLogicを持つHogeServiceを使う
  public HogeService production(ProductionLogic logic) {
    return new HogeService(logic);
  }

  @Bean
  public HogeService test(TestLogic logic) {
    return new HogeService(logic);
  }
}
class HogeService {
  @Autowired
  private Logic logic;

  public boolean exec() {
    ...
    return logic.exec();
  }
}

こちらのサービスをテストするためのテストコードを記述します。

class HogeServiceTest {
  @Autowired("test") //TestLogicを持つHogeServiceを使う
  private HogeService hogeService;

  public boolean execTest() {
    assetThat(logic.exec(), is(true));
  }
}

このように TestLogic を mock として実装すれば HogeService の単体テストが可能になります。
また mockito など便利なパッケージを使うとさらにテストが楽になります。

Spring Boot を実務運用する際に注意する点

便利な Spring Boot ですが、実務運用していくにあたって注意が必要な点もありましたのでまとめておきます。

  1. 同一オブジェクト内のメソッド呼び出しは SpringAOP が効かない
    → SpringAOP では内部で CGlib と呼ばれるコード生成ライブラリを用いて proxy と呼ばれるクラスでラップされます。 アスペクトによる処理はこのラッパー部分に編み込まれます。 そして DI からはそのラッパーオブジェクトが inject されます。
    そのため、DIが注入した参照を通してメソッドを呼び出した場合は proxy を介すため AOPによる処理が働きますが、 thisによる参照を通してメソッドの呼び出しを行う場合には proxy が経由されずAOP処理が効きません。

NGコード

@Service
@RequiredArgsConstructor
class HogeService {
  private final HogeRepository hogeRepository;
  
  @Transactional
  public save(Hoge hoge) {
    hogeRepository.save(hoge);
  }
  
  public create() { // <- このメソッドが外から呼ばれます
    Hoge hoge = new Hoge();
    save(hoge); // <- NG
  }
}

修正コード

@Service
@RequiredArgsConstructor
class HogeService {
  private final HogeRepository hogeRepository;
  private final ApplicationContext applicationContext;
  
  @Transactional
  public save(Hoge hoge) {
    hogeRepository.save(hoge);
  }
  
  public create() { // <- このメソッドが外から呼ばれます
    Hoge hoge = new Hoge();
    HogeService hogeService = (HogeService) AopContext.currentProxy(); // <- proxyを取得します
    hogeService.save(hoge);
  }
}
  1. proxy にすり替わった Bean に対するフィールドアクセスは意図通り動かない
    → CGlib で生成されたラッパーのインスタンスはフィールドに値を持っていません。
    例えばテストクラスからテスト対象のクラスのフィールドを読み出すなど、 DIが注入した参照を通してフィールドに直接アクセスすると、意図した値が読み出せないことがあります。 これはgetterなどアクセサメソッドを定義して、メソッド呼び出しとしてアクセスするようにする事で解決できます。

NGコード

@Service
class HogeService {
  public Integer value;
  
  @PostConstruct
  public init() {
    value = 100;
  }
}

@Service
@RequiredArgsConstructor
class Hoge2Service {
  private final HogeService hogeService;
  
  public exec() {
    Integer value = hogeService.value; // <- NG
    ...
  }
}

修正コード

@Service
class HogeService {
  @Getter // アクセサを用意
  private Integer value;
  
  @PostConstruct
  public init() {
    value = 100;
  }
}

@Service
@RequiredArgsConstructor
class Hoge2Service {
  private final HogeService hogeService;
  
  public exec() {
    Integer value = hogeService.getValue();
    ...
  }
}
  1. 循環参照すると injection がうまくできない
    → 循環参照時はそれぞれの Bean を必要とするため、単純にコンストラクタで injection するとそもそも Bean の作成に失敗します。
    設計の見直しをしろと書かれていたりしますが、setter インジェクション もしくは PostConstruct メソッドを用いる事で解決できます。
    このとき、Lombok で作るコンストラクタに注意しましょう。

NGコード

@Service
@RequiredArgsConstructor
class HogeService {
  private final Hoge2Service hoge2Service;
  ...
}

@Service
class Hoge2Service {
  private final HogeService hogeService; // <- NG
  ...
}

修正コード

@Service
@RequiredArgsConstructor
class HogeService {
  private final Hoge2Service hoge2Service;
  ...
}

@Service
@RequiredArgsConstructor
class Hoge2Service {
  private final ApplicationContext applicationContext; // <- ApplicationContextをAutowiredして
  private HogeService hogeService;
  
  @PostConstruct
  public init() {
    hogeService = applicationContext.getBean(HogeService.class); // <- PostConstructでBeanを読み込む
  }
}
  1. Scope が異なる Bean にアクセスする時は注意が必要
    → Requestをハンドルするメソッドから非同期に良び出したメソッド内で、RequestScopeのBeanにアクセスすると、Scopeの範囲外からのアクセスになるためクラッシュする。
    Bean の Scope の違いを意識して開発しましょう。

まとめ

以上を述べた上で Spring の大きな特徴として

  • DI が容易にできる
  • AOP フレームワークを持っている

がありました。

これらの特徴により

  • Test が容易に可能
  • ロジックの切り替えも簡単
  • 共通部分の切り出しが楽

などなどの利点もありました。

どうでしょう。 Spring なかなかよくないですか??

また、Ruby on Rails や CakePHP など他の Web アプリケーションフレームワークと比較すると、 Spring Frameworkでは、各種オブジェクトの役割は命名規則ではなくアノテーションにより付与します。 そのため、フレームワークによる制約が軽くなり、アークテクチャの選定など設計を柔軟に行うことが可能な点も魅力的だと思いました。

最後に

今回は僕が Spring Boot/Spring Frameworkを半年間使ってみて思った感想と Spring 結構いいのではという内容の記事でした。

Spring を使わない人でも Spring の技術から得られる事はあるのかなとも思いますので参考にしてみてください。

ここまで読み進んでいただいた皆さん、ありがとうございます。

また、本記事を作成するにあたって LINEキャリア 開発担当の小澤さんにはレビューやアイデアの提供していただきました。大変助かりました。ありがとうございます!

明日の記事は、YJさんによる「Deep Learning in Business with Keras and LIME」です。 お楽しみに!

Related Post