Spring Bootでデータ分析APIを作る|Stream API・groupingBy・集計処理をマスター

Spring Bootでデータ分析APIを作る|Stream API・groupingBy・集計処理をマスター

この記事はJava学習シリーズ「課題2」です。まだ環境が整っていない方は Java環境構築ガイド を先に読んでください。課題1(パスワードチェッカー)でSpring Bootの基本(@RestController@PostMapping@RequestBody)を学んでいることを前提とします。


この記事で作るもの

ユーザーデータ(JSON配列)を受け取り、部門別集計・給与ランキング・全体統計を返すデータ分析REST APIです。

APIの使い方(curl):

curl -X POST http://localhost:8080/api/analyze \
  -H "Content-Type: application/json" \
  -d '[
    {"name":"田中太郎","age":35,"department":"開発部","salary":650000},
    {"name":"鈴木一郎","age":42,"department":"開発部","salary":780000},
    {"name":"佐藤花子","age":28,"department":"マーケティング部","salary":450000}
  ]'

レスポンス:

{
  "totalCount": 3,
  "avgAge": 35.0,
  "avgSalary": 626666.7,
  "byDepartment": {
    "開発部":          {"count": 2, "avgSalary": 715000.0},
    "マーケティング部": {"count": 1, "avgSalary": 450000.0}
  },
  "top3": [
    {"rank": 1, "name": "鈴木一郎", "department": "開発部", "salary": 780000},
    {"rank": 2, "name": "田中太郎", "department": "開発部", "salary": 650000},
    {"rank": 3, "name": "佐藤花子", "department": "マーケティング部", "salary": 450000}
  ]
}

この記事で学べること

  • クラス設計(データをオブジェクトとして表現する)
  • Stream APIfilter, sorted, groupingBy):リストを変換・集計するJavaの機能
  • Map(キーと値のペアでデータを管理する)
  • 例外処理@ExceptionHandlerでAPIエラーを適切に返す)

1. 要件定義:何を作るかを整理する

#要件(やること)理由
1POST /api/analyze でユーザーデータのJSON配列を受け取るHTTPリクエストでデータを受け付けるWebサービスにするため
2全体統計(総人数・平均年齢・平均給与)を計算するデータの全体像を把握するため
3部門別に集計する(人数・平均給与)部門ごとの傾向を比較するため
4給与の高い順にTOP3を返すランキング形式で強調表示するため
5空のリストが送られた場合は400エラーを返す不正なリクエストを弾くため

2. プロジェクト作成

start.spring.io でプロジェクトを生成します。

Artifact:   data-analyzer
Java:       21

Dependencies:
  ✓ Spring Web   ← HTTPリクエストとJSONの変換に必要
  ✓ Validation   ← リクエストのバリデーションに使う

3. データモデルのクラスを作る(要件1の準備)

APIで受け取るユーザーデータの構造を先に定義します。「1人のユーザー = 1つのJavaオブジェクト」として扱うために、User クラスを作ります。

なぜクラスに分けるのか? nameagedepartmentsalary の4つの値がバラバラでは「誰のデータか」がわかりにくくなります。1人分のデータをひとまとめにした User クラスを作ることで、user.getName() / user.getSalary() のように明確に扱えます。

src/main/java/com/example/dataanalyzer/User.java を作成します。

package com.example.dataanalyzer;

import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;

// リクエストのJSON配列の1要素を表すクラス
public class User {

    @NotBlank(message = "名前は必須です")
    private String name;

    @Min(value = 0, message = "年齢は0以上です")
    private int age;

    @NotBlank(message = "部門は必須です")
    private String department;

    @Min(value = 0, message = "給与は0以上です")
    private int salary;

    // デフォルトコンストラクタ(Jacksonが JSON→オブジェクト変換に必要)
    public User() {}

    // getter / setter
    public String getName()       { return name; }
    public int getAge()           { return age; }
    public String getDepartment() { return department; }
    public int getSalary()        { return salary; }

    public void setName(String name)             { this.name = name; }
    public void setAge(int age)                  { this.age = age; }
    public void setDepartment(String department) { this.department = department; }
    public void setSalary(int salary)            { this.salary = salary; }
}

デフォルトコンストラクタが必要な理由 Spring Boot内蔵のJacksonライブラリが {"name": "田中"} というJSONを User オブジェクトに変換するとき、「引数なしのコンストラクタ」→「setterで値を設定」という手順を踏みます。デフォルトコンストラクタがないと変換に失敗します。


4. レスポンスのDTOクラスを作る

APIが返すJSONの構造を定義します。record を使うと、getter・コンストラクタ・equals・hashCodeが自動生成されます。

DeptSummary.java(部門ごとの集計結果):

package com.example.dataanalyzer;

public record DeptSummary(int count, double avgSalary) {}

TopUser.java(給与ランキング1件):

package com.example.dataanalyzer;

public record TopUser(int rank, String name, String department, int salary) {}

AnalysisResult.java(APIのレスポンス全体):

package com.example.dataanalyzer;

import java.util.List;
import java.util.Map;

public record AnalysisResult(
    int totalCount,
    double avgAge,
    double avgSalary,
    Map<String, DeptSummary> byDepartment,
    List<TopUser> top3
) {}

5. Stream APIで集計する(要件2〜4の実装)

Stream APIはリストのデータを変換・絞り込み・集計するJavaの機能です。

Stream API(ストリームAPI)とは? Java 8で追加された、リストや配列のデータを変換・集計するための機能です。JavaScriptの map() / filter() / reduce() に相当します。「データの流れ(ストリーム)」を途中で加工するイメージです。メソッドチェーン(. でメソッドをつなげて書く)が特徴です。

全体統計の計算(要件2)

平均値を計算するには mapToInt → average とメソッドをつなげます。

// 平均年齢
double avgAge = users.stream()
    .mapToInt(User::getAge)   // User::getAge は user -> user.getAge() の省略形
    .average()
    .orElse(0);               // データが0件のとき0を返す

// 平均給与(同じ構造)
double avgSalary = users.stream()
    .mapToInt(User::getSalary)
    .average()
    .orElse(0);

部門別集計(要件3)

部門ごとにユーザーをまとめるには Map を使います。

Map(マップ)とは? キーと値のペアでデータを管理するコレクションです。JavaScriptの Object{})に相当します。ここでは「部門名(String)をキー、そこに属するUserリストを値」として使います。

Collectors.groupingBy() を使うと、指定したキーでリストを自動的にグルーピングできます。

import java.util.*;
import java.util.stream.*;

// 部門名をキーとしてUserをグルーピング
Map<String, List<User>> grouped = users.stream()
    .collect(Collectors.groupingBy(User::getDepartment));

// 各部門の人数と平均給与を計算
Map<String, DeptSummary> byDepartment = new LinkedHashMap<>();
grouped.forEach((dept, members) -> {
    double avg = members.stream()
        .mapToInt(User::getSalary)
        .average()
        .orElse(0);
    byDepartment.put(dept, new DeptSummary(members.size(), Math.round(avg * 10.0) / 10.0));
});

給与TOP3の取得(要件4)

sorted() で降順ソートし、limit(3) で上位3件に絞ります。

List<User> sorted = users.stream()
    .sorted(Comparator.comparingInt(User::getSalary).reversed())
    .limit(3)
    .collect(Collectors.toList());

// ランク番号(1位・2位・3位)をつけてTopUserに変換
List<TopUser> top3 = new ArrayList<>();
for (int i = 0; i < sorted.size(); i++) {
    User u = sorted.get(i);
    top3.add(new TopUser(i + 1, u.getName(), u.getDepartment(), u.getSalary()));
}

6. Controllerを作る(完成版)

これまでの要件1〜5をすべて組み合わせた AnalyzerController.java の完成版です。

package com.example.dataanalyzer;

import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.*;
import java.util.stream.*;

@RestController
@RequestMapping("/api")
public class AnalyzerController {

    // 要件1:POST /api/analyze でユーザーリストを受け取り、分析結果を返す
    @PostMapping("/analyze")
    public ResponseEntity<AnalysisResult> analyze(@RequestBody List<@Valid User> users) {

        // 要件5:空のリストは400エラー
        if (users == null || users.isEmpty()) {
            return ResponseEntity.badRequest().build();
        }

        // 要件2:全体統計
        double avgAge    = users.stream().mapToInt(User::getAge).average().orElse(0);
        double avgSalary = users.stream().mapToInt(User::getSalary).average().orElse(0);

        // 要件3:部門別集計
        Map<String, List<User>> grouped = users.stream()
            .collect(Collectors.groupingBy(User::getDepartment));

        Map<String, DeptSummary> byDepartment = new LinkedHashMap<>();
        grouped.forEach((dept, members) -> {
            double avg = members.stream().mapToInt(User::getSalary).average().orElse(0);
            byDepartment.put(dept, new DeptSummary(members.size(), Math.round(avg * 10.0) / 10.0));
        });

        // 要件4:給与TOP3
        List<User> sorted = users.stream()
            .sorted(Comparator.comparingInt(User::getSalary).reversed())
            .limit(3)
            .collect(Collectors.toList());

        List<TopUser> top3 = new ArrayList<>();
        for (int i = 0; i < sorted.size(); i++) {
            User u = sorted.get(i);
            top3.add(new TopUser(i + 1, u.getName(), u.getDepartment(), u.getSalary()));
        }

        return ResponseEntity.ok(new AnalysisResult(
            users.size(),
            Math.round(avgAge * 10.0) / 10.0,
            Math.round(avgSalary * 10.0) / 10.0,
            byDepartment,
            top3
        ));
    }
}

7. 実行してテストする

起動後、以下のcurlコマンドでテストします。

curl -X POST http://localhost:8080/api/analyze \
  -H "Content-Type: application/json" \
  -d '[
    {"name":"田中太郎","age":35,"department":"開発部","salary":650000},
    {"name":"佐藤花子","age":28,"department":"マーケティング部","salary":450000},
    {"name":"鈴木一郎","age":42,"department":"開発部","salary":780000},
    {"name":"高橋美咲","age":31,"department":"人事部","salary":500000},
    {"name":"伊藤次郎","age":29,"department":"開発部","salary":520000},
    {"name":"渡辺明","age":45,"department":"マーケティング部","salary":620000},
    {"name":"小林由美","age":33,"department":"人事部","salary":480000},
    {"name":"加藤健","age":38,"department":"開発部","salary":700000}
  ]'

エラーケースも確認しましょう:

# 空のリストを送る → 400 Bad Request
curl -X POST http://localhost:8080/api/analyze \
  -H "Content-Type: application/json" \
  -d '[]'

8. GitHubに公開する

git init
git add .
git commit -m "feat: データ分析REST API(Spring Boot + Stream API)"
git remote add origin https://github.com/YOUR_NAME/data-analyzer.git
git push -u origin main

README.md に記載する内容例:

# データ分析 REST API

Spring Boot で作成したユーザーデータ分析REST API。
Stream APIで部門別集計・給与ランキング・全体統計を返します。

## 技術スタック
- Java 21 / Spring Boot 3.3 / Stream API

## エンドポイント
| メソッド | パス | 説明 |
|--------|------|------|
| POST | /api/analyze | ユーザーリストを受け取り集計結果を返す |

まとめ:学んだこと

概念内容どこで使ったか
クラス設計データをオブジェクトとして表現するUser クラス
recordイミュータブルなDTOクラスを簡潔に書くDeptSummary / TopUser / AnalysisResult
Stream APIリストの変換・集計・ソートをメソッドチェーンで記述全体統計・TOP3
Collectors.groupingBy()部門でグルーピング部門別集計
Mapキーと値のペアでデータ管理部門名→集計結果
Comparatorソート順の指定給与の降順ソート
ResponseEntityステータスコード付きレスポンス400エラー返却

Javaの学習シリーズ: 環境構築 | 課題1・パスワードチェッカー | 今ここ(課題2・データ分析API) | 課題3・為替変換API | 課題4・Spring Boot Todo API | 課題5・家計簿API

この記事をシェアする

関連記事


CHECK IT OUT

Members Only

サブスク加入者限定の コンテンツを配信中

  • 毎月のAI・自動化トレンドレポート
  • クリエイター向け業務効率化テンプレート
  • ツール選定・SaaS比較まとめ(限定公開)
Discord サーバー & LINE 公式の両方で通知します
クリエイター・個人事業主向け 受付中

「これ自動化できないかな?」 そのアイデア、ITで実現できます。

業務の自動化・お客様向けツール開発・AI活用まで、クリエイター・個人事業主専門のITコンサルタントが対応します。まずは気軽にご相談ください。

LINEで相談 フォームで相談