T-CREATOR

NestJS 依存循環(circular dependency)を断ち切る:ModuleRef と forwardRef の実戦対処

NestJS 依存循環(circular dependency)を断ち切る:ModuleRef と forwardRef の実戦対処

NestJS で大規模なアプリケーションを開発していると、必ずと言ってよいほど遭遇するのが依存循環(circular dependency)問題です。これは複数のモジュールやサービスが相互に依存し合うことで発生し、アプリケーションの起動時にエラーが発生したり、予期しない動作を引き起こしたりします。

本記事では、NestJS で依存循環を解決するための実践的な手法として、forwardRefModuleRef の使い方を詳しく解説します。実際のコード例を交えながら、どのような場面でどちらの手法を選ぶべきかを明確に示していきますので、ぜひ最後までお読みください。

背景

NestJS の依存性注入の仕組み

NestJS は Angular にインスパイアされた依存性注入(DI)システムを採用しています。このシステムにより、クラス間の依存関係を自動的に解決し、疎結合な設計を実現できます。

以下の図は、NestJS の基本的な DI フローを示しています。

mermaidflowchart TD
    container[IoC Container] -->|注入| service1[UserService]
    container -->|注入| service2[OrderService]
    service1 -->|利用| repository1[UserRepository]
    service2 -->|利用| repository2[OrderRepository]
    module[AppModule] -->|登録| container

IoC コンテナが各サービスの依存関係を管理し、必要に応じて適切なインスタンスを注入します。このシステムにより、開発者は依存関係の生成や管理を意識せずに済むのです。

依存循環が発生する典型的なケース

依存循環は、以下のような場面でよく発生します。

ケース説明発生頻度
サービス間の相互参照UserService と OrderService が互いを参照
認証・認可の連携AuthService と UserService の循環参照
階層構造の設計ミス親子関係のあるエンティティ間での循環
モジュール分割の不備責任分界点の曖昧さによる循環

最も典型的なのは、ビジネスロジックが複雑になった際にサービス間で相互参照が発生するケースです。

typescript// 問題のあるコード例
@Injectable()
export class UserService {
  constructor(private orderService: OrderService) {}

  async getUserOrders(userId: string) {
    return this.orderService.getOrdersByUserId(userId);
  }
}

@Injectable()
export class OrderService {
  constructor(private userService: UserService) {}

  async createOrder(orderData: any) {
    const user = await this.userService.findById(
      orderData.userId
    );
    // 注文作成処理
  }
}

循環参照がアプリケーションに与える影響

循環参照は以下のような深刻な問題を引き起こします。

mermaidflowchart LR
    start[アプリ起動] --> resolve[依存解決開始]
    resolve --> serviceA[ServiceA 初期化]
    serviceA --> serviceB[ServiceB が必要]
    serviceB --> serviceA_again[ServiceA が必要]
    serviceA_again --> error[Error: Circular dependency]
    error --> crash[アプリケーション停止]

主な影響:

  • 起動時エラー: アプリケーションが正常に起動しない
  • メモリリーク: オブジェクトの循環参照により GC が困難
  • デバッグの困難さ: エラーの原因特定に時間がかかる
  • テストの複雑化: モックの作成が困難になる

課題

依存循環の検出方法

依存循環を早期に発見するには、以下の方法が効果的です。

1. NestJS の組み込みエラーメッセージ

bash[Nest] 12345  - 2024/01/01, 10:30:00 AM   ERROR [ExceptionHandler]
Nest can't resolve dependencies of the UserService (?).
Please make sure that the argument dependency at index [0] is available
in the UserModule context.

Potential solutions:
- If OrderService is a provider, is it part of the current UserModule?
- If OrderService is exported from a separate @Module, is that module imported within UserModule?
  @Module({
    imports: [ /* the Module containing OrderService */ ]
  })

2. 静的解析ツールの活用

以下のコマンドで循環依存を検出できます。

bash# Madge を使用した循環依存検出
yarn add -D madge
npx madge --circular --extensions ts ./src

3. ESLint ルールの設定

json{
  "rules": {
    "import/no-cycle": "error"
  }
}

従来の解決方法の限界

インターフェース分離の限界

typescript// よくある解決策だが根本解決にならない
interface IUserService {
  findById(id: string): Promise<User>;
}

@Injectable()
export class OrderService {
  constructor(
    @Inject('IUserService')
    private userService: IUserService
  ) {}
}

この方法では、実行時の循環依存は解決されません。

モジュール分割の限界

モジュールを細かく分割しても、ビジネスロジック上の関連性が強い場合は、どこかで依存関係が発生してしまいます。

実際のエラーメッセージと症状

依存循環発生時によく見られるエラーパターンを整理しました。

エラータイプメッセージ例原因
Provider 解決エラーNest can't resolve dependenciesサービス間の循環参照
Module 初期化エラーNest cannot create the module instanceモジュール間の循環インポート
実行時エラーMaximum call stack size exceeded無限再帰呼び出し

解決策

forwardRef による前方参照の実装

forwardRef は最も基本的な循環依存解決手法です。依存関係を「遅延評価」することで循環を回避します。

基本的な使い方

typescript// forwardRef を使用したサービスレベルの解決
import {
  Injectable,
  Inject,
  forwardRef,
} from '@nestjs/common';

@Injectable()
export class UserService {
  constructor(
    @Inject(forwardRef(() => OrderService))
    private orderService: OrderService
  ) {}

  async getUserOrders(userId: string) {
    return this.orderService.getOrdersByUserId(userId);
  }

  async findById(id: string): Promise<User> {
    // ユーザー検索処理
    return { id, name: 'Sample User' } as User;
  }
}
typescript@Injectable()
export class OrderService {
  constructor(
    @Inject(forwardRef(() => UserService))
    private userService: UserService
  ) {}

  async createOrder(orderData: CreateOrderDto) {
    const user = await this.userService.findById(
      orderData.userId
    );
    // 注文作成処理
    return {
      id: '1',
      userId: user.id,
      items: orderData.items,
    };
  }

  async getOrdersByUserId(userId: string) {
    // 注文検索処理
    return [{ id: '1', userId, items: [] }];
  }
}

forwardRef の動作原理

mermaidsequenceDiagram
    participant Container as IoC Container
    participant UserService
    participant OrderService

    Container->>UserService: インスタンス作成開始
    UserService->>Container: OrderService を要求(forwardRef)
    Container->>Container: 遅延評価でプレースホルダ作成
    Container->>UserService: プレースホルダを注入
    Container->>OrderService: インスタンス作成開始
    OrderService->>Container: UserService を要求(forwardRef)
    Container->>OrderService: 既存の UserService を注入
    Container->>UserService: 実際の OrderService で置換

このように、forwardRef は依存関係の解決を段階的に行うことで循環を回避します。

ModuleRef を使った動的参照の実装

ModuleRef を使用すると、より柔軟で制御可能な依存解決が可能になります。特に複雑な依存関係や条件付きの依存関係に適しています。

基本的な実装

typescriptimport { Injectable, OnModuleInit } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';

@Injectable()
export class UserService implements OnModuleInit {
  private orderService: OrderService;

  constructor(private moduleRef: ModuleRef) {}

  onModuleInit() {
    this.orderService = this.moduleRef.get(OrderService);
  }

  async getUserOrders(userId: string) {
    return this.orderService.getOrdersByUserId(userId);
  }

  async findById(id: string): Promise<User> {
    return { id, name: 'Sample User' } as User;
  }
}

遅延取得パターン

typescript@Injectable()
export class OrderService {
  constructor(private moduleRef: ModuleRef) {}

  private get userService(): UserService {
    return this.moduleRef.get(UserService);
  }

  async createOrder(orderData: CreateOrderDto) {
    // 必要な時にのみサービスを取得
    const user = await this.userService.findById(
      orderData.userId
    );
    return {
      id: '1',
      userId: user.id,
      items: orderData.items,
    };
  }

  async getOrdersByUserId(userId: string) {
    return [{ id: '1', userId, items: [] }];
  }
}

ModuleRef の主な利点

特徴forwardRefModuleRef
実装の簡単さ
柔軟性
パフォーマンス
デバッグのしやすさ
条件付き依存×

Provider レベルでの循環解決

Provider レベルでの循環依存は、カスタムプロバイダーを活用して解決できます。

typescript// shared.module.ts
@Module({
  providers: [
    {
      provide: 'USER_SERVICE',
      useFactory: (moduleRef: ModuleRef) => {
        return new Proxy(
          {},
          {
            get: (target, prop) => {
              const userService =
                moduleRef.get(UserService);
              return userService[prop].bind(userService);
            },
          }
        );
      },
      inject: [ModuleRef],
    },
  ],
  exports: ['USER_SERVICE'],
})
export class SharedModule {}
typescript// order.service.ts
@Injectable()
export class OrderService {
  constructor(
    @Inject('USER_SERVICE') private userService: any
  ) {}

  async createOrder(orderData: CreateOrderDto) {
    const user = await this.userService.findById(
      orderData.userId
    );
    return {
      id: '1',
      userId: user.id,
      items: orderData.items,
    };
  }
}

Module レベルでの循環解決

モジュール間の循環依存は、双方向の forwardRef で解決します。

typescript// user.module.ts
import { Module, forwardRef } from '@nestjs/common';
import { OrderModule } from './order.module';

@Module({
  imports: [forwardRef(() => OrderModule)],
  providers: [UserService],
  exports: [UserService],
})
export class UserModule {}
typescript// order.module.ts
import { Module, forwardRef } from '@nestjs/common';
import { UserModule } from './user.module';

@Module({
  imports: [forwardRef(() => UserModule)],
  providers: [OrderService],
  exports: [OrderService],
})
export class OrderModule {}

モジュール循環の図解

mermaidflowchart LR
    subgraph "Module Level"
        UserModule -->|forwardRef| OrderModule
        OrderModule -->|forwardRef| UserModule
    end

    subgraph "Service Level"
        UserService -->|inject| OrderService
        OrderService -->|inject| UserService
    end

    UserModule -.-> UserService
    OrderModule -.-> OrderService

双方向の forwardRef により、モジュール間の循環依存を安全に解決できます。

具体例

ユーザーと注文サービス間の循環依存解決

実際の E-commerce システムを想定した実装例を見てみましょう。

要件

  • ユーザーは複数の注文を持つ
  • 注文作成時にユーザー情報の検証が必要
  • ユーザー詳細表示時に注文履歴も表示

Step 1: エンティティの定義

typescript// entities/user.entity.ts
export class User {
  id: string;
  name: string;
  email: string;
  createdAt: Date;
}

// entities/order.entity.ts
export class Order {
  id: string;
  userId: string;
  items: OrderItem[];
  totalAmount: number;
  status: OrderStatus;
  createdAt: Date;
}

export interface OrderItem {
  productId: string;
  quantity: number;
  price: number;
}

export enum OrderStatus {
  PENDING = 'pending',
  CONFIRMED = 'confirmed',
  SHIPPED = 'shipped',
  DELIVERED = 'delivered',
}

Step 2: DTOs の定義

typescript// dto/create-order.dto.ts
export class CreateOrderDto {
  userId: string;
  items: OrderItem[];
}

// dto/user-with-orders.dto.ts
export class UserWithOrdersDto {
  id: string;
  name: string;
  email: string;
  orders: Order[];
  totalSpent: number;
}

Step 3: forwardRef を使用したサービス実装

typescript// services/user.service.ts
import {
  Injectable,
  Inject,
  forwardRef,
} from '@nestjs/common';
import { OrderService } from './order.service';

@Injectable()
export class UserService {
  constructor(
    @Inject(forwardRef(() => OrderService))
    private orderService: OrderService
  ) {}

  async findById(id: string): Promise<User> {
    // 実際の実装では DB から取得
    return {
      id,
      name: 'John Doe',
      email: 'john@example.com',
      createdAt: new Date(),
    };
  }

  async getUserWithOrders(
    userId: string
  ): Promise<UserWithOrdersDto> {
    const user = await this.findById(userId);
    const orders =
      await this.orderService.getOrdersByUserId(userId);

    const totalSpent = orders.reduce(
      (sum, order) => sum + order.totalAmount,
      0
    );

    return {
      ...user,
      orders,
      totalSpent,
    };
  }

  async validateUser(userId: string): Promise<boolean> {
    const user = await this.findById(userId);
    return !!user;
  }
}
typescript// services/order.service.ts
import {
  Injectable,
  Inject,
  forwardRef,
  BadRequestException,
} from '@nestjs/common';
import { UserService } from './user.service';

@Injectable()
export class OrderService {
  constructor(
    @Inject(forwardRef(() => UserService))
    private userService: UserService
  ) {}

  async createOrder(
    createOrderDto: CreateOrderDto
  ): Promise<Order> {
    // ユーザー存在確認
    const isValidUser = await this.userService.validateUser(
      createOrderDto.userId
    );

    if (!isValidUser) {
      throw new BadRequestException('Invalid user');
    }

    const totalAmount = createOrderDto.items.reduce(
      (sum, item) => sum + item.price * item.quantity,
      0
    );

    const order: Order = {
      id: `order_${Date.now()}`,
      userId: createOrderDto.userId,
      items: createOrderDto.items,
      totalAmount,
      status: OrderStatus.PENDING,
      createdAt: new Date(),
    };

    // 実際の実装では DB に保存
    return order;
  }

  async getOrdersByUserId(
    userId: string
  ): Promise<Order[]> {
    // 実際の実装では DB から取得
    return [
      {
        id: 'order_1',
        userId,
        items: [
          { productId: 'prod_1', quantity: 2, price: 1000 },
        ],
        totalAmount: 2000,
        status: OrderStatus.DELIVERED,
        createdAt: new Date('2024-01-01'),
      },
    ];
  }

  async updateOrderStatus(
    orderId: string,
    status: OrderStatus
  ): Promise<Order> {
    // 注文ステータス更新処理
    const order = await this.findById(orderId);
    order.status = status;
    return order;
  }

  private async findById(id: string): Promise<Order> {
    // DB から注文を取得
    return {} as Order;
  }
}

Step 4: コントローラーの実装

typescript// controllers/user.controller.ts
import { Controller, Get, Param } from '@nestjs/common';
import { UserService } from '../services/user.service';

@Controller('users')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Get(':id')
  async getUser(@Param('id') id: string) {
    return this.userService.findById(id);
  }

  @Get(':id/orders')
  async getUserWithOrders(@Param('id') id: string) {
    return this.userService.getUserWithOrders(id);
  }
}
typescript// controllers/order.controller.ts
import {
  Controller,
  Post,
  Body,
  Get,
  Param,
  Patch,
} from '@nestjs/common';
import { OrderService } from '../services/order.service';

@Controller('orders')
export class OrderController {
  constructor(
    private readonly orderService: OrderService
  ) {}

  @Post()
  async createOrder(
    @Body() createOrderDto: CreateOrderDto
  ) {
    return this.orderService.createOrder(createOrderDto);
  }

  @Get('user/:userId')
  async getUserOrders(@Param('userId') userId: string) {
    return this.orderService.getOrdersByUserId(userId);
  }

  @Patch(':id/status')
  async updateStatus(
    @Param('id') id: string,
    @Body('status') status: OrderStatus
  ) {
    return this.orderService.updateOrderStatus(id, status);
  }
}

この実装により、ユーザーと注文サービス間の循環依存を forwardRef で安全に解決できました。

認証とユーザー管理の循環依存解決

認証システムでは、AuthService と UserService 間で循環依存が発生しがちです。ModuleRef を使った解決例を示します。

要件

  • ユーザー認証時にユーザー情報の取得が必要
  • ユーザー情報更新時に認証状態の確認が必要
  • パスワード変更時に認証トークンの無効化が必要

Step 1: ModuleRef を使用した AuthService

typescript// services/auth.service.ts
import {
  Injectable,
  OnModuleInit,
  UnauthorizedException,
} from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { JwtService } from '@nestjs/jwt';
import { UserService } from './user.service';

@Injectable()
export class AuthService implements OnModuleInit {
  private userService: UserService;

  constructor(
    private moduleRef: ModuleRef,
    private jwtService: JwtService
  ) {}

  onModuleInit() {
    this.userService = this.moduleRef.get(UserService);
  }

  async validateUser(
    email: string,
    password: string
  ): Promise<any> {
    const user = await this.userService.findByEmail(email);

    if (
      user &&
      (await this.verifyPassword(
        password,
        user.hashedPassword
      ))
    ) {
      const { hashedPassword, ...result } = user;
      return result;
    }
    return null;
  }

  async login(user: any) {
    const payload = { email: user.email, sub: user.id };
    const token = this.jwtService.sign(payload);

    // ユーザーのログイン履歴を更新
    await this.userService.updateLastLogin(user.id);

    return {
      access_token: token,
      user,
    };
  }

  async invalidateUserTokens(
    userId: string
  ): Promise<void> {
    // トークンブラックリストに追加など
    await this.addToBlacklist(userId);
  }

  private async verifyPassword(
    password: string,
    hashedPassword: string
  ): Promise<boolean> {
    // パスワード検証ロジック
    return password === hashedPassword; // 実際の実装では bcrypt などを使用
  }

  private async addToBlacklist(
    userId: string
  ): Promise<void> {
    // ブラックリスト処理
  }
}

Step 2: 遅延取得パターンを使用した UserService

typescript// services/user.service.ts
import {
  Injectable,
  ForbiddenException,
} from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { AuthService } from './auth.service';

@Injectable()
export class UserService {
  constructor(private moduleRef: ModuleRef) {}

  private get authService(): AuthService {
    return this.moduleRef.get(AuthService);
  }

  async findByEmail(email: string): Promise<User | null> {
    // DB からユーザーを検索
    return {
      id: '1',
      name: 'John Doe',
      email,
      hashedPassword: 'hashed_password',
      lastLoginAt: new Date(),
      createdAt: new Date(),
    };
  }

  async updateLastLogin(userId: string): Promise<void> {
    // ログイン時刻を更新
    console.log(`Updating last login for user ${userId}`);
  }

  async changePassword(
    userId: string,
    currentPassword: string,
    newPassword: string
  ): Promise<void> {
    const user = await this.findById(userId);

    // 現在のパスワード確認
    const isValidUser = await this.authService.validateUser(
      user.email,
      currentPassword
    );

    if (!isValidUser) {
      throw new ForbiddenException(
        'Current password is incorrect'
      );
    }

    // パスワード更新処理
    await this.updatePassword(userId, newPassword);

    // 既存トークンを無効化
    await this.authService.invalidateUserTokens(userId);
  }

  async findById(id: string): Promise<User> {
    return {
      id,
      name: 'John Doe',
      email: 'john@example.com',
      hashedPassword: 'hashed_password',
      lastLoginAt: new Date(),
      createdAt: new Date(),
    };
  }

  private async updatePassword(
    userId: string,
    newPassword: string
  ): Promise<void> {
    // パスワード更新処理
    console.log(`Updating password for user ${userId}`);
  }
}

Step 3: モジュール構成

typescript// auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { AuthService } from './services/auth.service';
import { UserService } from './services/user.service';
import { AuthController } from './controllers/auth.controller';

@Module({
  imports: [
    JwtModule.register({
      secret: 'jwt-secret',
      signOptions: { expiresIn: '1h' },
    }),
  ],
  providers: [AuthService, UserService],
  controllers: [AuthController],
  exports: [AuthService, UserService],
})
export class AuthModule {}

この実装では、ModuleRef を使用することで以下の利点を得られます:

  • 柔軟な依存解決: 必要な時にのみサービスを取得
  • 循環依存の回避: 初期化時の循環を完全に回避
  • テストの容易さ: モックの注入が簡単

複雑な階層構造での循環依存解決

最後に、より複雑な階層構造での循環依存解決例を見てみましょう。部門・プロジェクト・タスク管理システムを想定します。

要件

  • 部門は複数のプロジェクトを持つ
  • プロジェクトは複数のタスクを持つ
  • タスクには担当者(部門メンバー)がアサインされる
  • 部門統計にはプロジェクト・タスク情報が必要

Step 1: エンティティ設計

typescript// entities/department.entity.ts
export class Department {
  id: string;
  name: string;
  managerId: string;
  memberIds: string[];
}

// entities/project.entity.ts
export class Project {
  id: string;
  name: string;
  departmentId: string;
  managerId: string;
  status: ProjectStatus;
}

// entities/task.entity.ts
export class Task {
  id: string;
  title: string;
  projectId: string;
  assigneeId: string; // 部門メンバー
  status: TaskStatus;
}

Step 2: 統合サービスでの依存解決

typescript// services/organization.service.ts
import { Injectable } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';

@Injectable()
export class OrganizationService {
  constructor(private moduleRef: ModuleRef) {}

  private get departmentService() {
    return this.moduleRef.get('DepartmentService');
  }

  private get projectService() {
    return this.moduleRef.get('ProjectService');
  }

  private get taskService() {
    return this.moduleRef.get('TaskService');
  }

  async getDepartmentAnalytics(departmentId: string) {
    const department =
      await this.departmentService.findById(departmentId);
    const projects =
      await this.projectService.getByDepartmentId(
        departmentId
      );

    const analytics = {
      department,
      totalProjects: projects.length,
      completedProjects: 0,
      totalTasks: 0,
      completedTasks: 0,
      memberWorkload: new Map(),
    };

    for (const project of projects) {
      if (project.status === 'completed') {
        analytics.completedProjects++;
      }

      const tasks = await this.taskService.getByProjectId(
        project.id
      );
      analytics.totalTasks += tasks.length;

      for (const task of tasks) {
        if (task.status === 'completed') {
          analytics.completedTasks++;
        }

        // メンバーワークロード計算
        const currentLoad =
          analytics.memberWorkload.get(task.assigneeId) ||
          0;
        analytics.memberWorkload.set(
          task.assigneeId,
          currentLoad + 1
        );
      }
    }

    return analytics;
  }
}

Step 3: Factory Pattern による依存管理

typescript// factories/service.factory.ts
import { Injectable } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';

@Injectable()
export class ServiceFactory {
  constructor(private moduleRef: ModuleRef) {}

  createDepartmentService() {
    return this.moduleRef.get('DepartmentService');
  }

  createProjectService() {
    return this.moduleRef.get('ProjectService');
  }

  createTaskService() {
    return this.moduleRef.get('TaskService');
  }

  createOrganizationService() {
    return this.moduleRef.get('OrganizationService');
  }
}

Step 4: プロバイダー設定

typescript// organization.module.ts
import { Module } from '@nestjs/common';
import { ServiceFactory } from './factories/service.factory';

@Module({
  providers: [
    {
      provide: 'DepartmentService',
      useFactory: (serviceFactory: ServiceFactory) => {
        return new DepartmentService(serviceFactory);
      },
      inject: [ServiceFactory],
    },
    {
      provide: 'ProjectService',
      useFactory: (serviceFactory: ServiceFactory) => {
        return new ProjectService(serviceFactory);
      },
      inject: [ServiceFactory],
    },
    {
      provide: 'TaskService',
      useFactory: (serviceFactory: ServiceFactory) => {
        return new TaskService(serviceFactory);
      },
      inject: [ServiceFactory],
    },
    OrganizationService,
    ServiceFactory,
  ],
  exports: [
    'DepartmentService',
    'ProjectService',
    'TaskService',
    OrganizationService,
  ],
})
export class OrganizationModule {}

この実装パターンにより、複雑な階層構造でも循環依存を回避しながら、必要な機能を実現できます。

図で理解できる要点

  • Factory Pattern により依存関係を一元管理
  • ModuleRef による動的な依存解決
  • 階層構造を維持しながら循環を回避

まとめ

NestJS での依存循環解決には、状況に応じて適切な手法を選択することが重要です。

手法選択の指針

状況推奨手法理由
単純なサービス間依存forwardRef実装が簡単で理解しやすい
複雑な依存関係ModuleRef柔軟性が高く制御可能
条件付き依存ModuleRef動的な依存解決が可能
大規模システムFactory Pattern依存関係の一元管理
テスト重視ModuleRefモックの作成が容易

予防策として以下を心がけましょう

  1. 設計レビューの実施: 依存関係図の作成と確認
  2. 責任分界点の明確化: 単一責任原則の徹底
  3. 静的解析ツールの導入: 循環依存の早期発見
  4. 定期的なリファクタリング: コードの健全性維持

適切な依存循環対策により、保守性の高い NestJS アプリケーションを構築していきましょう。本記事の手法を参考に、皆様のプロジェクトでも効果的な依存管理を実現してください。

関連リンク