本文へスキップ
BecomeCoder

Spring / Spring Bootコース · 第6章 実践 ― 実務で通用するSpringアプリへ · レッスン23

レイヤ構成 ― Controller/Service/Repositoryの役割分担

ローカル実施

導入

ここまでの章で、Controller・Service・Repositoryという3つの層が何度も登場しました。もし全部の処理をControllerに詰め込んでしまったら、何が困るのでしょうか。

説明

すべてを1つのControllerに書いてしまった、いわゆる「肥大化したController」を想像してみましょう。

// 悪い例:業務ロジックもSQL相当の処理もController内にすべて書いてしまっている
@RestController
class UserApiController {

    @PostMapping("/api/users")
    public UserResponse create(@RequestBody CreateUserRequest request) {
        // バリデーション、重複チェック、パスワードのハッシュ化、
        // DB保存、メール送信……すべてここに書かれている
        if (userRepository.findByEmail(request.getEmail()).isPresent()) {
            throw new RuntimeException("既に登録されています");
        }
        String hashed = passwordEncoder.encode(request.getPassword());
        User user = new User(request.getName(), request.getEmail(), hashed);
        userRepository.save(user);
        mailSender.send(user.getEmail(), "登録完了しました");
        return UserResponse.from(user);
    }
    // 他のAPIメソッドでも似たようなロジックが繰り返される…
}

このコードには2つの問題があります。1つは、Controllerが「HTTPの受け口」という本来の役割を超えて、業務ロジックまで抱え込んでいること。もう1つは、同じ業務ロジックを別の場所(例えば管理画面用のバッチ処理)から使い回せないことです。

そこで、責務ごとにクラスを分けます。

graph TD
  Ctrl["Controller<br/>HTTPの受け口。<br/>リクエスト/レスポンスの変換に専念"] --> Svc["Service<br/>業務ロジック。<br/>複数のRepositoryを組み合わせる"]
  Svc --> Repo["Repository<br/>データベースとのやり取りに専念"]
  Repo --> DB[("データベース")]
@RestController
class UserApiController {
    private final UserService userService;

    @PostMapping("/api/users")
    public UserResponse create(@Valid @RequestBody CreateUserRequest request) {
        return userService.register(request); // 業務ロジックはServiceに任せる
    }
}

@Service
class UserService {
    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    private final MailSender mailSender;

    @Transactional
    public UserResponse register(CreateUserRequest request) {
        userRepository.findByEmail(request.getEmail()).ifPresent(u -> {
            throw new DuplicateEmailException(request.getEmail());
        });
        User user = new User(request.getName(), request.getEmail(),
                passwordEncoder.encode(request.getPassword()));
        userRepository.save(user);
        mailSender.send(user.getEmail(), "登録完了しました");
        return UserResponse.from(user);
    }
}
責務知っていてよいこと知らなくてよいこと
ControllerHTTPリクエスト/レスポンスの変換URL、DTO業務ルール、SQL
Service業務ロジック業務ルール、トランザクション境界HTTPやSQLの詳細
Repositoryデータ永続化エンティティ、クエリ業務ルール、HTTP

各層が「自分の責務」だけに集中することで、コードの見通しが良くなり、同じServiceを複数のControllerやバッチ処理から使い回すこともできるようになります。これは第2章で学んだDIの考え方——「抽象に依存し、責務を分離する」——をアプリ全体の構造に適用したものだと言えます。

やってみよう

「注文を確定する」処理を、Controller・Service・Repositoryのどこに書くべきか、以下の作業ごとに仕分けしてみましょう:(1)URLからPOSTされたJSONを受け取る、(2)在庫が足りるか業務ルールをチェックする、(3)注文テーブルに1行INSERTする。