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例外変換
どの層にも属さない部品@ComponentBean登録のみ

迷ったときの判断フローはシンプルです。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とは? もあわせてどうぞ。