このプロジェクトは、Spring Boot 2.7.1を使用したRESTful APIアプリケーションです。チュートリアル情報(タイトル、説明、公開状態)を管理するCRUD操作を提供します。
- フレームワーク: Spring Boot 2.7.1
- Java: 11
- データベース: MySQL
- ORM: Spring Data JPA (Hibernate)
- ビルドツール: Gradle 9.1.0
- テンプレートエンジン: Thymeleaf
- パッケージング: WAR
- spring-boot-starter-data-jpa
- spring-boot-starter-web
- spring-boot-starter-validation
- spring-boot-starter-jdbc
- spring-boot-starter-thymeleaf
- mysql-connector-java
- JUnit 5 (テスト)
src/
├── main/
│ ├── java/com/example/application/
│ │ ├── Application.java # メインアプリケーションクラス
│ │ ├── ServletInitializer.java # WAR デプロイメント用初期化クラス
│ │ ├── Tutorial.java # エンティティクラス
│ │ ├── TutorialRepository.java # データアクセス層
│ │ └── TutorialController.java # REST コントローラー
│ └── resources/
│ └── application.properties # アプリケーション設定
└── test/
└── java/com/example/application/
└── ApplicationTests.java # テストクラス
| メソッド | エンドポイント | 説明 |
|---|---|---|
| GET | /api/tutorials |
全チュートリアル取得(オプション: ?title=keywordで検索) |
| GET | /api/tutorials/{id} |
指定IDのチュートリアル取得 |
| GET | /api/tutorials/published |
公開済みチュートリアル一覧取得 |
| POST | /api/tutorials |
新規チュートリアル作成 |
| PUT | /api/tutorials/{id} |
指定IDのチュートリアル更新 |
| DELETE | /api/tutorials/{id} |
指定IDのチュートリアル削除 |
| DELETE | /api/tutorials |
全チュートリアル削除 |
POST /api/tutorials
{
"title": "Spring Boot Tutorial",
"description": "Learn Spring Boot basics",
"published": false
}レスポンス (201 Created)
{
"id": 1,
"title": "Spring Boot Tutorial",
"description": "Learn Spring Boot basics",
"published": false
}spring.datasource.url=jdbc:mysql://127.0.0.1/sample_db
spring.datasource.username=root
spring.datasource.password=pass
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.sql.init.mode=alwaysCREATE TABLE tutorials (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255),
description VARCHAR(255),
published BOOLEAN
);./gradlew build./gradlew bootRun./gradlew bootWar
# 生成されたWARファイルをTomcatなどのサーブレットコンテナにデプロイ- ファイル:
application.properties:2-3 - 問題: データベース認証情報がプレーンテキストで記載
- 影響: 認証情報の漏洩リスク
- 推奨: 環境変数または外部設定ファイル(Spring Cloud Config、Vault等)を使用
# 改善例
spring.datasource.url=${DB_URL:jdbc:mysql://127.0.0.1/sample_db}
spring.datasource.username=${DB_USERNAME}
spring.datasource.password=${DB_PASSWORD}- ファイル:
TutorialController.java:21 - 問題:
http://localhost:4200に固定されたCORS設定 - 推奨: 環境ごとに設定を外部化、本番環境では適切なオリジンを指定
- ファイル:
TutorialController.java:53-60 - 問題:
@RequestBodyに対する入力検証が不足 - 推奨:
@ValidアノテーションとTutorialエンティティに制約を追加
@PostMapping("/tutorials")
public ResponseEntity<Tutorial> createTutorial(@Valid @RequestBody Tutorial tutorial) {
// ...
}- ファイル:
TutorialController.java:25-26 - 問題: コントローラーが直接Repositoryを参照(サービス層がない)
- 影響: ビジネスロジックとプレゼンテーション層が混在、テスタビリティ低下
- 推奨: サービス層を導入
@Service
public class TutorialService {
private final TutorialRepository repository;
public TutorialService(TutorialRepository repository) {
this.repository = repository;
}
public List<Tutorial> getAllTutorials(String title) {
// ビジネスロジック
}
}- ファイル:
TutorialController.java:25-26 - 問題:
@Autowiredフィールドインジェクション使用 - 推奨: コンストラクタインジェクションを使用(不変性、テスタビリティ向上)
private final TutorialRepository tutorialRepository;
public TutorialController(TutorialRepository tutorialRepository) {
this.requireNonNull(tutorialRepository);
this.tutorialRepository = tutorialRepository;
}- ファイル:
TutorialController.java:39-40, 58-59, 80-81, 89-90, 101-102 - 問題: 全ての例外を
Exceptionでキャッチ、エラー詳細がログに記録されない - 推奨:
- 適切なログ出力を追加
- カスタム例外クラスを作成
@ControllerAdviceで集中的な例外ハンドリング
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleResourceNotFound(ResourceNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse(ex.getMessage()));
}
}- ファイル:
application.properties:4 - 問題:
com.mysql.jdbc.Driverは非推奨 - 推奨:
com.mysql.cj.jdbc.Driverを使用
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver- ファイル:
Tutorial.java - 問題: フィールドに制約アノテーションがない
- 推奨:
@NotBlank(message = "Title is required")
@Size(max = 255, message = "Title must be less than 255 characters")
private String title;
@Size(max = 1000, message = "Description must be less than 1000 characters")
private String description;- ファイル:
Tutorial.java:15, 24 - 問題:
long idとboolean publishedがプリミティブ型 - 推奨:
LongとBooleanに変更(null許容、JPA互換性向上)
- ファイル:
TutorialController.java:56 - 問題:
falseがハードコーディング - 推奨: デフォルト値をエンティティまたは定数で管理
- 全ファイル
- 問題: JavaDocコメントがない
- 推奨: 全てのpublicメソッドにJavaDocを追加
- ファイル:
TutorialController.java:93 - 問題:
/tutorials/publishedが形容詞を使用 - 推奨:
/tutorials?published=trueのようなクエリパラメータ方式
- ファイル:
TutorialRepository.java - 推奨: 将来的にリレーションシップを追加する場合は
@EntityGraphやJOIN FETCHを検討
- ファイル:
TutorialController.java:28-42 - 問題: 大量データ取得時のパフォーマンス問題
- 推奨:
Pageableパラメータを追加
@GetMapping("/tutorials")
public ResponseEntity<Page<Tutorial>> getAllTutorials(
@RequestParam(required = false) String title,
Pageable pageable) {
// ...
}- ファイル:
build.gradle:2 - 問題: Spring Boot 2.7.1は2022年リリース
- 推奨: Spring Boot 3.x系への移行を検討(Java 17+必須)
- ファイル:
build.gradle:23 - 推奨: 開発環境では有効化を検討
- ファイル:
ApplicationTests.java - 内容: コンテキストロードテストのみ(最小限)
- カバレッジ: 0% (ビジネスロジックのテストなし)
テストすべき項目:
a) GET /api/tutorials
@SpringBootTest
@AutoConfigureMockMvc
class TutorialControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private TutorialRepository repository;
@Test
void getAllTutorials_shouldReturnAllTutorials() throws Exception {
// Given: データベースに2件のチュートリアルが存在
repository.save(new Tutorial("Title 1", "Desc 1", false));
repository.save(new Tutorial("Title 2", "Desc 2", true));
// When & Then: 全件取得APIを呼び出すと200とリストが返る
mockMvc.perform(get("/api/tutorials"))
.andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(2)))
.andExpect(jsonPath("$[0].title").value("Title 1"));
}
@Test
void getAllTutorials_withTitleFilter_shouldReturnFilteredResults() {
// タイトル検索のテスト
}
@Test
void getAllTutorials_whenEmpty_shouldReturnNoContent() {
// 空の場合のテスト (204 No Content)
}
}b) GET /api/tutorials/{id}
@Test
void getTutorialById_whenExists_shouldReturnTutorial() {
// 存在するIDを指定した場合の正常系テスト
}
@Test
void getTutorialById_whenNotExists_shouldReturn404() {
// 存在しないIDを指定した場合のテスト
}
@Test
void getTutorialById_withInvalidId_shouldReturn400() {
// 無効なID形式のテスト
}c) POST /api/tutorials
@Test
void createTutorial_withValidData_shouldReturn201() {
// 正常な作成テスト
}
@Test
void createTutorial_withNullTitle_shouldReturn400() {
// バリデーションエラーテスト(現在未実装だが将来必要)
}
@Test
void createTutorial_withEmptyBody_shouldReturn400() {
// 空のリクエストボディのテスト
}
@Test
void createTutorial_withTooLongTitle_shouldReturn400() {
// 最大文字数超過テスト
}d) PUT /api/tutorials/{id}
@Test
void updateTutorial_whenExists_shouldUpdateAndReturn200() {
// 更新成功テスト
}
@Test
void updateTutorial_whenNotExists_shouldReturn404() {
// 存在しないリソースの更新テスト
}
@Test
void updateTutorial_partialUpdate_shouldUpdateOnlyProvidedFields() {
// 部分更新テスト(現在の実装では全フィールド更新)
}e) DELETE /api/tutorials/{id}
@Test
void deleteTutorial_whenExists_shouldReturn204() {
// 削除成功テスト
}
@Test
void deleteTutorial_whenNotExists_shouldReturn500() {
// 存在しないリソースの削除(現在は500、本来は404が望ましい)
}
@Test
void deleteTutorial_shouldActuallyRemoveFromDatabase() {
// データベースから実際に削除されることを確認
}f) DELETE /api/tutorials
@Test
void deleteAllTutorials_shouldRemoveAllRecords() {
// 全削除テスト
}
@Test
void deleteAllTutorials_whenEmpty_shouldReturn204() {
// 空の状態での削除テスト
}g) GET /api/tutorials/published
@Test
void findByPublished_shouldReturnOnlyPublishedTutorials() {
// 公開済みのみフィルタリングテスト
}
@Test
void findByPublished_whenNoPublished_shouldReturn204() {
// 公開済みが存在しない場合のテスト
}@DataJpaTest
class TutorialRepositoryTest {
@Autowired
private TutorialRepository repository;
@Test
void findByPublished_shouldReturnOnlyPublished() {
// Given
repository.save(new Tutorial("Published", "desc", true));
repository.save(new Tutorial("Draft", "desc", false));
// When
List<Tutorial> result = repository.findByPublished(true);
// Then
assertThat(result).hasSize(1);
assertThat(result.get(0).getTitle()).isEqualTo("Published");
}
@Test
void findByTitleContaining_shouldReturnMatchingTutorials() {
// 部分一致検索のテスト
}
@Test
void findByTitleContaining_caseInsensitive_shouldMatch() {
// 大文字小文字を区別しない検索テスト
}
}class TutorialTest {
@Test
void constructor_shouldSetAllFields() {
Tutorial tutorial = new Tutorial("Title", "Description", true);
assertThat(tutorial.getTitle()).isEqualTo("Title");
assertThat(tutorial.getDescription()).isEqualTo("Description");
assertThat(tutorial.isPublished()).isTrue();
}
@Test
void setters_shouldUpdateFields() {
// セッターのテスト
}
@Test
void defaultConstructor_shouldCreateEmptyTutorial() {
// デフォルトコンストラクタのテスト
}
}@Test
void api_shouldAcceptCorsFromAllowedOrigin() {
// CORS設定のテスト
}
@Test
void api_shouldRejectCorsFromUnknownOrigin() {
// 未許可オリジンからのリクエストテスト
}@Test
void whenDatabaseError_shouldReturn500() {
// データベースエラー時のテスト
}
@Test
void whenInvalidJson_shouldReturn400() {
// 不正なJSON形式のテスト
}@Test
void getAllTutorials_withLargeDataset_shouldCompleteInReasonableTime() {
// 大量データでのパフォーマンステスト
}- フロントエンドとの統合テスト
- Selenium/Cypress等を使用したブラウザテスト
- JMeter/Gatling等を使用した性能テスト
- 同時接続数の限界テスト
| レイヤー | 目標カバレッジ |
|---|---|
| Controller | 90%以上 |
| Service(実装後) | 95%以上 |
| Repository | 80%以上 |
| Entity | 70%以上 |
| 全体 | 85%以上 |
dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'com.h2database:h2' // インメモリDB
testImplementation 'io.rest-assured:rest-assured:5.3.0' // REST APIテスト
testImplementation 'org.testcontainers:mysql:1.17.6' // 実データベーステスト
}- 機密情報のハードコーディング解消
- サービス層の導入
- 基本的なコントローラー統合テストの実装
- 入力バリデーションの追加
- エラーハンドリングの改善とログ追加
- コンストラクタインジェクションへの移行
- Repository単体テストの実装
- 非推奨APIの更新
- ページング機能の追加
- JavaDocの追加
- エンティティのバリデーション強化
このプロジェクトは基本的なCRUD操作を提供するSpring Boot RESTアプリケーションとして機能していますが、本番環境への展開やエンタープライズ利用には以下の改善が必要です。
重大な問題:
- セキュリティ(機密情報管理)
- テストカバレッジ(ほぼ0%)
- アーキテクチャ(レイヤー分離不足)
改善後の期待効果:
- 保守性の向上
- テスト容易性の向上
- セキュリティリスクの軽減
- 拡張性の確保
上記のレビュー項目とテスト実装を段階的に進めることで、品質の高いプロダクションレディなアプリケーションに成長させることができます。