オニオンアーキテクチャにおけるインフラ層の設計とテスト

## 概要
- **オニオンアーキテクチャ**: ドメインを中心に、UI・アプリケーション層・インフラ層を外側に配置し、疎結合と保守性を高める設計手法  
- **本記事の目的**: NestJS + TypeORM を想定し、インフラ層（リポジトリ）の設計やテスト戦略、**複数集約連携**で使われる**ドメインイベント**と**Saga**の概念を解説

---

## 1. オニオンアーキテクチャとは

```mermaid
flowchart LR
  A[UI層<br/>Controller] --> B[アプリケーション層<br/>UseCase / Service]
  B --> C[ドメイン層<br/>Entities / Domain Services / Aggregates]
  C --> D[インフラ層<br/>Repository / DB / External Services]
```

1. **UI層**  
   - コントローラなどでリクエスト受付・レスポンス生成  
2. **アプリケーション層**  
   - ユースケース（ビジネスロジックの大まかな流れ）を定義し、外部サービスや複数集約間のやりとりをまとめる  
3. **ドメイン層**  
   - ビジネスルールをエンティティや集約 (Aggregate) として定義。**ドメインイベント**もここで扱う  
4. **インフラ層**  
   - DBや外部APIとの接続、リポジトリ実装など技術的関心を集約し、上層に抽象化して提供

> **Key Point**: 「ドメイン（ビジネスロジック）中心の設計」と「外部依存をインフラ層に閉じ込める」ことが重要

---

## 2. インフラ層の役割

### 2.1 リポジトリ
- **データアクセス**を抽象化し、ドメイン層から DB や ORM の詳細を隠す
- エンティティごと、あるいは集約単位で用意する (例: `UserRepository`, `OrderRepository`)

```ts
// IUserRepository.ts (インターフェース)
export interface IUserRepository {
  findById(id: number): Promise<User | null>;
  save(user: User): Promise<User>;
}
```

```ts
// user.repository.impl.ts (実装例)
@Injectable()
export class UserRepositoryImpl implements IUserRepository {
  constructor(private dataSource: DataSource) {}

  private get repo(): Repository<User> {
    return this.dataSource.getRepository(User);
  }

  async findById(id: number): Promise<User | null> {
    return this.repo.findOne({ where: { id } });
  }

  async save(user: User): Promise<User> {
    return this.repo.save(user);
  }
}
```

### 2.2 外部サービス連携
- REST API、gRPC、メッセージング（RabbitMQ, Kafkaなど）といった **技術的依存**を一手に引き受ける  
- アプリケーション層やドメイン層には **抽象インターフェース**だけ提供し、実装詳細は隠す

---

## 3. ドメインイベントと Saga とは？

### 3.1 ドメインイベント
- **ドメイン層で起きる重要な出来事**をオブジェクトとして表現する概念  
- 例:  
  - `UserRegistered`: ユーザーが新規登録された  
  - `OrderCompleted`: 注文が完了した  
- ドメインイベントを発行すると、**イベントハンドラー**がそれを受け取って別の集約を更新したり、通知を送ったりする  
- **同期**でも**非同期**でも可。非同期の場合はイベントブローカー（Kafkaなど）を使うことが多い

**メリット**  
1. **疎結合**: イベントを通して連携するため、発行元と受信先の直接的依存が減る  
2. **拡張性**: 新たな機能を追加したい場合、イベントリスナーを追加するだけでOK

### 3.2 Saga パターン
- **分散トランザクション**や**複数の集約・マイクロサービス間のワークフロー**を管理する設計パターン  
- 大規模システムやマイクロサービスアーキテクチャで、**各サービスのローカルトランザクション**を「ゆるやかに」つなぐ  
- Sagaはドメインイベントをトリガーに次のステップを呼び出し、ロールバック（補償トランザクション）を発行する流れを整備する

**例**  
1. `OrderPlaced` イベントが発火  
2. Saga が受け取り、`PaymentService` に支払い要求  
3. `PaymentConfirmed` イベントが発火  
4. `ShippingService` へ出荷指示  
5. エラー時は補償トランザクションで状態を元に戻す

> **Key Point**: Saga は**イベント駆動**でステップを連鎖させる仕組み。単一DB内のトランザクションが困難な場合に力を発揮

---

## 4. 複数集約連携：トランザクション vs ドメインイベント／Saga

### 4.1 単一DB・小規模ならトランザクション
- **アプリケーション層**で複数集約を同一トランザクションにまとめ、整合性を保つ  
- **外部サービス呼び出し**はトランザクション外にするのがベター

```ts
@Injectable()
export class OrderService {
  constructor(
    private dataSource: DataSource,
    private userRepo: UserRepositoryImpl,
    private orderRepo: OrderRepositoryImpl
  ) {}

  async processOrder(userId: number, orderId: number): Promise<void> {
    const queryRunner = this.dataSource.createQueryRunner();
    await queryRunner.startTransaction();

    try {
      // ユーザー集約 (ポイント操作)
      const user = await queryRunner.manager.findOneOrFail(User, { where: { id: userId } });
      user.points -= 10;
      await queryRunner.manager.save(user);

      // オーダー集約 (ステータス更新)
      const order = await queryRunner.manager.findOneOrFail(Order, { where: { id: orderId } });
      order.status = 'COMPLETED';
      await queryRunner.manager.save(order);

      await queryRunner.commitTransaction();
    } catch (err) {
      await queryRunner.rollbackTransaction();
      throw err;
    } finally {
      await queryRunner.release();
    }
  }
}
```

### 4.2 大規模・分散ならドメインイベント or Saga
- **ドメインイベント**: 集約Aで状態が変わったら `DomainEvent` を発行し、集約Bや別サービスがそれを受けて処理する  
- **Saga**: 分散した複数サービス間のトランザクションシーケンスをイベント駆動で管理。エラー時の補償トランザクションも定義

> **Best Practice**:  
> - シンプルなアプリ ⇒ トランザクションを使い倒す  
> - 大規模 or マイクロサービス ⇒ イベント + Saga でゆるやかな整合性  

---

## 5. テスト戦略（ユニット／統合／E2E）

### 5.1 ユニットテスト
- **目的**: ビジネスロジック（ドメインサービスやアプリケーションサービス）の検証  
- **手法**: リポジトリを**モック**化し、期待どおりの呼び出しが行われるかテスト

```ts
describe('UserService', () => {
  let userService: UserService;
  let mockUserRepo: IUserRepository;

  beforeEach(() => {
    mockUserRepo = {
      findById: jest.fn(),
      save: jest.fn(),
    } as IUserRepository;

    userService = new UserService(mockUserRepo);
  });

  it('should update user name correctly', async () => {
    (mockUserRepo.findById as jest.Mock).mockResolvedValue({ id: 1, name: 'OldName' });
    await userService.updateUserName(1, 'NewName');

    expect(mockUserRepo.save).toHaveBeenCalledWith({ id: 1, name: 'NewName' });
  });
});
```

### 5.2 統合テスト
- **目的**: 実際のDBやリポジトリを使用し、CRUDやトランザクション動作をチェック  
- **手法**: Docker などでテスト用DBを立ち上げ、`TypeOrmModule.forRoot` をテスト環境設定にする

```ts
describe('UserRepository Integration', () => {
  let userRepository: UserRepositoryImpl;

  beforeAll(async () => {
    const moduleRef = await Test.createTestingModule({
      imports: [
        TypeOrmModule.forRoot({
          type: 'postgres',
          host: 'localhost',
          port: 5432,
          username: 'test',
          password: 'test',
          database: 'testdb',
          synchronize: true,
          entities: [User],
        }),
        TypeOrmModule.forFeature([User]),
      ],
      providers: [UserRepositoryImpl],
    }).compile();

    userRepository = moduleRef.get(UserRepositoryImpl);
  });

  it('should create a user', async () => {
    const user = await userRepository.save({ name: 'IntegrationTestUser' } as User);
    expect(user.id).toBeDefined();
    expect(user.name).toBe('IntegrationTestUser');
  });
});
```

### 5.3 E2Eテスト
- **目的**: Controller経由で複数集約・外部サービスを含むシナリオを通し、DBの整合性やレスポンスを検証  
- **活用シーン**: Sagaを実装している場合や、ドメインイベント発行を含むフローの確認

> **Key Point**: テスト段階でも、**トランザクション or イベント駆動**のシナリオをしっかりカバーする

---

## 6. アンチパターンと回避策

1. **リポジトリにビジネスロジックを詰め込む**  
   - **回避**: リポジトリはデータアクセスのみ、ロジックはドメインサービスやアプリケーションサービスへ  
2. **巨大なトランザクション**  
   - **回避**: 集約ごとに切り分け、必要に応じてイベント駆動 (Saga) も検討  
3. **トランザクション内で外部サービスを呼ぶ**  
   - **回避**: ロールバック時に整合性が崩れるリスクあり、基本的に外部呼び出しは分離  
4. **Anemic Domain Model**  
   - **回避**: ドメインイベントやドメインサービスなどを活用し、ビジネスロジックをドメイン層に集約  
5. **1つのリポジトリに複数集約を詰め込む**  
   - **回避**: 単一責務 (SRP) を守り、集約ごとにリポジトリを分割

---

## 7. まとめ

1. **オニオンアーキテクチャ**: ドメインを最重要視し、インフラ依存を外側に隔離する設計  
2. **ドメインイベント**: ドメインで起こった「重要な出来事」を通知し、疎結合に集約間・サービス間を連携する  
3. **Saga**: 分散トランザクションや複数サービス連携を**イベント駆動**で管理し、ロールバックにも対応するパターン  
4. **テスト戦略**: ユニット・統合・E2Eの3レイヤーで、トランザクションやイベント駆動のシナリオを含めて検証  

以上のポイントを踏まえれば、ドメインロジックをしっかり守りながら、複数集約・分散環境での整合性確保を実現できます。ぜひご自身のプロジェクトに応用してみてください。