見出し画像

3分間プログラミング講座 第15回: 3層分離アーキテクチャ ~大規模アプリ開発の基本構造~



登場人物:

  • チャット先生(通称:先生): オブジェクト指向マスター。普段は優しく、たまにユーモアも交えながら解説。

  • ボット助手(通称:ボット君): プログラミングを始めたばかりの若手。素朴な疑問を投げかけるのが得意。


ボット君: 先生!前回はSpring Data JPAで、データベースにデータを保存したり読み込んだりできるようになりました!オブジェクトを操作するだけで、データが残るなんて、本当に魔法みたいです!

先生: その調子だ、ボット君!これで君のアプリケーションはデータを「記憶」できるようになったわけだ。しかし、アプリケーションが大きくなってくると、コードがごちゃごちゃになって、どこに何が書かれているか分からなくなったり、少し変更するだけで他の部分に影響が出てしまったりする、という問題が起こりがちだ。

ボット君: あー、確かに。ゲームとかも、最初のうちは全部一つのファイルに書いてたけど、だんだん訳が分からなくなってきて…どうしたらいいんでしょう?

先生: そこで必要になるのが、アプリケーションの設計をきれいに整理するための考え方、「3層分離アーキテクチャ」だ!これは、大規模なアプリケーション開発で広く使われている、非常に重要な設計の基本構造なんだ。

ボット君: 3層分離!なんだか難しそうですが、これでプログラムがもっと整理できるんですね!



1. 3層分離アーキテクチャとは?



先生:3層分離アーキテクチャ」とは、アプリケーションの機能を、役割に応じて大きく3つの層(レイヤー)に分ける設計思想のことだ。それぞれの層が特定の役割を持ち、互いに連携することで、アプリケーション全体が効率的かつ堅牢に動作するようにする。

ボット君: 役割ごとに分けるってことですか?

先生: その通り!まるで会社の部署のようなものだね。営業部、製造部、経理部、それぞれが自分の仕事に集中して、必要な情報を他の部署から受け取ったり渡したりする。そうすることで、会社全体がスムーズに動く。プログラムも同じだ。

具体的には、以下の3つの層に分けることが多い。

  1. プレゼンテーション層 (Presentation Layer)

  2. サービス層 (Service Layer) またはビジネスロジック層 (Business Logic Layer)

  3. データアクセス層 (Data Access Layer)

先生: これらの層は、通常、上から下への一方通行の依存関係を持つ。つまり、プレゼンテーション層はサービス層を知っているが、サービス層はプレゼンテーション層を知らない、という具合だ。この単方向の依存関係により、下位層の変更が上位層に影響を与えにくくなり、システムの安定性と柔軟性が向上するんだ。



2. 各層の役割とSpring Frameworkでの対応



先生: それでは、それぞれの層がどんな役割を果たすのか、そしてSpring Frameworkではどの技術が対応するのかを見ていこう。


第1層: プレゼンテーション層 (Presentation Layer)



  • 役割:

    • ユーザーからの入力(Webページからのリクエストなど)を受け取り、適切な処理をサービス層に依頼する。

    • サービス層からの処理結果を受け取り、ユーザーに表示する(Webページ、JSONデータなど)。

    • 主にユーザーインターフェース(UI)やAPIのエンドポイントを提供する。

    • 複雑なビジネスロジックは持たないが、入力値の基本的な検証や、表示に関する単純なロジックを含むことがある。

  • Spring Frameworkでの対応:

    • Spring MVC (@Controller, @RestController): **MVCパターン(Model-View-Controller)**に基づいたWebアプリケーションフレームワークで、HTTPリクエストを受け取り、適切な処理を行い、レスポンスを返す部分を担当します。これによりプレゼンテーション層の実装が体系化されます。

    • Thymeleaf, JSPなどのテンプレートエンジン: Webページを生成する部分。

ボット君: Webの画面を作る部分とか、APIの入り口になる部分ですね!

先生: そうだね。ユーザーと直接やり取りする「顔」になる部分だ。

Java

// Presentation Layer の例: タスク管理のAPIコントローラ
import org.springframework.web.bind.annotation.*;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import java.util.List;
import java.util.Optional;

// TaskServiceとTaskエンティティは、以前の回で作成したものを使用する
// import com.example.demo.Task;
// import com.example.demo.TaskService;

@RestController // このクラスがRESTful APIのコントローラであることを示す
@RequestMapping("/api/tasks") // このコントローラが処理するURLの基本パス
public class TaskController {

    private final TaskService taskService; // TaskServiceに依存

    // コンストラクタインジェクションでTaskServiceのインスタンスを注入
    public TaskController(TaskService taskService) {
        this.taskService = taskService;
    }

    // GET /api/tasks: 全てのタスクを取得
    @GetMapping
    public List<Task> getAllTasks() {
        System.out.println("Presentation Layer: 全てのタスク取得リクエストを受信");
        return taskService.getAllTasks(); // Service Layerに処理を依頼
    }

    // GET /api/tasks/{id}: 特定のIDのタスクを取得
    @GetMapping("/{id}")
    public ResponseEntity<Task> getTaskById(@PathVariable Long id) {
        System.out.println("Presentation Layer: ID=" + id + "のタスク取得リクエストを受信");
        Optional<Task> task = taskService.getTaskById(id); // Service Layerに処理を依頼
        return task.map(ResponseEntity::ok) // タスクが見つかれば200 OK
                   .orElseGet(() -> ResponseEntity.notFound().build()); // 見つからなければ404 Not Found
    }

    // POST /api/tasks: 新しいタスクを作成
    @PostMapping
    @ResponseStatus(HttpStatus.CREATED) // 作成成功時はHTTP 201 Createdを返す
    public Task createTask(@RequestBody Task task) { // リクエストボディからTaskオブジェクトをマッピング
        System.out.println("Presentation Layer: 新しいタスク作成リクエストを受信 - Title: " + task.getTitle());
        return taskService.addTask(task); // Service Layerに処理を依頼
    }

    // PUT /api/tasks/{id}: 特定のIDのタスクを更新
    @PutMapping("/{id}")
    public ResponseEntity<Task> updateTask(@PathVariable Long id, @RequestBody Task taskDetails) {
        System.out.println("Presentation Layer: ID=" + id + "のタスク更新リクエストを受信");
        try {
            Task updatedTask = taskService.updateTask(id, taskDetails); // Service Layerに処理を依頼
            return ResponseEntity.ok(updatedTask);
        } catch (RuntimeException e) {
            // 例: タスクが見つからない場合など
            System.err.println("Presentation Layer: タスク更新エラー - " + e.getMessage());
            return ResponseEntity.notFound().build();
        }
    }

    // DELETE /api/tasks/{id}: 特定のIDのタスクを削除
    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT) // 削除成功時はHTTP 204 No Contentを返す
    public void deleteTask(@PathVariable Long id) {
        System.out.println("Presentation Layer: ID=" + id + "のタスク削除リクエストを受信");
        taskService.deleteTask(id); // Service Layerに処理を依頼
    }
}


第2層: サービス層 (Service Layer) またはビジネスロジック層 (Business Logic Layer)



  • 役割:

    • ビジネスロジックを実装する中心的な層。

    • 複数のデータアクセス層の操作を組み合わせて、一つのまとまったビジネス処理を行う。

    • トランザクション管理、バリデーション、セキュリティチェックなども担当する。

    • 特定のUIやデータベースの技術には依存しない

  • Spring Frameworkでの対応:

    • @Service: このアノテーションを付けたクラスがサービス層のコンポーネントであることを示す。

    • @Transactional: このアノテーションは、メソッド内のすべてのデータベース操作を単一のトランザクションとして扱い、全ての操作が成功するか、あるいはどの操作も適用されない(ロールバックされる)ことを保証します。これにより、データの一貫性が保たれます。

ボット君: 前回作ったTaskServiceがこれですね!データベースの操作だけでなく、複数の操作を組み合わせたり、チェックしたりする場所なんですね。

先生: その通り!ここがアプリケーションの「頭脳」にあたる部分だ。

Java

// Service Layer の例: TaskService (再掲)
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

// TaskRepositoryとTaskエンティティは、以前の回で作成したものを使用する
// import com.example.demo.Task;
// import com.example.demo.TaskRepository;

@Service
public class TaskService {

    private final TaskRepository taskRepository; // TaskRepositoryに依存

    // コンストラクタインジェクションでTaskRepositoryのインスタンスを注入
    public TaskService(TaskRepository taskRepository) {
        this.taskRepository = taskRepository;
        System.out.println("Service Layer: TaskServiceが初期化され、TaskRepositoryが注入されました。");
    }

    @Transactional
    public Task addTask(Task task) {
        System.out.println("Service Layer: タスク追加ロジックを実行 - " + task.getTitle());
        // ここでビジネスロジック(例:重複チェック、デフォルト値設定など)を行う
        if (taskRepository.existsByTitle(task.getTitle())) {
            throw new IllegalArgumentException("タイトルが重複しています: " + task.getTitle()); // 重複チェックの例
        }
        return taskRepository.save(task);
    }

    @Transactional(readOnly = true)
    public List<Task> getAllTasks() {
        System.out.println("Service Layer: 全タスク取得ロジックを実行");
        return taskRepository.findAll();
    }

    @Transactional(readOnly = true)
    public Optional<Task> getTaskById(Long id) {
        System.out.println("Service Layer: ID=" + id + "のタスク取得ロジックを実行");
        if (id == null) {
            // 例外処理の例: 不正な引数を渡された場合
            System.err.println("Service Layer Error: IDがnullです");
            throw new IllegalArgumentException("IDはnullであってはなりません。");
        }
        return taskRepository.findById(id);
    }

    @Transactional
    public Task updateTask(Long id, Task updatedTask) {
        System.out.println("Service Layer: ID=" + id + "のタスク更新ロジックを実行");
        return taskRepository.findById(id).map(task -> {
            task.setTitle(updatedTask.getTitle());
            task.setDescription(updatedTask.getDescription());
            task.setCompleted(updatedTask.isCompleted());
            // ここで更新前の状態と更新後の状態を比較するなどの追加ロジックが可能
            return taskRepository.save(task);
        }).orElseThrow(() -> new RuntimeException("ID " + id + "のタスクが見つかりませんでした。"));
    }

    @Transactional
    public void deleteTask(Long id) {
        System.out.println("Service Layer: ID=" + id + "のタスク削除ロジックを実行");
        taskRepository.deleteById(id);
    }

    @Transactional(readOnly = true)
    public List<Task> getIncompleteTasks() {
        System.out.println("Service Layer: 未完了タスク取得ロジックを実行");
        return taskRepository.findByCompletedFalse();
    }

    @Transactional(readOnly = true)
    public List<Task> getTasksCreatedAfter(LocalDateTime date) {
        System.out.println("Service Layer: 指定日時以降に作成されたタスク取得ロジックを実行");
        return taskRepository.findTasksCreatedAfter(date);
    }
}


第3層: データアクセス層 (Data Access Layer)



  • 役割:

    • データベースとの直接的なやり取りを担当する。

    • データの永続化(保存、更新、検索、削除)を行う。

    • 具体的なSQLの生成や、O/Rマッピング(オブジェクトとデータベースの変換)を行う。

    • 複雑なビジネスロジックは持たないが、エンティティの整合性を保つための基本的な検証ルールなどは含まれることがあります。

    • 他の層からは、この層がどのようなデータストアを使用しているか(リレーショナルDBかNoSQLか、PostgreSQLかMySQLかなど)を意識させないようにする(抽象化/カプセル化)。

  • Spring Frameworkでの対応:

    • Spring Data JPA (@Repository, JpaRepository): JPA(Java Persistence API)のラッパーであり、JPA実装(通常はHibernate)を利用して、エンティティとリポジトリインターフェースを通じてデータベース操作を抽象化します。

    • JDBC, MyBatisなど: より低レベルでSQLを直接扱う場合。

ボット君: これは前回やったリポジトリエンティティですね!データベースのことだけを考えている層なんですね。

先生: その通り!ここがアプリケーションの「記憶」にあたる部分だ。

Java

// Data Access Layer の例: TaskRepository と Task エンティティ (再掲)
// Task.java
// import jakarta.persistence.*;
// import lombok.Data;
// import lombok.NoArgsConstructor;
// import java.time.LocalDateTime;
// import org.hibernate.annotations.CreationTimestamp;
// import org.hibernate.annotations.UpdateTimestamp;

@Entity
@Table(name = "tasks")
@Data
@NoArgsConstructor
class Task { // クラス名は大文字始まり
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(nullable = false)
    private String title;
    @Column(columnDefinition = "TEXT")
    private String description;
    @Column(name = "is_completed")
    private boolean completed = false;
    @CreationTimestamp
    private LocalDateTime createdAt;
    @UpdateTimestamp
    private LocalDateTime updatedAt;
}

// TaskRepository.java
// import org.springframework.data.jpa.repository.JpaRepository;
// import org.springframework.data.jpa.repository.Query;
// import org.springframework.data.repository.query.Param;
// import org.springframework.stereotype.Repository;
// import java.time.LocalDateTime;
// import java.util.List;

@Repository
interface TaskRepository extends JpaRepository<Task, Long> {
    List<Task> findByCompletedFalse();
    List<Task> findByTitleContainingIgnoreCase(String title);
    boolean existsByTitle(String title);

    @Query("SELECT t FROM Task t WHERE t.createdAt >= :startDate ORDER BY t.createdAt DESC")
    List<Task> findTasksCreatedAfter(@Param("startDate") LocalDateTime startDate);
}


3. 3層分離アーキテクチャのメリット



先生: このように層を分けることで、たくさんのメリットがあるんだ。

  1. 関心の分離 (Separation of Concerns):

    • それぞれの層が明確な役割を持つため、「どこに何を書くべきか」が分かりやすくなる。

    • これにより、コードの可読性が向上し、新しいメンバーがプロジェクトに参加した際も理解しやすくなる。

  2. 変更への強さ (Maintainability & Flexibility):

    • たとえば、WebフレームワークをSpring MVCから別のものに変更したい場合、プレゼンテーション層だけを修正すればよく、サービス層やデータアクセス層には影響が出にくい。

    • データベースの種類を変更する場合も、データアクセス層だけを修正すれば、サービス層やプレゼンテーション層は変更する必要がない。

  3. 再利用性 (Reusability):

    • サービス層のビジネスロジックは、Webアプリケーションだけでなく、デスクトップアプリケーションやバッチ処理など、様々な場所から再利用できる。

  4. テストのしやすさ (Testability):

    • 各層が独立しているため、他の層に依存せずに単体テストを行いやすくなる。例えば、サービス層のテストでは、実際のデータベースではなく、モック(テスト用のダミー)のデータアクセス層を差し込むことができる。

  5. 並行開発 (Parallel Development):

    • 複数の開発者がそれぞれの層の作業を並行して進めやすくなる。

ボット君: なるほど!役割分担がしっかりしているから、変更にも強いし、テストもしやすいんですね!これで大きなプログラムでも、みんなで開発できるようになるのか!

先生: その通り!3層分離アーキテクチャは、効率的で、品質が高く、将来の変更にも柔軟に対応できるアプリケーションを構築するための、非常に効果的な設計パターンなんだ。



4. アプリケーションの実行の流れ



先生: これまでの話を、タスク作成の例で見てみよう。

  1. プレゼンテーション層 (TaskController):

    • ユーザーがWebブラウザから「新しいタスクを作成」ボタンをクリックし、POST /api/tasksへリクエストを送信。

    • TaskControllerがこのリクエストを受け取る。

    • TaskControllerは、受け取ったタスク情報をサービス層のTaskService.addTask()メソッドに渡す。

  2. サービス層 (TaskService) またはビジネスロジック層:

    • TaskService.addTask()メソッドがTaskControllerからタスク情報を受け取る。

    • ビジネスロジック(例:同じタイトルのタスクが既に存在しないかTaskRepository.existsByTitle()で確認)を実行。

    • 問題なければ、データアクセス層のTaskRepository.save()メソッドにタスクを渡して保存を依頼。

    • @Transactionalアノテーションにより、ここで行われる複数のデータベース操作が1つのトランザクションとして扱われる。

  3. データアクセス層 (TaskRepository):

    • TaskRepository.save()メソッドがTaskServiceからタスクオブジェクトを受け取る。

    • Spring Data JPAはJPAのラッパーであり、JPA実装(通常はHibernate)を利用して、このタスクオブジェクトをデータベースのtasksテーブルの1行に変換し、SQL文を生成してデータベースに実行します。

    • データベースがタスクを保存し、自動生成されたIDなどをオブジェクトに設定して返却。

  4. サービス層 (TaskService) またはビジネスロジック層:

    • データベースに保存されたタスクオブジェクトをTaskRepositoryから受け取り、TaskControllerに返す。

  5. プレゼンテーション層 (TaskController):

    • TaskServiceから受け取った保存済みタスクオブジェクトを、JSON形式などに変換して、Webブラウザにレスポンスとして返す。

    • Webブラウザは、そのデータを受け取って画面に表示する。

ボット君: うわー、こうやって見ると、役割分担が本当にハッキリしてるんですね!データが流れていくのが目に見えるようです!



今日のまとめ!



  • 3層分離アーキテクチャ: アプリケーションの機能を以下の3つの層に分けて設計する考え方。

    • プレゼンテーション層: ユーザーインターフェースやAPIのエンドポイント。ユーザーからの入力受付と結果の表示。

      • Spring:@Controller, @RestController

    • サービス層 (またはビジネスロジック層): ビジネスロジックの中心。データアクセス層の操作を組み合わせてビジネス処理を実行。トランザクション管理なども行う。

      • Spring:@Service, @Transactional

    • データアクセス層: データベースとの直接的なやり取り。データの永続化。

      • Spring:@Repository, JpaRepository

  • メリット:

    • 関心の分離: 各層の役割が明確になり、コードが分かりやすくなる。

    • 変更への強さ: 特定の層の変更が他の層に影響しにくくなる。

    • 再利用性: ビジネスロジックがUIやDBから独立し、再利用しやすくなる。

    • テストのしやすさ: 各層を独立してテストしやすい。

    • 並行開発: 複数人での開発がしやすくなる。

  • 依存関係: 原則として、プレゼンテーション層 → サービス層 → データアクセス層 の一方向。


ボット君: 先生、今日は大規模なプログラムをどうやって整理するのかが、よーく分かりました!これで、もっともっと大きなアプリケーションを作る自信が湧いてきました!

先生: その調子だ、ボット君!3層分離アーキテクチャを理解していれば、どんなに複雑なアプリケーションでも、効率的に、そして堅牢に開発していくことができるだろう。これがプロの現場で当たり前のように使われている設計の基本だ。

先生: さて、これでJavaのオブジェクト指向の基礎から、Spring Frameworkを使ったアプリケーションの骨格まで、一通りの学習を終えたことになる。もちろん、Spring Frameworkには他にもたくさんの強力な機能や、学ぶべきことは山ほどあるが、今回のシリーズで学んだ知識があれば、次のステップへ大きく踏み出せるはずだ。

先生: 次回は、これまでの知識を総動員して、実際に簡単なWebアプリケーションを「設計・実装・リファクタリング」という一連の流れで構築してみよう!

ボット君: わー!今度は実際に作ってみるんですね!楽しみです!ありがとうございました!


次回:第16回:実践演習~簡単なWebアプリを作ってみよう(設計・実装・リファクタリング)~

#オブジェクト指向 #プログラミング初心者 #Java #クラスとメソッド #プログラミング学習 #入門


いいなと思ったら応援しよう!

ナオユキ|物語と知識のコンシェルジュ もしこの記事が少しでも役に立ったり、楽しんでいただけたりしたら、チップをいただけるととても嬉しいです! いただいたチップは、今後のより良いコンテンツ制作のための書籍代📚とnote購入🗒️、他の方へのチップ代💰に使わせていただきます。 いつも応援ありがとうございます!