目次
- はじめに
- java, spring概要
- パッケージ構成
- コード解説
- テスト
- まとめ
はじめに
この記事をご覧いただきありがとうございます。
はじめまして、石川 聖(ヒジリ)と申します。
僕は今年の4月、あるIT企業に入社し、社会人・エンジニアとしてのキャリアを歩みはじめました。
その会社の新入社員研修でJavaやSpringを学んで得た知識・気づきを、本記事で駆け出しエンジニアの同志に共有したいと思います。
Javaというと「古い」「難しそう」というイメージを持っている方もいるかもしれませんが、実際に触ってみると(eclipseという開発環境では)意外にも初心者にやさしいと感じました。フレームワークであるSpringも、最初はとっつきにくい印象がありましたが、使い方が分かってくるととても便利です。
この記事では、私自身の学習の振り返りも兼ねて、JavaとSpringを使って簡単なWebアプリ(いわゆるToDoアプリ)を作っていく過程をまとめてみました。
開発環境の構築から、DB接続、テストの書き方まで、初心者の方にもできるだけわかりやすく紹介していきたいと思います。
これからJavaやSpringに挑戦してみたい方の参考になれば幸いです。
java, spring概要
Javaとは?
Javaは、1990年代に登場したプログラミング言語で、オブジェクト指向(OOP)の考え方を本格的に普及させた代表的な言語です。それ以前にも「Smalltalk」のようなオブジェクト指向言語は存在していましたが、高額な有料ライセンスが必要であることやマシンスペックの制約などがあり、研究機関でしか使われておらず、一般の開発者にはあまり馴染みがありませんでした。
そんな中、Javaはオープンソースとして誰でも無料で使える形で登場し、多くの開発者の手に届くようになりました。オブジェクト指向の基本的な機能をひと通り備えており、さらに長年にわたってバージョンアップが続けられてきた結果、今では関数型プログラミングの要素や、非同期処理の仕組みなど、非常に多機能な言語となっています。
「世の中のプログラムの半分はJavaで動いている」と言われるほど、企業の業務システムやWebサービス、Androidアプリの開発など、さまざまな現場で使われています。
Springとは?
Springは、JavaでWebアプリケーション開発やDIコンテナのためのフレームワークです。特に、次のような特徴を持っています。
- DI(依存性注入):複雑なクラス同士の関係を、コードの外側からうまく管理してくれる仕組み
- AOP(アスペクト指向):ログ出力やエラーハンドリングなど、共通処理を自動的に追加できる
- MVCアーキテクチャ:アプリをModel(データ)、View(画面)、Controller(入力受付・制御)の3つに分けて構成できる
これらの仕組みのおかげで、実際の開発では「やるべきこと」に集中でき、複雑な裏側の処理はSpringがうまくやってくれます。最初は機能が多くて圧倒されがちですが、使い方が分かってくると「これ便利!」と思える場面がたくさんあります。
Eclipseとは?
Eclipse(エクリプス)は、Javaをはじめとしたさまざまなプログラミング言語に対応した統合開発環境(IDE)です。少し昔(2001年)からあるので、「UIがちょっと古臭い」と思う人もいますが(自分)、実は初心者にも使いやすい一面があります。
Eclipseには、Javaの開発に必要な設定や仮想環境が最初から用意されており、SpringプロジェクトもGUI操作で簡単に作成できます。プロジェクトの作成やビルド、依存関係の管理など、ほとんどの作業をマウス操作で完結できるため、初学者にとっては安心感があります。
(インストールはこの公式サイトから。
)
操作例
この動画のようにspringなどでのプロジェクトの作成, パッケージ・クラスの追加, コード・テストの実行までを全てGUIで行うことができます。
パッケージ構成
アプリケーションを作るとき、クラスをパッケージ(package)という単位で整理するのが一般的です。これは、いわば「フォルダ分け」に近い感覚で、機能ごとにクラスを分類することで、コード全体を見通しやすくし、管理しやすくするための仕組みです。
今回のアプリでは、以下のようなパッケージ構成にしてみました。
このような構成は「レイヤードアーキテクチャ」と呼ばれる考え方に基づいています。正直に言うと、今回のような小さなToDoアプリには大げさな設計ですが、実務でよく使われている構成であり、各層の役割を理解しておくことで、より複雑なアプリを開発する際にとても役立ちます。
-
controller
ユーザーからのリクエストを受け取り、適切な処理へ橋渡しをする層です。いわば「受付係」のような役割です。
-
service
アプリケーションのビジネスロジック(処理の中身)を担当します。controllerとrepositoryの中間に入り、処理の流れを制御します。
-
repository
データベースとのやり取りを担当します。SpringではJPAという仕組みを使って、SQLを直接書かずにDB操作ができるのが特徴です。
-
model
データの設計図(エンティティ)を定義する場所です。テーブルと1対1で対応するクラスを作成し、JPAがこれをもとにデータの読み書きを行います。
(他にも色々なアーキテクチャの種類があり、プロジェクトに応じて選択します。)
実際には、このような構成になっています。
/src/main/java以下に各パッケージがあります。
コード解説
まず、完成したアプリの画面です。
入力欄に文字を入力し、Addボタンを押すと、
タスクが追加されます。
バックエンドのコンソールで、データベースに追加した履歴が確認できます。
...
Hibernate: insert into task (title) values (?) returning id
Hibernate: select t1_0.id,t1_0.title from task t1_0
次にデータベースへの接続を解説します。
dockerでpostgreSQlサーバーを立てます。environmentにある情報を接続に使います。
version: 3.8
services: db:
image: postgres:16
container_name: my_postgres
restart: always
environment:
POSTGRES_USER: myuser
POSTGRES_PASSWORD: mypassword
POSTGRES_DB: mydb
ports:
- "5432:5432"
volumes:
- db-data:/var/lib/postgresql/data
springプロジェクトを作ると/src/main/resource/application.propertiesが生成されます。そこに接続情報を書きます。
spring.application.name=TaskApp1
# ここに接続情報
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
spring.datasource.username=myuser
spring.datasource.password=mypassword
spring.jpa.hibernate.ddl-auto=update # modelの更新を自動でDBに反映
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
本命の各パッケージのコードを解説していきます。
model/Task.java
package TaskApp1.model;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class Task {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
// --- Getter ---
public Long getId() {
return id;
}
public String getTitle() {
return title;
}
// --- Setter ---
public void setId(Long id) {
this.id = id;
}
public void setTitle(String title) {
this.title = title;
}
}
このTaskクラスは、todo(タスク管理)アプリで使うタスクの情報を表しています。
でも、ただのデータクラスではなく、Springの機能を使ってこのクラスの内容を自動的にデータベースに保存できるようにしています。
@Entity
このクラスの一番上についている @Entity
は、「このクラスはデータベースのテーブルとつながっていますよ」という意味のマークです。
このおかげで、Springが自動的にテーブルを作ってくれます。たとえばこのクラスだと:
-
id
→ テーブルの「idカラム(番号)」になります -
title
→ テーブルの「titleカラム(タイトル)」になります
@Id
と @GeneratedValue
-
@Id
は「この変数が主キー(識別番号)です」というマークです。 -
@GeneratedValue(...)
は「番号は自動的にふってくださいね」という指定です。
つまり、新しくタスクを追加するたびに、自動で1, 2, 3…と番号をつけてくれるんです。
先述のapplication.propertiesのspring.jpa.hibernate.ddl-auto=update
の設定によって、自動でデータベースのテーブルが生成されます。
repository/TaskRepository.java
package TaskApp1.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import TaskApp1.model.Task;
public interface TaskRepository extends JpaRepository<Task, Long>, TaskRepositoryInterface {
}
この TaskRepository
は、タスクのデータ(Task)をデータベースに保存・取得・削除するためのクラスです。
extends JpaRepository<Task, Long>
の意味
この部分がとても重要です。
-
JpaRepository
は、Spring Data JPA が用意している「データ操作の便利セット」です。 -
<Task, Long>
は、「このリポジトリはTask
というクラスを扱い、その主キーはLong
型です」という意味です。
これを継承すると、たくさんの便利なメソッドが自動的に使えるようになります。
たとえば以下のようなメソッドが、何も書かずに使えるようになります。
メソッド名 | 説明 |
---|---|
findAll() |
全てのデータを取得 |
findById(id) |
IDを指定して1件取得 |
save(task) |
データを保存(新規 or 更新) |
deleteById(id) |
IDを指定して削除 |
count() |
件数を数える |
(TaskRepositoryInterfaceも継承していますが、後のテストで使います。)
service/TaskController.java
package TaskApp1.service;
import java.util.List;
import org.springframework.stereotype.Service;
import TaskApp1.model.Task;
import TaskApp1.repository.TaskRepositoryInterface;
@Service
public class TaskService {
private TaskRepositoryInterface taskRepository;
public TaskService(TaskRepositoryInterface taskRepository) {
this.taskRepository = taskRepository;
}
public List<Task> getAllTasks() {
return taskRepository.findAll();
}
public Task createTask(Task task) {
return taskRepository.save(task);
}
public void deleteTask(Long id) {
taskRepository.deleteById(id);
}
}
TaskService
は、「何をするか」の処理の流れ(ロジック)を担当しています。
データの保存・取得・削除などの操作を、直接 Repository
に書くのではなく、一度 Service
を通して使うようにしています。
これにより、たとえば将来「保存前に入力チェックしたい」などの処理を Service
に追加することで、コードを整理しやすくなります。(この例の場合、必ずしもserviceに書く必要はなく、modelにバリデーションロジックを集約した方が良いこともあります。)
例えば、
public List<Task> getAllTasks() {
return taskRepository.findAll();
}
ここでは、先ほどのrepositoryのコードのfindall()を呼び出して、全てのタスクのデータを返すメソッドです。
@Service
このアノテーション(デコレータ)は「このクラスはサービス層です」とSpringに教えています。(Springが自動的に管理してくれるようになり、次のcontrollerで自動的に初期化されます(DIされる))
controller/TaskController.java
package TaskApp1.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import TaskApp1.model.Task;
import TaskApp1.service.TaskService;
tankaController
public class TaskController {
@Autowired
private TaskService taskService;
@GetMapping("/")
public String list(Model model) {
model.addAttribute("tasks", taskService.getAllTasks());
model.addAttribute("task", new Task());
return "index";
}
@PostMapping("/add")
public String add(@ModelAttribute Task task) {
taskService.createTask(task);
return "redirect:/";
}
@PostMapping("/delete/{id}")
public String delete(@PathVariable Long id) {
taskService.deleteTask(id);
return "redirect:/";
}
}
このクラスは、「ユーザーからのリクエストを受け取る係」です。
画面(Webページ)でボタンを押したり、フォームを送信したりしたときに動きます。何か処理をしたいときは TaskService
を呼び出します。
@Controller
このクラスは「コントローラ層(Webリクエストを処理する)」とSpringに認識させるためのアノテーション(デコレータ)です。
@Autowired
@Autowired
private TaskService taskService;
このように書くと、SpringがTaskService
を自動で探して、このクラスの中に入れてくれます。
(このスクショのようにコントローラ層でAutowiredを使った場合、サービス層のクラスを自動的に代入する様です。そのため、サービス層のクラスに@serviceのアノテーションを書く必要がある。)
これで、自分でnew TaskService()
と書かなくても使えるようになります。
フロントエンド
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head><title>Task List</title></head>
<body>
<h1>Tasks</h1>
<form th:action="@{/add}" method="post">
<input type="text" name="title" />
<button type="submit">Add</button>
</form>
<ul>
<li th:each="task : ${tasks}">
<span th:text="${task.title}"></span>
<form th:action="@{/delete/{id}(id=${task.id})}" method="post">
<button type="submit">Delete</button>
</form>
</li>
</ul>
</body>
</html>
<html xmlns:th="[http://www.thymeleaf.org](http://www.thymeleaf.org/)">
は、Thymeleafを使うための設定です。
Thymeleafは、Spring Bootと組み合わせてよく使われるテンプレートエンジンで、動的にWebページを生成できます。
<form th:action="@{/add}" method="post">
<input type="text" name="title" />
<button type="submit">Add</button>
</form>
これは、新しいタスクを追加するためのフォームです。
th:action="@{/add}"
は、フォームが送信されたときに、Spring Controllerの/add
にリクエストを送っています。先ほどの、controllerの/addの処理により、新たなタスクが作成されます。(method="post"
なので、データはPOSTメソッドで送信されます。)
<ul>
<li th:each="task : ${tasks}">
<span th:text="${task.title}"></span>
<form th:action="@{/delete/{id}(id=${task.id})}" method="post">
<button type="submit">Delete</button>
</form>
</li>
</ul>
th:each="task : ${tasks}"
は、tasks
というリストの中身を1つずつ表示するループです。Spring Controllerからtasks
が渡されると、その中のタスク(task
)を表示します。
th:text="${task.title}"
で、各タスクのタイトルを表示しています。
各タスクの横にある「Delete」ボタンは、そのタスクを削除するためのフォームです。th:action="@{/delete/{id}(id=${task.id})}"
で、削除するタスクのIDをURLに渡し、Spring Controllerにリクエストを送ります。(controllerの/delete/{id}の処理で該当のタスクが削除されます。)
つまり、addやdeleteのボタンを押すとタスクが追加, 削除され、ページが再度表示→変更後のタスクの一覧が表示されるという処理の流れで動いています。(このような仕組みをマルチページアプリケーション(MPA)と言います。)
これまで解説したように、各層の連携によってデータの追加や削除といった処理を明快なコードで実装できます。
テスト
次にテストコードの解説をしていきます。
テストの必要性
アプリを作っていて、「本当にこの処理で合ってるかな?」「あとで変更して壊れたりしないかな?」と不安になることはありませんか?
そんなときに力を発揮するのが テストコード です。
テストとは、プログラムが期待どおりに動いているかを自動で確認するしくみのことです。
ボタンを押したり、APIを手動で確認したりしなくても、コードで書いたチェック項目を自動で実行してくれます。
テストを書くと、次のようなメリットがあります。
-
バグの早期発見
「この関数、ちゃんと動いてるかな?」という確認を自動化できます。
-
リファクタリングの安心感
コードをキレイに書き直しても、テストが通れば「壊れていない」と分かります。
-
チーム開発でも信頼される
他の人が書いたコードが、テストで壊れていないと分かれば安心して開発できます。
つまり、テストは「アプリの安全装置」。初めはちょっと面倒に感じるかもしれませんが、長く使うコードほどテストがあると安心です。
2. テストの種類(ざっくり)
テストにはいくつかの種類がありますが、ここでは代表的なものを3つ紹介します。
単体テスト(ユニットテスト)
ひとつのクラスやメソッドが、単独で正しく動くかを確認するテストです。
たとえば「タスクのタイトルが保存されるか?」といった、細かい処理を確認します。
本記事では、TaskService の処理をモックと一緒に検証する例を紹介します。
結合テスト(インテグレーションテスト)
クラス同士がつながって正しく動くかを確認するテストです。
コントローラとサービスが連携して、ちゃんと画面に値が出るか?などを確かめます。
今回は、TaskController を MockMvc を使ってテストする例を取り上げます。
エンドツーエンドテスト(E2Eテスト)
画面のクリックや入力など、ユーザーの操作を再現してアプリ全体を検証するテストです。
今回は扱いませんが、大きなアプリでは重要になります。
単体テストの実装(TaskServiceのテスト)
まずは、「TaskService」というクラスの処理が正しく動いているかどうかを確かめるテストを書いてみましょう。
テストコード
package TaskApp1;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.util.List;
import org.junit.jupiter.api.Test;
import TaskApp1.model.Task;
import TaskApp1.repository.TaskRepositoryInterface;
import TaskApp1.service.TaskService;
public class TaskServiceUnitTest {
@Test
public void testGetAllTasks() {
// テスト1 新規タスクの作成
// モックの用意
TaskRepositoryInterface mockRepo = mock(TaskRepositoryInterface.class);
TaskService taskService = new TaskService(mockRepo);
Task task = new Task();
task.setTitle("Unit Test Task");
// モックでfindall()が呼ばれたとき、新たに作ったタスクを返すようにする
when(mockRepo.findAll()).thenReturn(List.of(task));
List<Task> result = taskService.getAllTasks();
// 結果の検証
assertEquals(1, result.size());
assertEquals("Unit Test Task", result.get(0).getTitle());
}
@Test
public void testCreateTask() {
TaskRepositoryInterface mockRepo = mock(TaskRepositoryInterface.class);
TaskService taskService = new TaskService(mockRepo);
Task task = new Task();
task.setTitle("Create Task");
when(mockRepo.save(task)).thenReturn(task);
Task result = taskService.createTask(task);
assertEquals("Create Task", result.getTitle());
verify(mockRepo).save(task);
}
}
(以下のような流れでテストコードは書かれています:
- モックの準備:Mockitoでリポジトリをモックにする
- テスト対象のクラスを作る:TaskService にモックを渡してインスタンスを作成
- テストデータを用意:Taskオブジェクトを用意して、モックに返すよう設定
-
メソッドを呼び出す:実際に
getAllTasks()
やcreateTask()
を呼ぶ - 結果を検証する:返ってきた値や、呼び出しが行われたかを検証)
どんな場面をテストする?
今回は以下の2つの処理をテストします。
- タスク一覧を取得する処理
- 新しいタスクを作成する処理
この TaskService
は、データベースにアクセスするリポジトリ(TaskRepository)を使ってタスクの操作をしていますが、
テストのときは実際のデータベースを使いたくないですよね。
そこで登場するのが Mockito です。
Mockitoで依存部分をモック化する
Mockito は、他のクラスの振る舞いを「偽物(モック)」として再現してくれるライブラリです。
これを使えば、「本物のリポジトリの代わりに、決まった結果を返してくれるモノ」を使ってテストできます。
たとえば、次のようなイメージです:
// リポジトリをモックにする
TaskRepositoryInterface mockRepo = mock(TaskRepositoryInterface.class);
// モックに対して、findAll()を呼んだら特定の結果を返すように指定
when(mockRepo.findAll()).thenReturn(List.of(task));
このようにして、TaskService の中でfindAll()
が呼ばれても、本物のDBアクセスをしないで、指定した結果を返すようになります。
テストの実行と検証
画像のように、…/src/test/java/パッケージ名/テストコード
を右クリックして、実行/JUnitテスト
でテストを実行できます。
少し待つと、テストが完了します。
“実行 : 2/2”で、”エラー : 0”なので、テストを2つともパスしていることが分かります。
結合テストの実装(TaskControllerのテスト)
次は、画面にアクセスしたとき正しく動くかを確認する コントローラのテストです。
テストコード
package TaskApp1;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import TaskApp1.controller.TaskController;
import TaskApp1.model.Task;
import TaskApp1.service.TaskService;
@WebMvcTest(TaskController.class)
public class TaskControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;// MVCモックの作成
@MockBean
private TaskService taskService;
@Test
public void testListTasks() throws Exception {
Task task = new Task();
task.setTitle("Integration Task");
// getAllTasks()が呼ばれたとき、作成したタスクを含むリストを返す
when(taskService.getAllTasks()).thenReturn(List.of(task));
// 結果の検証
mockMvc.perform(MockMvcRequestBuilders.get("/"))// `/`にアクセスしたとき
.andExpect(status().isOk()) // リクエストが正常(status code = 200)
.andExpect(model().attributeExists("tasks")) // model(フロントエンド用のデータ)に`task`が含まれる
.andExpect(view().name("index")); // View名は実際のテンプレートに合わせて調整
}
}
@WebMvcTest
を使ってコントローラだけをテスト
Spring Bootでは、@WebMvcTest
というアノテーションを使うと、コントローラに特化したテスト環境を作れます。
@WebMvcTest(TaskController.class)
public class TaskControllerIntegrationTest {
...
}
このようにすると、Springが「TaskController だけを読み込んだ軽量なアプリ」を立ち上げてくれます。
実際のサーバーは起動しないので、高速に動きます。
MockMvc
で仮想リクエスト
MockMvc
を使うと、「ブラウザからアクセスしたようなリクエスト」をコードでシミュレートできます。
たとえば次のように、トップページ /
に GET リクエストを送ることができます:
mockMvc.perform(MockMvcRequestBuilders.get("/"))
.andExpect(status().isOk())
.andExpect(model().attributeExists("tasks"))
.andExpect(view().name("index"));
このコードでは、次のようなことをチェックしています:
- ステータスコードが 200(= OK)であること
- HTMLに渡されるモデル
tasks
属性が存在すること - 表示するビュー名が
index
であること
コントローラの中では TaskService
を呼び出していますが、
サービス層の本物の処理まではテストの対象にしたくない場合があります。
そのときは、@MockBean
を使ってサービス層をモック化します。
@MockBean
private TaskService taskService;
これで、taskService.getAllTasks()
が呼ばれたときに任意の結果を返すように設定できます。
まとめると、テストコードの構成はこのようになっています。
-
@WebMvcTest
でコントローラだけを読み込む -
@MockBean
でサービスをモック化 -
MockMvc
でHTTPリクエストをシミュレーション - ステータス、モデル属性、ビュー名などを検証
(こちらも単体テストと同様に実行して結果を確認できます。)
まとめ
最後までご覧いただき、ありがとうございました。
この記事では、JavaやSpringに初めて触れたばかりの自分が、研修や個人開発を通して学んだことをもとに、初心者の方向けに解説をしてみました。
Springを使ったアプリ開発は、最初は少し難しそうに見えるかもしれませんが、実は意外と親切で、初心者でも取り組みやすいフレームワークです。DIやMVC、JPAといった用語が出てくると身構えてしまいがちですが、1つ1つ触れてみることで、着実に理解できるようになっていきます。
また、アーキテクチャを意識した構成でポートフォリオを作ったり、テストコードまで書けていると「実務を意識している」と評価されやすく、インターン選考や就活で他の人と差別化するポイントになります。
自分もまだまだ学びの途中ですが、これから一緒に少しずつスキルアップしていけたら嬉しいです。
このアカウントでは週1ペースで投稿しています。
来週は、UMLに関する記事か、以前に書いたUMLの記事の修正、現在読んでいるユースケース駆動開発のまとめにしようかと考えています。
それでは皆さま、また来週。