Service層のクラスには@Service、リポジトリには@Repository。チームの慣習でなんとなく付けているけれど、「@Componentと何が違うの?」と聞かれると答えに詰まる、という方は多いのではないでしょうか。
この記事では、3つのステレオタイプアノテーションと@Componentの正確な関係、@Repositoryと@Controllerだけが持つ固有の効果、そしてレイヤーごとの使い分け基準を、実際に動くコードで確認していきます。
なお、@ComponentによるBean登録やコンポーネントスキャンの基本は @Componentって何? で解説しているので、この記事では3つのアノテーション「間の差分」に集中します。
まず結論。違いはこの表だけ覚えればOK
| アノテーション | Bean登録 | 固有の効果 |
|---|---|---|
| @Component | される | なし(基本形) |
| @Service | される | なし(意味付けのみ) |
| @Repository | される | 例外をDataAccessExceptionに変換 |
| @Controller | される | Spring MVCのハンドラ検出対象になる |
使い分けは「Web層は@Controller(REST APIなら@RestController)、ビジネスロジック層は@Service、データアクセス層は@Repository」。これだけです。
Bean登録という意味では4つとも完全に同じですが、@Repositoryと@Controllerには追加の仕掛けがあります。順番に見ていきましょう。
3つともすべて@Componentの派生
「ほぼ同じ」というのは感覚論ではなく、Spring Frameworkのソースコードに書いてある事実です。@Serviceの宣言を見てみましょう。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component // ← @Component自体がメタアノテーションとして付いている
public @interface Service {
@AliasFor(annotation = Component.class)
String value() default "";
}
@Serviceの定義に@Componentが付いていますよね。@Repositoryや@Controllerもまったく同じ構造です。
コンポーネントスキャンは「@Componentが付いたクラス」だけでなく、「@Componentが付いたアノテーションが付いたクラス」も検出します。この仕組みをメタアノテーションと呼びます。
つまり、Beanとして登録されDIの対象になるという点では、4つのアノテーションに一切の差はありません。では、なぜわざわざ3種類も用意されているのでしょうか。
@Repositoryの実効果は例外変換
3つの中でいちばん明確な固有機能を持つのが@Repositoryです。
Springには PersistenceExceptionTranslationPostProcessor という仕組みがあり、@Repositoryが付いたBeanをプロキシでラップして、JPAやJDBCが投げる実装依存の例外を、Spring共通の DataAccessException 階層に変換してくれます。Spring Bootでは自動構成されるので、設定なしで有効です。
実際に確かめてみましょう。EntityManagerを直接使う自作リポジトリを用意します。
@Repository
public class ProductDao {
@PersistenceContext
private EntityManager em;
public void save(Product product) {
em.persist(product);
em.flush(); // 制約違反をこの時点で発生させる
}
}
codeカラムに一意制約がある状態で、同じコードの商品を2回保存してみます。
@SpringBootTest
@Transactional
class ProductDaoTest {
@Autowired
ProductDao productDao;
@Test
void 一意制約違反はDataAccessExceptionに変換される() {
productDao.save(new Product("SKU-001"));
// JPAのPersistenceExceptionではなく、Springの例外がスローされる
assertThrows(DataIntegrityViolationException.class,
() -> productDao.save(new Product("SKU-001")));
}
}
JPAが投げるのは本来 PersistenceException 系の例外ですが、実際にキャッチできるのはSpringの DataIntegrityViolationException(DataAccessExceptionのサブクラス)です。
これの何が嬉しいかというと、呼び出し側のService層が「JPAを使っているか、JdbcTemplateを使っているか」を意識せずに例外ハンドリングを書けることです。永続化技術を差し替えてもcatch節を書き直す必要がありません。REST APIでの例外ハンドリング全体は 例外処理の記事 も参考にしてください。
そしてここが重要ですが、このクラスを@Componentに変えると例外変換は効かなくなり、生のPersistenceExceptionが飛んでくるようになります。明確な機能差ですね。
Spring Data JPAのリポジトリインターフェースには不要
よくある誤解がこれです。
// @Repositoryを付ける必要はない
public interface ProductRepository extends JpaRepository<Product, Long> {
}
JpaRepository を継承したインターフェースは、Spring Data JPAが独自の仕組みでBean登録し、例外変換も標準で有効にしてくれます。@Repositoryを付けても害はありませんが、意味もありません。
自分で@Repositoryを付けるべきなのは、先ほどのようにEntityManagerやJdbcTemplateを直接使う「自作の」リポジトリ実装クラスの場合、と覚えておきましょう。
@Controllerの実効果はハンドラ検出
@Controllerにも固有の効果があります。Spring MVCの RequestMappingHandlerMapping は、@Controllerが付いたBeanの中からだけ@GetMappingなどのハンドラメソッドを探します。
つまり@Controllerを@Componentに変えると、Beanとしては登録されるのに、@GetMappingを付けたメソッドがハンドラとして検出されず、エンドポイントが404になります。「全部@Componentでも動くのでは?」への反例その1です。
ちなみに、REST APIでおなじみの@RestControllerの正体はこうなっています。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
@AliasFor(annotation = Controller.class)
String value() default "";
}
@Controllerと@ResponseBodyの合成アノテーションですね。画面(ビュー名)を返すなら@Controller、JSONを返すREST APIなら@RestControllerを選びます。REST APIの実装の流れは CRUD APIのチュートリアル で詳しく解説しています。
@Serviceには固有機能がない。それでも付ける3つの理由
さて本題の@Serviceです。実は現行のSpring Frameworkにおいて、@Serviceは@Componentの純粋なエイリアスで、付加的な処理は何もありません。
「じゃあ@Componentでいいのでは?」と思いますよね。それでも@Serviceを付けるべき理由が3つあります。
1. レイヤーが一目で伝わる
アノテーションを見るだけで「このクラスはビジネスロジックを担う層だ」と読み取れます。パッケージ構成やコードレビューの指針にもなり、チーム開発では地味に効いてきます。レイヤー構成の考え方は パッケージ構成の記事 にまとめています。
2. AOPのポイントカット対象にできる
@Serviceを付けておくと、ビジネスロジック層「だけ」にログや監視を横断適用できます。
@Aspect
@Component
public class ServiceLogAspect {
@Around("@within(org.springframework.stereotype.Service)")
public Object log(ProceedingJoinPoint jp) throws Throwable {
long start = System.currentTimeMillis();
try {
return jp.proceed();
} finally {
System.out.println(jp.getSignature() + " " + (System.currentTimeMillis() - start) + "ms");
}
}
}
全部@Componentにしてしまうと、こういう「層を狙った」横断処理が書けなくなります。
3. 将来の機能追加に備えられる
@ServiceのJavadocには、この特殊化は将来のリリースで付加機能の対象になり得る、という趣旨の記載があります。実際@Repositoryには後から例外変換が載りました。意味に沿って付けておくこと自体が保険になるわけです。
全部@Componentにしても動くのか?
ここまでの内容を整理すると、こう答えられます。
動くケースもあります。DIの解決だけを見れば、@Serviceや@Repositoryを@Componentに置き換えてもアプリは起動し、注入も成立します。DIの基本は DIとは何かの記事 を参照してください。
しかし壊れるケースが2つあります。
- @Controller → @Componentに変えると、ハンドラマッピングが効かずエンドポイントが404になる
- @Repository → @Componentに変えると、例外変換が効かず、DataAccessException前提のハンドリングをすり抜ける
つまり「動くかどうか」で選ぶのではなく、「レイヤーの意図が伝わり、固有機能が正しく効くか」で選ぶのが正解です。
使い分け早見表
新しいクラスを書くときは、この表のとおり素直に付ければ迷いません。
| 層 | アノテーション | 得られる効果 |
|---|---|---|
| Web層(画面) | @Controller | ハンドラ検出 |
| Web層(REST API) | @RestController | ハンドラ検出 + @ResponseBody |
| ビジネスロジック層 | @Service | レイヤー表明・AOP対象 |
| データアクセス層(自作実装) | @Repository | 例外変換 |
| どの層にも属さない部品 | @Component | Bean登録のみ |
迷ったときの判断フローはシンプルです。HTTPリクエストを受けるなら@Controller系、DBに触るなら@Repository、ビジネスロジックなら@Service、どれでもなければ@Componentです。設定を担うクラスなら @Configuration の出番ですね。
最後に、3層を並べた完成形を見ておきましょう。
@RestController
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@GetMapping("/products/{id}")
public Product get(@PathVariable Long id) {
return productService.find(id);
}
}
@Service
public class ProductService {
private final ProductDao productDao;
public ProductService(ProductDao productDao) {
this.productDao = productDao;
}
public Product find(Long id) {
return productDao.findById(id);
}
}
@Repository
public class ProductDao {
// EntityManagerやJdbcTemplateを使った実装
}
注入はコンストラクタインジェクションが推奨です。理由は インジェクション方式の比較記事 で解説しています。
まとめ
@Service・@Repository・@Controllerは、Bean登録という意味ではすべて@Componentと同じです。ただし@Repositoryは例外変換、@Controllerはハンドラ検出という固有の効果を持ち、@Serviceは機能こそないものの、レイヤーの表明とAOPの適用単位として付ける価値があります。
レイヤーに応じて素直に使い分けることが、コードの可読性とフレームワークの機能を両方活かす最短ルートです。
Bean登録の仕組みそのものをおさらいしたい方は @Componentと@Beanの違い、DIの考え方から整理したい方は DIとは? もあわせてどうぞ。