導入
ここまでの章で、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);
}
}
| 層 | 責務 | 知っていてよいこと | 知らなくてよいこと |
|---|---|---|---|
| Controller | HTTPリクエスト/レスポンスの変換 | URL、DTO | 業務ルール、SQL |
| Service | 業務ロジック | 業務ルール、トランザクション境界 | HTTPやSQLの詳細 |
| Repository | データ永続化 | エンティティ、クエリ | 業務ルール、HTTP |
各層が「自分の責務」だけに集中することで、コードの見通しが良くなり、同じServiceを複数のControllerやバッチ処理から使い回すこともできるようになります。これは第2章で学んだDIの考え方——「抽象に依存し、責務を分離する」——をアプリ全体の構造に適用したものだと言えます。
やってみよう
「注文を確定する」処理を、Controller・Service・Repositoryのどこに書くべきか、以下の作業ごとに仕分けしてみましょう:(1)URLからPOSTされたJSONを受け取る、(2)在庫が足りるか業務ルールをチェックする、(3)注文テーブルに1行INSERTする。