Skip to content

bl-hori/springboot-rest-api

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Spring Boot REST API - Tutorial Management System

プロジェクト概要

このプロジェクトは、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         # テストクラス

API エンドポイント

ベースURL: /api

メソッド エンドポイント 説明
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
}

データベース設定

application.properties

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=always

データベーススキーマ

CREATE TABLE tutorials (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  title VARCHAR(255),
  description VARCHAR(255),
  published BOOLEAN
);

ビルドと実行

ビルド

./gradlew build

実行

./gradlew bootRun

WAR デプロイ

./gradlew bootWar
# 生成されたWARファイルをTomcatなどのサーブレットコンテナにデプロイ

コードレビュー項目

1. セキュリティ(重要度: 高)

🔴 機密情報のハードコーディング

  • ファイル: 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}

🟡 CORS設定の範囲

  • ファイル: TutorialController.java:21
  • 問題: http://localhost:4200に固定されたCORS設定
  • 推奨: 環境ごとに設定を外部化、本番環境では適切なオリジンを指定

🟡 入力バリデーション不足

  • ファイル: TutorialController.java:53-60
  • 問題: @RequestBodyに対する入力検証が不足
  • 推奨: @ValidアノテーションとTutorialエンティティに制約を追加
@PostMapping("/tutorials")
public ResponseEntity<Tutorial> createTutorial(@Valid @RequestBody Tutorial tutorial) {
    // ...
}

2. アーキテクチャと設計(重要度: 高)

🔴 レイヤー分離の欠如

  • ファイル: 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;
}

3. エラーハンドリング(重要度: 高)

🟡 一般的すぎる例外処理

  • ファイル: 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()));
    }
}

4. データモデルとデータベース(重要度: 中)

🟡 非推奨のドライバー

  • ファイル: 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 idboolean publishedがプリミティブ型
  • 推奨: LongBooleanに変更(null許容、JPA互換性向上)

5. コード品質とメンテナンス性(重要度: 中)

🟡 マジックバリューの使用

  • ファイル: TutorialController.java:56
  • 問題: falseがハードコーディング
  • 推奨: デフォルト値をエンティティまたは定数で管理

🟡 ドキュメンテーション不足

  • 全ファイル
  • 問題: JavaDocコメントがない
  • 推奨: 全てのpublicメソッドにJavaDocを追加

🟡 リソース命名の一貫性

  • ファイル: TutorialController.java:93
  • 問題: /tutorials/publishedが形容詞を使用
  • 推奨: /tutorials?published=trueのようなクエリパラメータ方式

6. パフォーマンス(重要度: 低)

🟡 N+1クエリ問題の潜在リスク

  • ファイル: TutorialRepository.java
  • 推奨: 将来的にリレーションシップを追加する場合は@EntityGraphJOIN FETCHを検討

🟡 ページング機能の欠如

  • ファイル: TutorialController.java:28-42
  • 問題: 大量データ取得時のパフォーマンス問題
  • 推奨: Pageableパラメータを追加
@GetMapping("/tutorials")
public ResponseEntity<Page<Tutorial>> getAllTutorials(
    @RequestParam(required = false) String title,
    Pageable pageable) {
    // ...
}

7. Spring Boot設定(重要度: 低)

🟡 古いSpring Bootバージョン

  • ファイル: build.gradle:2
  • 問題: Spring Boot 2.7.1は2022年リリース
  • 推奨: Spring Boot 3.x系への移行を検討(Java 17+必須)

🟡 devtoolsがコメントアウト

  • ファイル: build.gradle:23
  • 推奨: 開発環境では有効化を検討

テスト不足の指摘

現在のテスト状況

  • ファイル: ApplicationTests.java
  • 内容: コンテキストロードテストのみ(最小限)
  • カバレッジ: 0% (ビジネスロジックのテストなし)

🔴 必須テストケース(優先度: 高)

1. TutorialController統合テスト

テストすべき項目:

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() {
    // 公開済みが存在しない場合のテスト
}

2. TutorialRepository単体テスト

@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() {
        // 大文字小文字を区別しない検索テスト
    }
}

3. Tutorial エンティティ単体テスト

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() {
        // デフォルトコンストラクタのテスト
    }
}

🟡 追加推奨テスト(優先度: 中)

4. セキュリティテスト

@Test
void api_shouldAcceptCorsFromAllowedOrigin() {
    // CORS設定のテスト
}

@Test
void api_shouldRejectCorsFromUnknownOrigin() {
    // 未許可オリジンからのリクエストテスト
}

5. エラーハンドリングテスト

@Test
void whenDatabaseError_shouldReturn500() {
    // データベースエラー時のテスト
}

@Test
void whenInvalidJson_shouldReturn400() {
    // 不正なJSON形式のテスト
}

6. パフォーマンステスト

@Test
void getAllTutorials_withLargeDataset_shouldCompleteInReasonableTime() {
    // 大量データでのパフォーマンステスト
}

🟢 将来的な改善テスト(優先度: 低)

7. E2Eテスト

  • フロントエンドとの統合テスト
  • Selenium/Cypress等を使用したブラウザテスト

8. 負荷テスト

  • 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' // 実データベーステスト
}

改善の優先順位

🔴 最優先(即座に対応)

  1. 機密情報のハードコーディング解消
  2. サービス層の導入
  3. 基本的なコントローラー統合テストの実装

🟡 高優先(近日中に対応)

  1. 入力バリデーションの追加
  2. エラーハンドリングの改善とログ追加
  3. コンストラクタインジェクションへの移行
  4. Repository単体テストの実装

🟢 中優先(計画的に対応)

  1. 非推奨APIの更新
  2. ページング機能の追加
  3. JavaDocの追加
  4. エンティティのバリデーション強化

まとめ

このプロジェクトは基本的なCRUD操作を提供するSpring Boot RESTアプリケーションとして機能していますが、本番環境への展開やエンタープライズ利用には以下の改善が必要です。

重大な問題:

  • セキュリティ(機密情報管理)
  • テストカバレッジ(ほぼ0%)
  • アーキテクチャ(レイヤー分離不足)

改善後の期待効果:

  • 保守性の向上
  • テスト容易性の向上
  • セキュリティリスクの軽減
  • 拡張性の確保

上記のレビュー項目とテスト実装を段階的に進めることで、品質の高いプロダクションレディなアプリケーションに成長させることができます。

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •  

Languages