Java ベースのタスク割当システム設計と実装ガイド

システムアーキテクチャと開発フロー

本ガイドでは、Spring Boot を基盤とした高スループットなタスク割当プラットフォームの構築手順を段階的に解説します。ドメイン駆動設計(DDD)の考え方を一部取り入れ、責務を明確に分割したモジュール構造を目指します。

1. プロジェクト初期化とビルド設定

開発環境の基盤には Maven を採用し、Spring Boot 3.x の依存関係を一括管理します。プロジェクトの骨格作成には初期化テンプレートを活用し、JDK 17 以上で動作確認できるように設定します。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version>
    </parent>
    <groupId>com.example</groupId>
    <artifactId>task-dispatcher</artifactId>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
    </dependencies>
</project>

2. ユーザ認証とアカウント管理

プラットフォームへのアクセス制御には、Spring Security と JWT を組み合わせたステートレス認証を採用します。ユーザ登録時にはパスワードのハッシュ化処理を施し、不正な入力へのバリデーションを実装します。

@Entity
public class Account {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(unique = true, nullable = false)
    private String username;
    @Column(nullable = false)
    private String passwordHash;
    private LocalDateTime createdAt;

    // コンストラクタ、ゲッター、セッター省略
}

@Service
public class AuthenticationService {
    private final AccountRepository accountRepo;
    private final PasswordEncoder encoder;

    public AuthenticationService(AccountRepository accountRepo, PasswordEncoder encoder) {
        this.accountRepo = accountRepo;
        this.encoder = encoder;
    }

    public Account register(String username, String rawPassword) {
        if (accountRepo.existsByUsername(username)) {
            throw new IllegalArgumentException("指定されたユーザー名は既に存在します。");
        }
        Account newAccount = new Account();
        newAccount.setUsername(username);
        newAccount.setPasswordHash(encoder.encode(rawPassword));
        newAccount.setCreatedAt(LocalDateTime.now());
        return accountRepo.save(newAccount);
    }
}

3. タスクの公開とステート管理

コア機能であるタスクの登録処理では、状態遷移を厳密に制御するための Enum を定義します。タスクは `OPEN`, `ASSIGNED`, `COMPLETED`, `CANCELLED` の状態を持ちます。

@Entity
public class WorkItem {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String title;
    private String description;
    private BigDecimal rewardPoints;

    @Enumerated(EnumType.STRING)
    private WorkStatus status = WorkStatus.OPEN;

    @Version
    private Long version;

    // コンストラクタ、ゲッター、セッター省略
}

@Service
@RequiredArgsConstructor
public class WorkItemService {
    private final WorkItemRepository workItemRepo;

    public WorkItem publish(WorkItemRequest request) {
        WorkItem item = new WorkItem();
        item.setTitle(request.getTitle());
        item.setDescription(request.getDescription());
        item.setRewardPoints(request.getRewardPoints());
        return workItemRepo.save(item);
    }
}

4. タスクの取得(Accept)と競合制御

複数のユーザが同時にタスクを取得しようとした場合のデータ整合性を確保するため、JPA の楽観ロック(`@Version`)を活用します。状態が `OPEN` のみを対象として、`ASSIGNED` へ更新するトランザクション処理を実装します。

    @Transactional
    public WorkItem acceptTask(Long itemId, String operatorId) {
        WorkItem target = workItemRepo.findById(itemId)
                .orElseThrow(() -> new ResourceNotFoundException("タスクが存在しません"));

        if (target.getStatus() != WorkStatus.OPEN) {
            throw new IllegalStateException("現在の状態では取得できません。");
        }

        target.setStatus(WorkStatus.ASSIGNED);
        // @Version アノテーションにより、楽観ロックのチェックが自動実行される
        return workItemRepo.save(target);
    }

5. 単体テストカバレッジの確保

ビジネスロジックの品質保証のため、JUnit 5 と Mockito を用いたテストケースを定義します。特に境界条件や例外発生時の動作確認を自動化します。

@ExtendWith(MockitoExtension.class)
class WorkItemServiceTest {
    @Mock
    private WorkItemRepository mockRepo;
    @InjectMocks
    private WorkItemService service;

    @Test
    void testPublishSuccessfully() {
        WorkItemRequest req = new WorkItemRequest("データ検証作業", "CSVファイルの校正", BigDecimal.valueOf(100));
        when(mockRepo.save(any(WorkItem.class))).thenAnswer(invocation -> invocation.getArgument(0));

        WorkItem result = service.publish(req);

        assertEquals("データ検証作業", result.getTitle());
        assertEquals(WorkStatus.OPEN, result.getStatus());
        verify(mockRepo, times(1)).save(any());
    }
}

6. システムテストと本番環境へのデプロイ

開発環境でのテストが完了した後、コンテナ化されたイメージを CI/CD パイプライン経由でクラウド基盤へ展開します。デプロイ後は自動ヘルスチェックエンドポイントとメトリクス収集エージェントを設定し、障害発生時の早期検知とスケーリングを実施します。監視ダッシュボードに CPU 使用率、メモリアロケーション、リクエストレイテンシを可視化することで、プラットフォームの安定稼働を維持します。

タグ: Java Spring Boot jpa REST API JUnit 5

7月24日 03:58 投稿