「保存したはずのデータがなぜか消える」「NullPointerExceptionが出て原因がわからない」——Spring Boot初学者がつまずく問題の多くは、DI(依存性注入)の仕組みを理解していないことが原因です。
この記事では、DIがない場合に起きる具体的なバグから、SpringのDIコンテナの動作原理、@Controller/@Service/@Repository/@Componentの正しい使い分け、コンストラクタ注入が推奨される理由まで、実際に動くコード例を交えて体系的に解説します。
DIがない場合に起きる問題
まずDIを使わないコードで何が起きるかを確認します。以下は自分でnewしてインスタンスを生成するパターンです。
// ❌ DIなし:自分でnewする問題パターン
public class UserController {
private UserService userService = new UserService(); // 自分でnew
public void saveUser(String name) {
userService.save(name); // ここで保存
}
}
public class AdminController {
private UserService userService = new UserService(); // 別のインスタンス!
public List getUsers() {
return userService.findAll(); // 空のリストが返る…
}
}⚠️ バグの原因:UserControllerでsave()したデータは、そのインスタンスだけが保持しています。AdminControllerが持つUserServiceは別のインスタンスなので、保存したはずのデータが見つかりません。「保存したのになぜかデータが消えた!」という謎バグの正体はこれです。

DI(依存性注入)とは
DI(Dependency Injection:依存性注入)とは、「クラスが必要とするオブジェクトを、自分でnewするのではなく外部から渡してもらう」設計パターンです。
Spring Bootでは、クラスに決まったアノテーションを付けるだけで、Springが自動でインスタンスを生成・管理し、必要なクラスへ渡してくれます。
わかりやすい例えで言えば、自炊(自分でnew)と食堂(Springに任せる)の違いです。食堂なら厨房(DIコンテナ)が一括で料理を作り、注文した人全員に同じ料理を提供します。Springも同様に、1つのインスタンスを必要なすべてのクラスへ届けてくれます。
DIコンテナとは — Springが管理するオブジェクトの置き場所
SpringにはDIコンテナ(正式名称:ApplicationContext)という「オブジェクトの倉庫」があります。アプリ起動時にアノテーション付きクラスのインスタンスを生成してこの倉庫に一元管理し、倉庫に格納されたインスタンスをBean(ビーン)と呼びます。

💡 デフォルトはシングルトン:SpringのBeanはデフォルトで「シングルトン(アプリ内に1インスタンス)」です。つまりUserControllerもAdminControllerも、まったく同じUserServiceインスタンスを受け取ります。「自分でnewした別インスタンス問題」が根本から解決されます。
動く仕組み — 起動からBean注入までの5ステップ
Spring Bootがアプリを起動してからBeanを注入するまでの流れを順番に確認しましょう。

- 起動:
SpringApplication.run()が呼ばれる - スキャン:
@SpringBootApplication配下のパッケージを自動探索 - アノテーション発見:
@Component/@Service/@Repository/@Controller等を見つける - 自動インスタンス化:Springが
newを代行してインスタンスを生成 - コンテナに格納・注入:生成したBeanをApplicationContextに格納し、コンストラクタ等を通じて必要なクラスへ注入
ポイント:@SpringBootApplicationには@ComponentScanが含まれており、配下のパッケージを自動的に探索します。追加の設定ファイルは不要です。
実例コード — 3層構造でDIを体験
Repository層・Service層・Controller層の3層構造を使って、DIが実際にどう機能するかを確認します。
① Repository層(DB操作)
@Repository // ← これだけでSpringがBeanとして管理
public class UserRepository {
private List<String> users = new ArrayList<>();
public void save(String name) {
users.add(name);
}
public List<String> findAll() {
return users;
}
}② Service層(ビジネスロジック)
@Service
public class UserService {
private final UserRepository userRepository;
// コンストラクタインジェクション(推奨)
// コンストラクタが1つなら @Autowired は省略可
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
// ↑ SpringがコンテナからUserRepositoryを自動で渡してくれる
}
public void register(String name) {
userRepository.save(name);
}
public List<String> getAll() {
return userRepository.findAll();
}
}③ Controller層(リクエスト受付)
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
// ↑ SpringがコンテナからUserServiceのBeanを自動注入
}
// POST /users?name=Taro → ユーザー登録
@PostMapping
public String register(@RequestParam String name) {
userService.register(name);
return name + " を登録しました";
}
// GET /users → 全ユーザー取得
@GetMapping
public List<String> getAll() {
return userService.getAll();
}
}④ 起動クラス
@SpringBootApplication // @ComponentScan を内包
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}✅ ポイント:開発者はどこにもnew UserService()やnew UserRepository()と書いていません。Springがコンテナ内の同一インスタンスを渡すため、どのクラスからアクセスしてもデータが共有されます。
各アノテーションの違いと使い分け
「全部@Controllerを付ければ動くのでは?」という疑問は自然です。技術的には多くのケースで動いてしまいますが、それは正しくありません。4つの観点から理由を説明します。
観点① アノテーション同士の「親子関係」
@Service・@Controller・@Repositoryはすべて内部で@Componentを持っています。継承関係は以下のとおりです。

@Component(Bean登録の基本)
├── @Controller(Web入力受付 / MVC用)
├── @Service(ビジネスロジック処理の中心)
├── @Repository(DB操作 / 例外変換あり★)
└── @RestController(@Controller + @ResponseBody)観点② 付加機能の違い — @Repositoryだけは動作が変わる
| アノテーション | @Componentへの付加機能 | 動作の変化 |
|---|---|---|
@Controller | Spring MVCがWebリクエスト受け付けとして認識 | Web層として機能 |
@RestController | @Controller + @ResponseBody | 自動でJSONを返す |
@Service | 現時点では付加機能なし(AOP適用点として設計) | 機能差はほぼない |
@Repository | DB例外をDataAccessExceptionに自動変換★ | 実際に動作が変わる |
@Component | 純粋なBean登録のみ | 汎用部品 |
⚠️ @Repositoryを@Componentで代替すると起きる問題:DB層のクラスに@Componentを付けると、JDBCやJPAが投げるSQLExceptionなどがDataAccessExceptionに変換されません。上位レイヤーで例外ハンドリングがうまく動かなくなる原因になります。DB操作クラスには必ず@Repositoryを使いましょう。
観点③ 可読性・意図の伝達
チーム開発では、アノテーションを見ただけで「このクラスの役割」が伝わることが重要です。
// パッと見て役割がわかる ✅
@Controller // → Webの入り口
@Service // → ビジネスロジック
@Repository // → DB操作
@Component // → それ以外の汎用部品(スケジューラーなど)観点④ AOPの適用範囲
AOP(Aspect Oriented Programming)とは、ログ出力やトランザクション処理を「横断的」に差し込む仕組みです。@Transactional(トランザクション管理)は@Serviceレイヤーに付けるのが慣習です。
@Service
public class OrderService {
@Transactional // Service層に書くのが慣習
public void placeOrder(Order order) {
// 在庫を減らして、注文を保存して、請求を作成する
// → どれか1つ失敗したら全部ロールバック
}
}使い分けチートシート
| クラスの役割 | 使うアノテーション | 理由 |
|---|---|---|
| HTTPリクエストを受け付ける(画面用) | @Controller | Spring MVCのWeb層として認識される |
| REST APIのエンドポイント | @RestController | 自動でJSONレスポンスを返せる |
| 業務ロジック・計算・判定処理 | @Service | ビジネスロジック層を明示。@Transactionalと相性◎ |
| DBへの読み書き(SQL・JPA) | @Repository | DB例外の自動変換機能あり。必ずこれを使う |
| 上記以外の汎用部品(スケジューラー等) | @Component | 特定の役割に当てはまらない場合 |
@Autowiredとは何か — 「作る」と「渡す」を分けて理解する
@AutowiredはBeanを新たに作るアノテーションではありません。役割を一言で言うと——
「Springさん、ここにコンテナから適切なBeanを渡してください」というお願いの目印
インスタンス化(Bean登録)は@Serviceや@Repositoryが担当します。@Autowiredはすでにコンテナに入っているBeanを「取り出して差し込む」指示です。
| 役割 | 担当 | タイミング |
|---|---|---|
| ① Beanを作ってコンテナに入れる | @Service / @Repository 等 | アプリ起動時に自動実行 |
| ② コンテナから取り出して注入する | @Autowired(またはコンストラクタ注入) | 必要なクラスへBeanを差し込む |
3種類の注入方法
// ① フィールドインジェクション(簡単だが非推奨)
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
// テスト時にモックを差し替えにくい・finalにできない
}
// ② セッターインジェクション(オプション注入に使うことがある)
@Service
public class UserService {
private UserRepository userRepository;
@Autowired
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
// ③ コンストラクタインジェクション(現在の推奨 ✅)
@Service
public class UserService {
private final UserRepository userRepository; // final にできる
// コンストラクタが1つだけなら @Autowired は省略可(Spring 4.3以降)
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}注入方法の比較
| 注入方法 | final付与 | テストのしやすさ | 推奨度 |
|---|---|---|---|
| フィールドインジェクション | ❌ 不可 | △ 差し替えにくい | 非推奨 |
| セッターインジェクション | ❌ 不可 | ○ 可能 | オプション依存時のみ |
| コンストラクタインジェクション | ✅ 可能 | ◎ 差し替えやすい | ✅ 推奨 |
@Autowiredがないと何が起きるか
@Service
public class UserService {
// @Autowired なし・コンストラクタ注入なし
private UserRepository userRepository; // null のまま!
public void register(String name) {
userRepository.save(name); // ← NullPointerException 発生!
}
}⚠️ 実務で頻出のNullPointerExceptionの原因:「@Serviceを付けているのにNPEが出る」というトラブルの多くはこのパターンです。@Repository等でBean登録していても、注入の指示(@Autowiredまたはコンストラクタ注入)がなければSpringは差し込んでくれません。
まとめ
DIなしとDIありの違いを改めて整理します。
| 項目 | DIなし(自分でnew) | DIあり(Springに任せる) |
|---|---|---|
| インスタンス管理 | ❌ 各クラスが個別に生成 | ✅ Springが一元管理(シングルトン) |
| データ共有 | ❌ 別インスタンスで共有不可 | ✅ 同一インスタンスを共有 |
| 記述量 | ❌ newを随所に書く | ✅ アノテーション1行だけ |
| テスト容易性 | ❌ 差し替えが難しい | ✅ モックに差し替えやすい |
登場したアノテーション早見表
| アノテーション | 役割 | タイミング |
|---|---|---|
@Service / @Repository 等 | Beanを「作って」コンテナに格納 | アプリ起動時 |
@Autowired(コンストラクタ注入) | コンテナからBeanを「取り出して注入」 | アプリ起動時 |
@Repository(特記) | DB例外→DataAccessExceptionに変換 | DB操作時 |
Spring BootのDIは「アノテーションを付けてSpringに任せる」という発想の転換です。@Service等はBeanを「作ってコンテナに入れる」係、@Autowired(またはコンストラクタ注入)はコンテナから「取り出して差し込む」係と、2つの仕事に分かれています。この「作る」と「渡す」を分けて理解することが、Spring DIを掴む最大のコツです。
アノテーション使い分けの鉄則
- Web受付 →
@Controller/@RestController - 業務処理 →
@Service - DB操作 →
@Repository(例外変換のため必須) - それ以外 →
@Component - 注入の指示 →
@Autowiredまたは コンストラクタインジェクション(推奨)


コメント