Tools/crypto-wallet-core — NestJS 구조 가이드
schemaBlockchain2026-04-25· 30분 읽기

crypto-wallet-core — NestJS 구조 가이드

모듈·컨트롤러·서비스·DTO 레이어로 구성한 멀티체인 API의 NestJS 구현 가이드.

list목차(42)

NestJS 프레임워크 개발 가이드

이 문서는 Crypto Wallet Core 프로젝트를 NestJS 프레임워크로 개발하는 방법을 안내합니다.

개요

NestJS는 TypeScript 기반의 Node.js 프레임워크로, 모듈화된 아키텍처와 의존성 주입을 통해 확장 가능하고 유지보수하기 쉬운 애플리케이션을 구축할 수 있습니다.

NestJS 기술 스택

핵심 프레임워크

  • NestJS: TypeScript 기반 Node.js 프레임워크
  • @nestjs/common: NestJS 핵심 모듈
  • @nestjs/core: NestJS 코어 기능
  • @nestjs/platform-express: Express 기반 HTTP 서버
  • @nestjs/config: 환경 변수 관리
  • @nestjs/swagger: API 문서화 (선택사항)

각 네트워크별 라이브러리

  • Bitcoin: bitcoinjs-lib, bitcoin-core
  • Ethereum: ethers.js
  • Solana: @solana/web3.js, @solana/spl-token
  • BNB Chain: ethers.js (EVM 호환)

테스팅

  • @nestjs/testing: NestJS 테스팅 유틸리티
  • Jest: 테스트 프레임워크 (NestJS 기본)
  • Supertest: HTTP 테스트

프로젝트 구조

NestJS 모듈 기반 아키텍처

crypto-wallet-core/
├── apps/
│   └── api/                          # NestJS 애플리케이션
│       ├── src/
│       │   ├── main.ts              # 애플리케이션 진입점
│       │   ├── app.module.ts        # 루트 모듈
│       │   ├── app.controller.ts   # 루트 컨트롤러
│       │   ├── app.service.ts       # 루트 서비스
│       │   ├── common/              # 공통 모듈
│       │   │   ├── decorators/      # 커스텀 데코레이터
│       │   │   ├── filters/         # 예외 필터
│       │   │   ├── guards/          # 가드
│       │   │   ├── interceptors/    # 인터셉터
│       │   │   └── pipes/           # 파이프
│       │   ├── networks/            # 네트워크 모듈
│       │   │   ├── bitcoin/
│       │   │   │   ├── bitcoin.module.ts
│       │   │   │   ├── bitcoin.controller.ts
│       │   │   │   ├── bitcoin.service.ts
│       │   │   │   ├── bitcoin-node-manager.service.ts
│       │   │   │   ├── dto/         # Data Transfer Objects
│       │   │   │   │   ├── get-balance.dto.ts
│       │   │   │   │   ├── get-transaction.dto.ts
│       │   │   │   │   └── broadcast-transaction.dto.ts
│       │   │   │   └── entities/   # 엔티티
│       │   │   ├── ethereum/
│       │   │   │   ├── ethereum.module.ts
│       │   │   │   ├── ethereum.controller.ts
│       │   │   │   ├── ethereum.service.ts
│       │   │   │   ├── ethereum-node-manager.service.ts
│       │   │   │   └── dto/
│       │   │   ├── solana/
│       │   │   │   ├── solana.module.ts
│       │   │   │   ├── solana.controller.ts
│       │   │   │   ├── solana.service.ts
│       │   │   │   ├── solana-node-manager.service.ts
│       │   │   │   └── dto/
│       │   │   └── bnb/
│       │   │       ├── bnb.module.ts
│       │   │       ├── bnb.controller.ts
│       │   │       ├── bnb.service.ts
│       │   │       ├── bnb-node-manager.service.ts
│       │   │       └── dto/
│       │   └── health/              # 헬스 체크 모듈
│       │       ├── health.module.ts
│       │       └── health.controller.ts
│       ├── test/                    # E2E 테스트
│       │   ├── app.e2e-spec.ts
│       │   └── jest-e2e.json
│       ├── __tests__/               # 통합 테스트
│       ├── package.json
│       └── tsconfig.json
│
├── packages/
│   ├── core/                        # 공통 인터페이스 및 유틸리티
│   │   ├── src/
│   │   │   ├── interfaces/
│   │   │   │   ├── blockchain-network.interface.ts
│   │   │   │   └── node-manager.interface.ts
│   │   │   ├── types/
│   │   │   │   └── index.ts
│   │   │   └── utils/
│   │   ├── __tests__/
│   │   ├── package.json
│   │   └── tsconfig.json
│   │
│   ├── bitcoin/                     # Bitcoin 라이브러리
│   ├── ethereum/                    # Ethereum 라이브러리
│   ├── solana/                      # Solana 라이브러리
│   └── bnb/                         # BNB Chain 라이브러리
│
├── package.json                     # 루트 package.json (workspace)
├── nest-cli.json                    # NestJS CLI 설정
├── tsconfig.json                    # 루트 TypeScript 설정
└── README.md

모듈 구조

1. 루트 모듈 (App Module)

// apps/api/src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { BitcoinModule } from './networks/bitcoin/bitcoin.module';
import { EthereumModule } from './networks/ethereum/ethereum.module';
import { SolanaModule } from './networks/solana/solana.module';
import { BnbModule } from './networks/bnb/bnb.module';
import { HealthModule } from './health/health.module';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: '.env',
    }),
    BitcoinModule,
    EthereumModule,
    SolanaModule,
    BnbModule,
    HealthModule,
  ],
})
export class AppModule {}

2. 네트워크 모듈 예시 (Bitcoin)

// apps/api/src/networks/bitcoin/bitcoin.module.ts
import { Module } from '@nestjs/common';
import { BitcoinController } from './bitcoin.controller';
import { BitcoinService } from './bitcoin.service';
import { BitcoinNodeManagerService } from './bitcoin-node-manager.service';

@Module({
  controllers: [BitcoinController],
  providers: [BitcoinService, BitcoinNodeManagerService],
  exports: [BitcoinService, BitcoinNodeManagerService],
})
export class BitcoinModule {}

3. 서비스 예시 (Bitcoin Service)

// apps/api/src/networks/bitcoin/bitcoin.service.ts
import { Injectable, Inject } from '@nestjs/common';
import { BitcoinNodeManagerService } from './bitcoin-node-manager.service';
import { GetBalanceDto } from './dto/get-balance.dto';
import { GetTransactionDto } from './dto/get-transaction.dto';
import { BroadcastTransactionDto } from './dto/broadcast-transaction.dto';

@Injectable()
export class BitcoinService {
  constructor(
    private readonly nodeManager: BitcoinNodeManagerService,
  ) {}

  async getBalance(dto: GetBalanceDto): Promise<string> {
    const node = await this.nodeManager.getAvailableNode();
    // Bitcoin 잔액 조회 로직
    // ...
    return balance;
  }

  async getTransaction(dto: GetTransactionDto): Promise<any> {
    const node = await this.nodeManager.getAvailableNode();
    // 트랜잭션 조회 로직
    // ...
    return transaction;
  }

  async broadcastTransaction(dto: BroadcastTransactionDto): Promise<string> {
    const node = await this.nodeManager.getAvailableNode();
    // 트랜잭션 브로드캐스팅 로직
    // ...
    return txHash;
  }
}

4. 노드 관리 서비스 예시

// apps/api/src/networks/bitcoin/bitcoin-node-manager.service.ts
import { Injectable, OnModuleInit } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { NodeManager } from '@crypto-wallet-core/core';

@Injectable()
export class BitcoinNodeManagerService implements NodeManager, OnModuleInit {
  private nodes: string[] = [];
  private nodeHealthStatus: Map<string, { healthy: boolean; latency: number | null }> = new Map();

  constructor(private configService: ConfigService) {}

  async onModuleInit() {
    // 초기 노드 목록 로드 (선택사항)
    const initialNodes = this.configService.get<string>('BITCOIN_NODES');
    if (initialNodes) {
      initialNodes.split(',').forEach(node => this.addNode(node.trim()));
    }

    // 주기적 헬스 체크 시작
    this.startHealthCheck();
  }

  addNode(endpoint: string): void {
    if (!this.nodes.includes(endpoint)) {
      this.nodes.push(endpoint);
      this.nodeHealthStatus.set(endpoint, { healthy: true, latency: null });
    }
  }

  async checkHealth(endpoint: string): Promise<boolean> {
    try {
      const startTime = Date.now();
      // 실제 헬스 체크 로직 (예: RPC 호출)
      const isHealthy = await this.performHealthCheck(endpoint);
      const latency = Date.now() - startTime;

      this.nodeHealthStatus.set(endpoint, { healthy: isHealthy, latency });
      return isHealthy;
    } catch (error) {
      this.nodeHealthStatus.set(endpoint, { healthy: false, latency: null });
      return false;
    }
  }

  async getAvailableNode(): Promise<string> {
    const healthyNodes = Array.from(this.nodeHealthStatus.entries())
      .filter(([_, status]) => status.healthy)
      .sort(([_, a], [__, b]) => (a.latency || Infinity) - (b.latency || Infinity));

    if (healthyNodes.length === 0) {
      throw new Error('No available Bitcoin nodes');
    }

    return healthyNodes[0][0];
  }

  async checkAllNodes(): Promise<Array<{ endpoint: string; healthy: boolean; latency: number | null }>> {
    const checks = this.nodes.map(async (endpoint) => {
      const healthy = await this.checkHealth(endpoint);
      const status = this.nodeHealthStatus.get(endpoint);
      return {
        endpoint,
        healthy,
        latency: status?.latency || null,
      };
    });

    return Promise.all(checks);
  }

  markNodeUnavailable(endpoint: string): void {
    this.nodeHealthStatus.set(endpoint, { healthy: false, latency: null });
  }

  private async performHealthCheck(endpoint: string): Promise<boolean> {
    // 실제 헬스 체크 구현
    // 예: Bitcoin RPC getblockchaininfo 호출
    return true;
  }

  private startHealthCheck(): void {
    // 30초마다 모든 노드 헬스 체크
    setInterval(async () => {
      await this.checkAllNodes();
    }, 30000);
  }
}

5. 컨트롤러 예시 (Bitcoin Controller)

// apps/api/src/networks/bitcoin/bitcoin.controller.ts
import { Controller, Get, Post, Body, Param, HttpCode, HttpStatus } from '@nestjs/common';
import { BitcoinService } from './bitcoin.service';
import { BitcoinNodeManagerService } from './bitcoin-node-manager.service';
import { GetBalanceDto } from './dto/get-balance.dto';
import { GetTransactionDto } from './dto/get-transaction.dto';
import { BroadcastTransactionDto } from './dto/broadcast-transaction.dto';

@Controller('bitcoin')
export class BitcoinController {
  constructor(
    private readonly bitcoinService: BitcoinService,
    private readonly nodeManager: BitcoinNodeManagerService,
  ) {}

  @Get('balance/:address')
  async getBalance(@Param('address') address: string) {
    return this.bitcoinService.getBalance({ address });
  }

  @Get('transaction/:txHash')
  async getTransaction(@Param('txHash') txHash: string) {
    return this.bitcoinService.getTransaction({ txHash });
  }

  @Post('broadcast')
  @HttpCode(HttpStatus.OK)
  async broadcastTransaction(@Body() dto: BroadcastTransactionDto) {
    return this.bitcoinService.broadcastTransaction(dto);
  }

  @Get('nodes')
  async getNodes() {
    return this.nodeManager.checkAllNodes();
  }

  @Post('nodes')
  async addNode(@Body() body: { endpoint: string }) {
    this.nodeManager.addNode(body.endpoint);
    return { message: 'Node added successfully' };
  }
}

6. DTO 예시

// apps/api/src/networks/bitcoin/dto/get-balance.dto.ts
import { IsString, IsNotEmpty } from 'class-validator';

export class GetBalanceDto {
  @IsString()
  @IsNotEmpty()
  address: string;
}
// apps/api/src/networks/bitcoin/dto/broadcast-transaction.dto.ts
import { IsString, IsNotEmpty } from 'class-validator';

export class BroadcastTransactionDto {
  @IsString()
  @IsNotEmpty()
  transaction: string;
}

TDD 기반 개발 (NestJS)

테스트 구조

apps/api/src/networks/bitcoin/
├── bitcoin.service.spec.ts          # 서비스 단위 테스트
├── bitcoin.controller.spec.ts       # 컨트롤러 단위 테스트
├── bitcoin-node-manager.service.spec.ts  # 노드 관리자 테스트
└── __tests__/
    └── integration/
        └── bitcoin.integration.spec.ts   # 통합 테스트

서비스 테스트 예시

// apps/api/src/networks/bitcoin/bitcoin.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { BitcoinService } from './bitcoin.service';
import { BitcoinNodeManagerService } from './bitcoin-node-manager.service';

describe('BitcoinService', () => {
  let service: BitcoinService;
  let nodeManager: BitcoinNodeManagerService;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        BitcoinService,
        {
          provide: BitcoinNodeManagerService,
          useValue: {
            getAvailableNode: jest.fn().mockResolvedValue('http://localhost:8332'),
          },
        },
      ],
    }).compile();

    service = module.get<BitcoinService>(BitcoinService);
    nodeManager = module.get<BitcoinNodeManagerService>(BitcoinNodeManagerService);
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });

  describe('getBalance', () => {
    it('should return balance for valid address', async () => {
      // Red: 실패하는 테스트 작성
      const address = '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa';
      const balance = await service.getBalance({ address });
      
      expect(balance).toBeDefined();
      expect(typeof balance).toBe('string');
    });
  });
});

컨트롤러 테스트 예시

// apps/api/src/networks/bitcoin/bitcoin.controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { BitcoinController } from './bitcoin.controller';
import { BitcoinService } from './bitcoin.service';
import { BitcoinNodeManagerService } from './bitcoin-node-manager.service';

describe('BitcoinController', () => {
  let controller: BitcoinController;
  let service: BitcoinService;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      controllers: [BitcoinController],
      providers: [
        {
          provide: BitcoinService,
          useValue: {
            getBalance: jest.fn().mockResolvedValue('1000000'),
            getTransaction: jest.fn(),
            broadcastTransaction: jest.fn(),
          },
        },
        {
          provide: BitcoinNodeManagerService,
          useValue: {
            checkAllNodes: jest.fn(),
            addNode: jest.fn(),
          },
        },
      ],
    }).compile();

    controller = module.get<BitcoinController>(BitcoinController);
    service = module.get<BitcoinService>(BitcoinService);
  });

  it('should be defined', () => {
    expect(controller).toBeDefined();
  });

  describe('GET /bitcoin/balance/:address', () => {
    it('should return balance', async () => {
      const address = '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa';
      const result = await controller.getBalance(address);
      
      expect(result).toBe('1000000');
      expect(service.getBalance).toHaveBeenCalledWith({ address });
    });
  });
});

E2E 테스트 예시

// apps/api/test/bitcoin.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';

describe('BitcoinController (e2e)', () => {
  let app: INestApplication;

  beforeAll(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = moduleFixture.createNestApplication();
    await app.init();
  });

  afterAll(async () => {
    await app.close();
  });

  it('/bitcoin/balance/:address (GET)', () => {
    return request(app.getHttpServer())
      .get('/bitcoin/balance/1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa')
      .expect(200)
      .expect((res) => {
        expect(res.body).toHaveProperty('balance');
      });
  });

  it('/bitcoin/broadcast (POST)', () => {
    return request(app.getHttpServer())
      .post('/bitcoin/broadcast')
      .send({ transaction: '0x...' })
      .expect(200);
  });
});

시작하기

사전 요구사항

  • Node.js 18+
  • npm 또는 yarn

설치

# 프로젝트 루트에서
npm install

# NestJS 애플리케이션 디렉토리로 이동
cd apps/api

# NestJS CLI 설치 (전역)
npm install -g @nestjs/cli

# 또는 로컬 설치
npm install --save-dev @nestjs/cli

개발

1. 개발 서버 실행

# 개발 모드 실행 (hot reload)
npm run start:dev

# 또는 특정 포트로 실행
PORT=3000 npm run start:dev

# 디버그 모드 실행
npm run start:debug

2. 프로덕션 빌드 및 실행

# 빌드
npm run build

# 프로덕션 모드 실행
npm run start:prod

3. 테스트 실행

# 단위 테스트
npm run test

# 테스트 watch 모드
npm run test:watch

# 테스트 커버리지
npm run test:cov

# E2E 테스트
npm run test:e2e

4. TDD 워크플로우

# 1. 테스트 작성 및 watch 모드 실행
npm run test:watch

# 2. 테스트 실행
npm run test

# 3. 특정 파일 테스트
npm run test -- bitcoin.service.spec.ts

# 4. 커버리지 확인
npm run test:cov

API 사용 예시

1. 잔액 조회

# Bitcoin
curl http://localhost:3000/bitcoin/balance/1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa

# Ethereum
curl http://localhost:3000/ethereum/balance/0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb

# Solana
curl http://localhost:3000/solana/balance/9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM

# BNB Chain
curl http://localhost:3000/bnb/balance/0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb

2. 트랜잭션 조회

# Bitcoin
curl http://localhost:3000/bitcoin/transaction/tx-hash-here

# Ethereum
curl http://localhost:3000/ethereum/transaction/0x...

3. 트랜잭션 브로드캐스팅

# Bitcoin
curl -X POST http://localhost:3000/bitcoin/broadcast \
  -H "Content-Type: application/json" \
  -d '{"transaction": "signed-tx-hex"}'

# Ethereum
curl -X POST http://localhost:3000/ethereum/transaction/broadcast-signed \
  -H "Content-Type: application/json" \
  -d '{"transaction": "0x..."}'

# Solana
curl -X POST http://localhost:3000/solana/broadcast \
  -H "Content-Type: application/json" \
  -d '{"transaction": "base58-encoded-tx"}'

4. 노드 관리

# 모든 노드 상태 조회
curl http://localhost:3000/bitcoin/nodes

# 새 노드 추가
curl -X POST http://localhost:3000/bitcoin/nodes \
  -H "Content-Type: application/json" \
  -d '{"endpoint": "http://new-node.example.com:8332"}'

환경 변수 설정

.env 파일 예시:

# 서버 설정
PORT=3000
NODE_ENV=development

# Bitcoin 노드 (PublicNode 무료 RPC 엔드포인트 - 기본값)
BITCOIN_NODES=https://bitcoin-rpc.publicnode.com

# Ethereum 노드 (PublicNode 무료 RPC 엔드포인트 - 기본값)
ETHEREUM_NODES=https://ethereum-rpc.publicnode.com

# Solana 노드 (PublicNode 무료 RPC 엔드포인트 - 기본값)
SOLANA_NODES=https://solana-rpc.publicnode.com

# BNB Chain 노드 (PublicNode 무료 RPC 엔드포인트 - 기본값)
BNB_NODES=https://bsc-rpc.publicnode.com

# 참고: 여러 노드를 사용하려면 쉼표로 구분
# BITCOIN_NODES=https://bitcoin-rpc.publicnode.com,http://localhost:8332
# ETHEREUM_NODES=https://ethereum-rpc.publicnode.com,https://mainnet.infura.io/v3/YOUR_KEY

중요: .env 파일의 노드는 초기 설정일 뿐이며, 런타임에 API를 통해 동적으로 추가/제거/전환됩니다.

NestJS 모듈 확장

새로운 네트워크 추가 예시

새로운 블록체인 네트워크를 추가하려면:

  1. 모듈 생성
nest generate module networks/polygon
nest generate controller networks/polygon
nest generate service networks/polygon
  1. 서비스 구현
// apps/api/src/networks/polygon/polygon.service.ts
@Injectable()
export class PolygonService {
  // Polygon 네트워크 로직 구현
}
  1. AppModule에 등록
// apps/api/src/app.module.ts
import { PolygonModule } from './networks/polygon/polygon.module';

@Module({
  imports: [
    // ... 기존 모듈들
    PolygonModule,
  ],
})
export class AppModule {}

주요 NestJS 기능 활용

1. 의존성 주입 (Dependency Injection)

@Injectable()
export class BitcoinService {
  constructor(
    private readonly nodeManager: BitcoinNodeManagerService,
    private readonly configService: ConfigService,
  ) {}
}

2. 인터셉터 (Interceptors)

로깅, 에러 처리, 응답 변환 등에 활용:

// apps/api/src/common/interceptors/logging.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const now = Date.now();
    return next
      .handle()
      .pipe(
        tap(() => console.log(`After... ${Date.now() - now}ms`)),
      );
  }
}

3. 가드 (Guards)

인증, 권한 부여 등에 활용:

// apps/api/src/common/guards/api-key.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class ApiKeyGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    const apiKey = request.headers['x-api-key'];
    return apiKey === process.env.API_KEY;
  }
}

4. 예외 필터 (Exception Filters)

// apps/api/src/common/filters/http-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus();

    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      message: exception.message,
    });
  }
}

문제 해결

포트 충돌

# 다른 포트로 실행
PORT=3001 npm run start:dev

모듈 로드 오류

# 의존성 재설치
rm -rf node_modules
npm install

# 빌드 캐시 클리어
npm run build

테스트 실행 오류

# 테스트 캐시 클리어
npm run test -- --clearCache

# 특정 테스트만 실행
npm run test -- bitcoin.service.spec.ts

추가 리소스

라이선스

MIT

More in Blockchain