SpringのComponent、Service、Repositoryの違いを解説する記事のアイキャッチ。Java道場風の空間で若手エンジニアがBeanの役割分担を学ぶイラスト。

実装・エラー解決

Springの@Component・@Service・@Repositoryの違い|どれを付けるべき?

AIによる要約

@Serviceと@Repositoryは、どちらも@Componentを土台にしたステレオタイプで、コンポーネントスキャンによりBean登録されます。違いは主に役割の表現です。業務処理はService、DBなどの永続化アクセスはRepository、どちらにも当てはまらない共通部品はComponentを基本にします。Repositoryにはデータアクセス例外変換の対象になる意味もあるため、全部をComponentへ統一しない方が設計意図を伝えられます。

新人SE
新人SE
どのアノテーションでもDIできるなら、全部@Componentで統一してもよいですか?
ポンコツSE
ポンコツSE
動作だけならBean登録できますが、コードを読む人に役割が伝わりません。クラスの責務に合わせて選ぶのが基本です。

Springのクラスへ@Component、@Service、@Repositoryのどれを付けるかは、新人がレビューでよく質問されるポイントです。実際、いずれもSpringのBeanとして検出され、コンストラクタDIできます。

しかし、同じように動くことと、同じ意味であることは別です。業務システムでは、障害調査や改修で初めて見るクラスを短時間で読むため、アノテーションも設計上の案内になります。

この記事のポイント

  • 3つともコンポーネントスキャンでBean登録できる
  • 業務のユースケースや判断は@Serviceを基本にする
  • 永続化やデータアクセスは@Repositoryを基本にする
  • 汎用部品や補助クラスは@Componentを検討する
  • Spring DataのRepositoryインターフェースは通常アノテーション不要

実行用サンプル

3つのステレオタイプを付けたクラスと、Spring Data JPAのRepositoryが登録されることを比べます。 ソース全文、JUnitの検証コード、pom.xml、READMEを収録しています。JDK 17とMaven 3.9系で mvn test を実行します。Spring Boot 3.5.16のサンプルです。

3つともSpringのBeanとして登録される

OrderRegistrationTest.java の抜粋。登録したBeanと、Spring Dataのプロキシを確認する箇所

    @Test
    void ステレオタイプの三つのクラスとSpringDataのインターフェースがBeanになる() {
        assertThat(context.getBeansOfType(OrderCsvParser.class)).hasSize(1);
        assertThat(context.getBeansOfType(OrderService.class)).hasSize(1);
        assertThat(context.getBeansOfType(OrderQueryRepository.class)).hasSize(1);
        assertThat(context.getBeansOfType(OrderRow.class)).isEmpty();
        assertThat(AopUtils.isAopProxy(context.getBean(OrderRepository.class))).isTrue();
    }

@Service と @Repository は、どちらも @Component を土台にしています。サンプルでは、3つのクラスを OrderSampleApplication の探索範囲に置き、それぞれが1件ずつ登録されることを確認します。

ここだけを見ると「全部@Componentでよい」と感じますが、専用アノテーションにはクラスの役割を伝える意味があります。フレームワークの追加処理やAOPの適用対象として利用される場合もあります。

@Serviceは業務処理の入口に付ける

OrderService.java の全文

package info.syachiku.registration.orders;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class OrderService {
    private final OrderCsvParser parser;
    private final OrderRepository orders;
    private final OrderQueryRepository queries;

    public OrderService(OrderCsvParser parser, OrderRepository orders, OrderQueryRepository queries) {
        this.parser = parser;
        this.orders = orders;
        this.queries = queries;
    }

    @Transactional
    public long place(String orderLine) {
        var row = parser.parse(orderLine);
        return orders.save(new OrderRecord(row)).getId();
    }

    @Transactional(readOnly = true)
    public String findStatus(long id) {
        return queries.findStatus(id);
    }
}

注文を登録する OrderService#place は、入力の変換とDB保存を順番に呼び、注文登録のトランザクションを始めます。変換処理とSQLは、それぞれ別のクラスへ任せます。

単に「何か処理しているクラス」へ全部Serviceを付けるのではありません。日付の書式変換、CSVの1行解析、ハッシュ計算のように業務ユースケースではない補助処理なら、Componentや普通のJavaクラスの方が自然です。

@Repositoryはデータアクセスに付ける

OrderQueryRepository.java の全文

package info.syachiku.registration.orders;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

@Repository
public class OrderQueryRepository {
    private final JdbcTemplate jdbc;

    public OrderQueryRepository(JdbcTemplate jdbc) {
        this.jdbc = jdbc;
    }

    public String findStatus(long id) {
        return jdbc.queryForObject("select status from registered_order where id = ?", String.class, id);
    }
}

自作の OrderQueryRepository は、JdbcTemplate で注文IDに対応する状態を読みます。呼ぶ側はSQLや結果の取得方法を知る必要がありません。

@Repository は役割の目印で、対応する変換の仕組みが有効なら永続化例外を変換する対象にもなります。この例で該当行がない場合の EmptyResultDataAccessException は JdbcTemplate が返す例外です。アノテーション単独の効果とは分けて考えます。

例外変換を目的に何でも@Repositoryへしない

外部API Clientやファイル出力クラスは、DBへアクセスしていないならRepositoryとは限りません。技術的な効果だけでなく、業務上どの境界を表すクラスかを優先します。

Spring DataのRepositoryインターフェースは通常アノテーション不要

OrderRepository.java の全文

package info.syachiku.registration.orders;

import org.springframework.data.jpa.repository.JpaRepository;

public interface OrderRepository extends JpaRepository<OrderRecord, Long> {
}

OrderRepository はアノテーションなしで、Spring Data JPAのRepository探索とFactoryがプロキシを登録します。通常のコンポーネントスキャンでインターフェースをインスタンス化するわけではありません。テストで取得したBeanがプロキシであることを確認します。

一方、JdbcTemplateやEntityManagerを使って自分で実装する検索クラスには、@Repositoryを付けると役割が明確です。

@Componentはどこで使うか

OrderCsvParser.java の全文

package info.syachiku.registration.orders;

import org.springframework.stereotype.Component;

@Component
public class OrderCsvParser {
    public OrderRow parse(String line) {
        String[] fields = line.split(",", -1);
        if (fields.length != 2 || fields[0].isBlank() || fields[1].isBlank()) {
            throw new IllegalArgumentException("注文番号と状態の2列が必要です");
        }
        return new OrderRow(fields[0], fields[1]);
    }
}

補助部品の OrderCsvParser は @Component で登録します。サンプルは引用符のない固定2列だけを対象とし、変換した値の OrderRow はBeanにしません。

  • CSV・固定長ファイルのParser
  • テンプレートを使うメール本文Builder
  • アプリ固有のID採番器
  • 複数クラスから使う変換・検証部品
  • バッチ処理の補助クラス

ただし、依存を持たない単純な計算クラスまで必ずBeanにする必要はありません。普通にnewして使える値オブジェクトや純粋関数的なクラスは、Springへ登録しない選択もあります。

判断基準を表で整理する

質問候補
業務のユースケースや判断を担当するか@Service
DBなどの永続化先へアクセスするか@Repository
どちらでもないアプリ共通部品か@Component
設定を作るメソッド群か@Configuration + @Beanを検討
状態を持つデータそのものかBean登録せず普通のクラスを検討

「DIしたいから何を付けるか」ではなく、「このクラスは何の責任を持つか」からアノテーションを選びます。

全部@Componentにした場合の困りごと

@Component
public class OrderManager {
    // DB検索
    // 在庫判定
    // メール送信
    // CSV出力
}

このクラス名とアノテーションだけでは、業務処理の入口なのか、データアクセスなのか、補助部品なのか判断できません。さらにManagerという曖昧な名前が重なると、改修時に責務を追うコストが増えます。

アノテーションを変えるだけで巨大クラスが直るわけではありませんが、Service・Repository・Componentを分ける検討が、責務整理のきっかけになります。

コンポーネントスキャンされない原因

正しいアノテーションを付けてもBeanが見つからない場合、パッケージ位置を確認します。Spring Bootは通常、@SpringBootApplicationがあるパッケージとその配下をスキャンします。

com.example.app
├── Application.java        // @SpringBootApplication
├── controller
├── service
└── repository             // 通常はスキャン対象

com.example.shared
└── parser                 // app配下でなければ対象外の可能性

対象外パッケージを使うなら@ComponentScanなどの設定を検討します。ただし、設定を増やす前に、アプリケーションの基点パッケージを上位へ置けないか確認します。

同じ型のBeanが複数ある場合

同じ型に一致するBeanが2件あれば、クラスの役割を @Service へ変更しても、単一注入の候補は1件に絞られません。選択ルールとList注入は末尾の @Qualifier と @Primary の記事で試せます。

テストでも役割を意識する

対象主なテスト観点
Service業務分岐、Repository・外部Clientとの連携
RepositorySQL、JPAマッピング、DB制約
Component変換・解析・計算の入出力

役割が分かれていれば、Repositoryのテストへ業務判断を詰め込む、Serviceの単体テストで実DBまで毎回起動するといった混乱を避けやすくなります。

配布コードのテストでは、起動したコンテナから4種類のBeanを取り出した後、注文を1件保存し、別の呼び出しでH2へSQLを実行して READY を読みます。起動できたことと、各層が意図した仕事をしたことを分けて確かめます。

OrderRegistrationTest.java の全文

package info.syachiku.registration.orders;

import org.junit.jupiter.api.Test;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
import org.springframework.dao.EmptyResultDataAccessException;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

@SpringBootTest(classes = OrderSampleApplication.class,
        webEnvironment = SpringBootTest.WebEnvironment.NONE,
        properties = {"spring.jpa.hibernate.ddl-auto=create-drop", "spring.jpa.open-in-view=false"})
class OrderRegistrationTest {
    @Autowired
    private ApplicationContext context;

    @Test
    void ステレオタイプの三つのクラスとSpringDataのインターフェースがBeanになる() {
        assertThat(context.getBeansOfType(OrderCsvParser.class)).hasSize(1);
        assertThat(context.getBeansOfType(OrderService.class)).hasSize(1);
        assertThat(context.getBeansOfType(OrderQueryRepository.class)).hasSize(1);
        assertThat(context.getBeansOfType(OrderRow.class)).isEmpty();
        assertThat(AopUtils.isAopProxy(context.getBean(OrderRepository.class))).isTrue();
    }

    @Test
    void 入力変換と業務処理を通して保存し検索用RepositoryでDBから状態を読める() {
        var service = context.getBean(OrderService.class);
        long id = service.place("order-42,READY");

        assertThat(service.findStatus(id)).isEqualTo("READY");
        assertThatThrownBy(() -> context.getBean(OrderQueryRepository.class).findStatus(-1))
                .isInstanceOf(EmptyResultDataAccessException.class);
    }
}

このプロジェクトでは、上の2件に加え、決済Bean登録の4件とMVCスライスの1件を実行します。

Tests run: 7, Failures: 0, Errors: 0, Skipped: 0

現場レビューでよくある指摘

// レビューコメント例
注文確定というユースケースを担当しているため、
@Componentではなく@Serviceで役割を示してください。

// レビューコメント例
JdbcTemplateでDB検索を行うクラスなので@Repositoryが適切です。
データアクセス例外の扱いも既存Repositoryへ揃えてください。

// レビューコメント例
このFormatterは依存を持たない単純な変換クラスです。
Spring Beanにする必要があるか、普通のJavaクラスでよいか確認してください。

提出前のセルフチェック

  • クラスの責務を一文で説明できるか
  • 業務ユースケースなら@Serviceを選んだか
  • 永続化アクセスなら@Repositoryを選んだか
  • 汎用部品なら@Componentまたは普通のクラスを検討したか
  • Spring Dataインターフェースへ不要な@Repositoryを足していないか
  • コンポーネントスキャン対象パッケージにあるか
  • 同じ型のBeanが複数ないか

Springの役割分担を体系的に学ぶ参考書

アノテーションの暗記ではなく、DI、AOP、トランザクション、データアクセス層までつなげて学ぶと、クラスの責務から判断できるようになります。

書籍「Spring徹底入門 第2版」の表紙
商品画像:Amazon.co.jp
PR 入門後の参考書|Spring Framework 5向け

Spring徹底入門 第2版

株式会社NTTデータ (著), 株式会社NTTデータグループ (編集)

アプリを作った後に、DIやDB更新の仕組みを調べる。

DI、MVC、データアクセス、テスト、セキュリティまで幅広く確認できます。小さなWebアプリを一通り作り、Springの仕組みを詳しく学びたい人に向く参考書です。

  • Springで小さなWebアプリを作ったことがある
  • DIやトランザクションの仕組みを確認したい

Spring Framework 5向けです。Boot 3の実装に使うコード・設定は、採用している版の公式資料と照合してください。

当サイトはAmazonアソシエイト・プログラムの参加者です。価格・在庫・配送条件はAmazonでご確認ください。

この記事とあわせて読みたい

関連記事Spring BootのServiceに業務ロジックを書く理由 Controllerから業務ロジックを分け、ServiceとRepositoryの責任を整理します。 記事を読む 関連記事Spring Bootの「No qualifying bean of type」原因|DIできないときの対処法 Beanの登録、スキャン範囲、コンストラクタ注入を確認します。 記事を読む 関連記事Spring Bootの@Qualifierと@Primaryの違いは?Beanが複数ある時の使い分け 同じ型のBeanが複数あるときの選び方を整理できます。 記事を読む

まとめ

@Component、@Service、@RepositoryはいずれもBean登録できますが、表す役割が違います。業務処理はService、永続化アクセスはRepository、その他の共通部品はComponentを基本にします。

動くアノテーションを選ぶのではなく、クラスの責務を一文で説明してから選びます。Spring Data Repositoryの扱い、コンポーネントスキャン、複数Beanまで確認し、アノテーションと実装内容を一致させましょう。

-実装・エラー解決
-,