#はじめに
この記事では、「Spring Boot」を利用したアプリケーション開発・構築において、
最初に引っかかるであろう「DI」、「AOP」についてまとめています。
今回は1章目として、DIについてまとめていきます。
今回の記事は、奥深くまで追求するというより、SpringFramework,SpringBootを触り始める
導入としての記事になります。
※Javaの一般的な知識や、SpringFrameworkについては理解がある前提で記事をまとめますが、
できる限り初学者に分からなさそうな部分は、掘り下げて説明をしていきます。
#目次
1.SpringBootって?
2.SpringFrameworkとは?
3.DIとは?
4.DIとDIコンテナ
#1.SpringBootって?
SpringBootとは、Springアプリケーション(SpringFramework)をより簡単な、設定や準備、簡潔なコード(アノテーション)で
効率的に開発+デプロイができるようになる、フレームワークと考えてください。
Springフレームワーク のための フレームワークといった立ち位置のイメージです。
上記のように、SpringFrameworkをより良く利用しやすくなるといった点から、
・SpringFrameworkはどんなものか?
・SpringFramework単体に比べ、どのような良さがあるのか?(ここは総じて、次回の記事でまとめる)
といった点を理解するのが、SpringBootの理解に繋がるかと思うため、
SpringFrameworkに関してまとめる。
#2.SpringFrameworkとは?
Javaのフレームワークの一つで、DIやAOPといった仕組みが導入されている、部分が大きな特徴です。
また、SpringFrameworkを軸として、SpringCloud,SpringBatch,SpringShell,SpringSecurityのような
多種多様に渡る汎用的に利用可能な、プロジェクトも提供されているのも大きな特徴です。
SpringFrameworkの教育コストをかけておくことで、将来的にいろんなシステムパターン(バッチ等も含めて)
構築できていく部分は、利点かと思います。
いや、そもそも...DIや、AOP....聞いたことあるけど、良くわかってない...というのが大半だと思うので、
その辺りも含めて、話をしようと思います。
今回は”DI”についてまとめていきます。
#3.DIとは?
DependencyInjectionの略で、DIと言います。
日本語の意味としては、「依存性の注入」と表現します。
いや、意味わからんねん。ってのが感想だと思います。笑
私も、この記事をまとめるにあたって、自己の認識が誤ってる部分も含めて
再学習を行いましたが、以下の記事がとてもわかりやすく、
こちらの表現だと、「オブジェクトの注入」という考え方がしっくりきます。
では、このDIとは実際にどんなものかを見ていきましょう。
以下2つが分かれば、ある程度DIについては理解できるかと思います。
1.従来どんな形式だったコードを、DI使うとどんなコードになるのか?
2.DIを利用することによって、どんな利点があるのか?
を確認していきましょう。
まずは、DIを利用しないパターンのサンプルコードをSpringBootアプリで作ってみます。
以下のリポジトリに置いているので、java11,mavenインストールしてる方は、ご自由にどうぞ。
https://github.com/sasdos-engee/Practice-DI
今回作成するsampleコードは、以下のようなrestAPIをイメージしています。
◽️仕様
機能:「POSTで受けたリクエストを元に、学生情報を登録するAPI」
MVC系のクラス
・Controller ・・・ リクエストを受けて、サービス層を呼び出し、結果に応じてレスポンスを返す。
・Service ・・・ 業務的なチェックを実施後、登録処理を行うためrepository層呼び出し。
・Repository ・・・ DB登録処理を実施。
モデル系のクラス
・Student ・・・ 今回登録する単位となる、学生クラス
・request ・・・ POSTでリクエストを受ける際のオブジェクト
・response ・・・ レスポンスのオブジェクト
DIを利用しないパターン
対象のクラスは以下です。
・SampleNonDiController
・SampleNonDiSerivce
・SampleNonDiRepository
・RegistStudentRequest
・RegistStudentResponse
・Student
今回は、DIを使わない・・・>インスタンスは全て各クラスの中でインスタンス化しています。
さあ、ぱっと見る感じ、これでも動きそう。
はい、動きます。 ではどんなケースに困るのか?
ControllerとService層、Repository層がそれぞれ違うメンバーで
実装していたと仮定しましょう。
それぞれのメンバー(A~Cさん)は、自分の担当層の実装、およびUTの実装を行います。
AさんのUTに、フォーカスして考えてみましょう。
AさんがUTにて担保するのは、
・リクエストに対して、想定通りのオブジェクトを引数に、service層を呼び出せるか?
・service層の戻りパターン(今回だと1 or 0)に応じた正しいレスポンスを返せるか?
である。 それは即ち、service層の振る舞いに左右されることなく、想定の結果が得られるか?
=> service層をモック化してテストを実施することになると思います。
今回Service層の実装が完了していないとして、モック化を考えてみましょう。
・・・と調べてみると分かると思います。
基本的には、JUnit等の実装にて、「メソッド内でインスタンス化している別クラスはモック化”できません”」。
例外的に、JMockitoライブラリや、PowerMockライブラリでモック化はできないことはないですが、
それぞれのライブラリ内の機能が非推奨や、利用できなくなることが多いうえに、
利用しているJUnit,Spring等のフレームワークバージョンとの相性等も考える必要があるため、
できれば利用したくないです。
よっぽどのことがなければ、上記のようなライブラリを使わずとも済むような設計でいきたい。
「そうなると....メソッド内でのインスタンス化を辞めるしかない。」にたどり着きます。
打開策として、newしているものを、引数として実態を渡してもらうこととしましょう。
service層にフォーカスして実装を組み替えてみましょう。
引数としてインスタンスを渡してもらう方式にしてみる
対象のクラスは以下です。
・SampleNonDiVer2Controller
・SampleNonDiVer2Serivce
・SampleNonDiVer2Repository
Serviceクラスをみると、メソッド内でのインスタンス化がなくなりましたね。
これで、Service層のモック化が可能になりました。(引数のrepositoryInstanceの実態を、適宜
テスト側で変更することで、可能)
おっ、これで問題解決では?と感じますが、これではダメです。
なぜなら、Service層だけを見ると解決ですが、Controller層側の問題が深刻になります。
Service層を利用する側のController層が
Repository層のインスタンスも用意する必要が出てきて、いよいよカオスになります。
こうなってくると、serviceクラスのコンストラクタで、repository層のインスタンスを
設定するようにしたとしても、結局呼び元(Controller)でのインスタンス生成が避けれません。
これでは、クラスの依存階層が深ければ深くなるほど、呼び元でのインスタンス定義が増える一方で
根本解決になっていません。
そうなってくると、 Controller層で意識するServiceクラスのインスタンスを
適宜入れ替える仕組み(注入)がないと、UTも含めて根本解決できない。
同じように、Service層で意識するRepositoryクラスのインスタンスも同じです。
ここで出てくるのが、DI(依存性の注入=オブジェクトの注入)です。
DI利用の場合
対象のクラスは以下です。
・SampleDiController
・SampleDiSerivce
・SampleDiRepository
おいおい、アノテーション(@)付けるだけでよしなにやれてるの?って感じです。
そうです、これがDIになります。
コード的にはこれで、うまくやれてんだと分かりますが、中身が意味わかんないですね。
なので、DIについてももう少し掘り下げましょう。
DIを実現するにあたって必要な”DIコンテナ”という仕組みを覚えましょう。
#4.DIとDIコンテナについて
DIは上記で作成したコードのように、
・クラス間での依存性の排除(厳密には、各クラスのメソッド内でのinstance化の排除)
・依存していたクラスの注入(オブジェクトの注入)
することで実現できました。(できたようです)
みた感じ、各クラスに @Serviceやら@Repositoryをつけたり、
依存していたクラスを @Autowiredをつけた、privateフィールドで定義しているくらいですね。
これらが、実際に何をしているのか含めて見ていきましょう。
######DIコンテナについて
上記DI利用の場合のソースを見てみましょう。
SampleDiControllerのソース内のprivateフィールド
"sampleDiSerivice"に着目してみましょう。
今回、sampleDiServiceをメソッド内から外だしすることで、DI化に成功しました。
※上記ソースでは、SpringFrameworkを使ったDIコンテナを利用したDIになってます。
(説明上分かり辛いのは、お許しを。)
しかし、プライベートフィールド化できたところで、
結局インスタンスの実態をクラスを利用する側が、生成する必要があります。
今回の例だと、SampleDiControllerを利用するモジュール側で、
SampleDiControllerのコンストラクタを利用する等して、sampleDiServiceのインスタンスを渡す等ですね。
それに、今回はSampleDiServiceの一つだけですが、利用する依存クラスが多いほど
DI時に呼び元が渡すインスタンスの数は膨大に増えていきます。
(コンストラクタにどんどん引数増えます)
それは嫌だな...なんか簡単にやる方法ないのかな.....と編み出されたのが、
”DIコンテナ”の仕組みです。
DIコンテナは一言で表すと”インスタンス生成+よしなにインスタンスを渡す(DI)”機能を持ったものです。
######SpringFramework(Boot)での DI と DIコンテナについて
上記で説明したように、
DIコンテナに インスタンス生成や、よしなにインスタンスを渡す(DIする)ために、
DI対象のクラスを宣言する必要があります。
それらを実施しているのが、
@Component および @Controller,@Service,@Repositoryのアノテーションです。
私自身も学習する中で、以下の記事が一番理解しやすかったです。
https://blog.mookjp.io/blog-ja/spring-framework%E3%81%A8di%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6/
それぞれのアノテーションについて説明をしていく。
@Controller、@Service,@Repositoryは、@Componentを継承した形で アーキテクチャに則ったアノテーションになっているので、
@Componentについて説明する。
このアノテーションをつけることで、”コンポーネントスキャン”という機能により、DIコンテナにBean登録されます。
これにより、DIコンテナ内にシングルトン(※最後の項で説明します)なクラスとして、インスタンスが登録されているという訳です。
では、DIコンテナからインスタンスを取り出して、注入してくれるのは誰か?
そうです。@Autowired アノテーションです。
このアノテーションを利用することで、DIコンテナに登録されているインスタンスを、よしなに注入してくれます。
これで、SpringでのDI,DIコンテナの仕組みは、大体理解できたかと思います。
「おおお!じゃあ、基本的に独自クラスは、全部 @Componentつけて、 @Autowired つけまくれば
メソッド内でnewせずとも、動作できるんですね!!!」
と思うところですが、1点危険な点があるので注意しておきましょう。
######DIの注意点(シングルトンについて)
上記のように、@AutowiredをつけてDIできることが分かったと思いますが、
これらの”DI”はDI対象のクラスが 「シングルトン」というものになってます。
このシングルトンとは、インスタンスが1つしかないことを意味しています。
つまり、A,B,Cのクラスに対して、 DというクラスをDIした場合、
A,B,Cで利用されているDのインスタンスは同様のものということになります。
当然、Dの中で保持している、フィールドも共有ということになります。
今回、例示しているソースで説明すると、
StudentというクラスをDI対象にすると、
Studentクラス内の”number","name","furigana"のそれぞれの値を共有することになります。
ん?何か起きるの?悪いことがあるの? => はい。設計次第では致命的です。
どんなことが起きてしまうのか?については、以下がわかりやすくまとめてありました。
何も考えずにDI対象のクラスが、状態や変数を持っておくと
スレッドセーフな作りにならず、WebAPIとして致命的な作りになってしまう可能性があります。
(Aさんの個人情報が、Bさんに見えてしまったりするよ)
なので、DIを利用する場合は、以下のどちらかの方針で気をつけましょう。
案1.各スレッドで共有すると困るような、フィールドはクラスに持たせない作りにする。
(いわゆる、スレッドアンセーフになってしまうような変数をそもそもDI対象には持たせない。)
案2.@Scopeを用いて、インスタンスのスコープを変更する。
SpringのDIでは、デフォルトがシングルトンになっているため、 @ScopeアノテーションをDI対象のクラスにつけて、
スコープの指定が行えます。
対象クラスの呼び出しの度、 セッション毎、 リクエスト毎等、複数のスコープ設定が存在するため、
各スレッドで競合が起きてしまわないような、スコープ定義を行ってスレッドセーフな作りにする。
案2は、シングルトンにしない時点で、多数のインスタンスを作り上げることを考えると、
できる限り、設計でスレッドセーフを回避して、案1によせるほうがいいのか?という印象を受けました。
ここまでで、DIについては粗方理解できたかと思います。
次回は、AOPと、SpringBootの特徴についてまとめていこうと思います。