複数のクラスをDIする場合の工夫を完全ガイド!初心者でも理解できるSpring DIの基本
新人
「先輩、Springで複数のクラスを同時に依存性注入したいときって、どうやって実装するんですか?」
先輩
「それはいい質問だね。複数のクラスをDIするには、設計の工夫や注入の方法をうまく使い分ける必要があるんだよ。」
新人
「単体のクラスならコンストラクタ注入で簡単にできますけど、複数になるとごちゃごちゃしそうで不安です…」
先輩
「心配しなくて大丈夫。まずは依存性注入(DI)の基本から確認して、次に複数のクラスを扱うユースケースを一緒に見ていこう。」
1. 依存性注入(DI)の基本を復習しよう
Spring Frameworkにおける依存性注入(DI)とは、クラスが必要とするオブジェクトを外部から渡すことで、疎結合な設計を実現する仕組みです。Springコンテナがオブジェクトの生成と依存関係の管理を担当することで、開発者は実装に集中できるようになります。
たとえば、UserServiceというクラスをUserControllerに注入するには、次のようにコンストラクタ注入を使います。
@Controller
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/user")
public String getUser(Model model) {
model.addAttribute("user", userService.getUserInfo());
return "user";
}
}
このように、Spring DIでは依存するクラスを明示的にコンストラクタで受け取り、内部で利用できるようにすることで、テストや拡張がしやすくなります。
@Autowiredを省略できる点も初心者にとっては理解しやすいポイントです。
2. 複数クラスを使う場面とは?ユースケースで理解しよう
現実の業務システムでは、複数のサービスクラスやユーティリティクラスを同時に扱うケースがよくあります。
たとえば、ProductControllerで商品情報だけでなく、ユーザー情報やログ出力のサービスも使いたい場合などです。
そのようなケースでも、コンストラクタ注入を活用すればスムーズに対応できます。
以下は複数クラスを一度に注入する例です。
@Controller
public class ProductController {
private final ProductService productService;
private final UserService userService;
private final LoggingService loggingService;
public ProductController(ProductService productService, UserService userService, LoggingService loggingService) {
this.productService = productService;
this.userService = userService;
this.loggingService = loggingService;
}
@GetMapping("/product")
public String getProduct(Model model) {
loggingService.logAccess();
model.addAttribute("product", productService.getProductInfo());
model.addAttribute("user", userService.getUserInfo());
return "product";
}
}
このように複数のクラスをSpring DIで同時に注入する場合でも、コンストラクタにまとめて記述することで、明確でメンテナンスしやすいコードになります。
このような構成は、以下のような状況でよく使われます:
- ユーザー情報と商品情報の両方を扱う場合
- トランザクション管理とビジネスロジックを分離する場合
- ログや監査情報の出力処理を追加したい場合
また、設計によってはこれらのサービスをさらにまとめて、ひとつのFacadeクラスとして注入するパターンもあります。
このように、Spring DIを活用して複数クラスを扱うことで、保守性の高いJavaアプリケーションを実現できます。
3. 複数の依存クラスをコンストラクタで注入する方法
Spring DIでは、コンストラクタに複数の依存クラスを記述することで、必要なオブジェクトを一括で注入できます。複数のクラスを注入することで、複雑な処理を各サービスクラスに分担でき、コントローラの責務をシンプルに保つことができます。
次のように、複数の依存クラスをコンストラクタで受け取ることで、Springが自動的に適切なBeanをバインドしてくれます。
@Controller
public class OrderController {
private final OrderService orderService;
private final PaymentService paymentService;
private final NotificationService notificationService;
public OrderController(OrderService orderService, PaymentService paymentService, NotificationService notificationService) {
this.orderService = orderService;
this.paymentService = paymentService;
this.notificationService = notificationService;
}
@PostMapping("/order")
public String processOrder(Model model) {
orderService.createOrder();
paymentService.processPayment();
notificationService.sendConfirmation();
return "orderSuccess";
}
}
このような構成にすることで、各処理を独立したサービスに委譲でき、再利用性やテストのしやすさが飛躍的に向上します。
また、@Autowiredを明示せずとも、Springが自動でDIを行ってくれるため、記述も簡潔に保てます。
4. コンストラクタの引数が多くなる場合の工夫
依存するクラスが多くなると、コンストラクタの引数も多くなり、可読性や保守性が低下することがあります。
そのような場合に有効なのが、Beanのグルーピングや中間層の導入といった工夫です。
たとえば、関連するサービス群を1つのFacadeクラスにまとめることで、注入先のクラスをすっきりさせることができます。
@Component
public class OrderFacade {
private final OrderService orderService;
private final PaymentService paymentService;
private final NotificationService notificationService;
public OrderFacade(OrderService orderService, PaymentService paymentService, NotificationService notificationService) {
this.orderService = orderService;
this.paymentService = paymentService;
this.notificationService = notificationService;
}
public void processFullOrder() {
orderService.createOrder();
paymentService.processPayment();
notificationService.sendConfirmation();
}
}
@Controller
public class OrderController {
private final OrderFacade orderFacade;
public OrderController(OrderFacade orderFacade) {
this.orderFacade = orderFacade;
}
@PostMapping("/order")
public String process(Model model) {
orderFacade.processFullOrder();
return "orderSuccess";
}
}
このように設計を整理して依存関係をまとめることで、コントローラの見通しが良くなり、変更にも強い構造になります。
他にも、Builderパターンやプロパティクラスの活用も考慮できますが、初心者にはまずFacadeによるまとめ方が最も実践的で理解しやすい方法です。
5. 実務での具体的な設計パターン
実際の業務開発では、Spring DIの活用と同時に、レイヤー分離による設計が重要になります。
代表的なパターンは、次のようなController → Service → Repositoryという3層構造です。
// コントローラ
@Controller
public class AccountController {
private final AccountService accountService;
public AccountController(AccountService accountService) {
this.accountService = accountService;
}
@GetMapping("/account")
public String getAccount(Model model) {
model.addAttribute("account", accountService.findAccount());
return "account";
}
}
// サービス
@Service
public class AccountService {
private final AccountRepository accountRepository;
public AccountService(AccountRepository accountRepository) {
this.accountRepository = accountRepository;
}
public Account findAccount() {
return accountRepository.findById(1L);
}
}
// リポジトリ
@Repository
public class AccountRepository {
public Account findById(Long id) {
// データベースから取得する処理(仮)
return new Account(id, "ユーザーA");
}
}
このように各レイヤーが責任を分担しながら依存関係を持つ構造にすることで、拡張やテストが容易になります。
また、依存の向きが一方向(Controller → Service → Repository)になるように設計することが、循環依存エラーの回避にもつながります。
Spring DI 工夫やコンストラクタ 複数といった検索キーワードで情報を探すことで、より実践的なノウハウも得られるでしょう。
6. 実際のSpringプロジェクトでの構成例
ここでは、実際のSpringプロジェクトでの構成を例に、Controller → Service → Repositoryの流れに沿って、複数クラスをどうやってDIするかを解説します。
プロジェクトの中で、ユーザーと商品を扱う画面を考えてみましょう。
以下は、ProductControllerがUserServiceとProductServiceの2つのサービスクラスに依存している構成です。
@Controller
public class ProductController {
private final ProductService productService;
private final UserService userService;
public ProductController(ProductService productService, UserService userService) {
this.productService = productService;
this.userService = userService;
}
@GetMapping("/dashboard")
public String dashboard(Model model) {
model.addAttribute("productList", productService.findAll());
model.addAttribute("userInfo", userService.getCurrentUser());
return "dashboard";
}
}
@Service
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
public List<Product> findAll() {
return productRepository.findAll();
}
}
@Repository
public class ProductRepository {
public List<Product> findAll() {
// 仮のデータを返す
return List.of(new Product("ノートパソコン"), new Product("マウス"));
}
}
このように、それぞれの層でDIによって役割が分離されているため、後から機能を追加するときにもコードが煩雑になりません。
この構成は、Spring 複数 DI 方法や@Autowired 複数といったキーワードでもよく検索される実践的な例です。
7. テストや保守のしやすさを高めるための設計ポイント
複数のクラスを依存注入する場合でも、テストしやすい構造を意識することが大切です。
まず、インターフェースを使って抽象化することで、テスト時にMockやStubを差し替えやすくなります。
次のように、依存先のサービスをインターフェースで定義することが基本です。
public interface UserService {
User getCurrentUser();
}
@Service
public class UserServiceImpl implements UserService {
@Override
public User getCurrentUser() {
return new User("山田 太郎");
}
}
また、コンストラクタで明示的にDIすることで、テストコードで依存を注入しやすくなるという利点があります。
以下はテスト時にMockitoなどで依存クラスを注入する例です。
UserService mockUserService = Mockito.mock(UserService.class);
ProductService mockProductService = Mockito.mock(ProductService.class);
ProductController controller = new ProductController(mockProductService, mockUserService);
こうした設計にすることで、将来的な保守作業も容易になります。依存の入れ替えや機能の追加にも柔軟に対応できます。
8. よくある質問(FAQ)とその回答
ここでは初心者がよくつまずくポイントを「よくある質問(FAQ)」形式で整理し、それぞれ丁寧に解説します。
Q1. @Autowiredを使わなくても動くの?
A. はい、コンストラクタに1つしか引数がない、または明示的に@Autowiredを書かない場合でも、Springが自動的に依存を注入してくれます。初心者でも自然に書けるようになっているのがSpringの魅力です。
Q2. 複数の同じ型のBeanをDIしたいときは?
A. その場合は@Qualifierを使ってBeanの名前を指定するか、@Primaryを使って優先されるBeanを明示します。
@Autowired
@Qualifier("mainService")
private MyService myService;
Q3. 依存クラスが多すぎて読みづらい…
A. 設計を見直すタイミングです。関連するクラスをFacadeなどでまとめたり、責務を分割することで、見通しのよいコードに改善できます。
Q4. @Autowiredとnewの違いは?
A. newでインスタンスを生成すると、Springコンテナの管理外になるため、DIの恩恵を受けられません。必ずDIで管理されるBeanを使うようにしましょう。
このようなよくある疑問も、Spring DIの基本を理解しておけばすぐに解決できます。
初心者 DI Springといったキーワードで検索すると、同じような疑問に対するヒントも見つかるので、ぜひ活用してみてください。