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 모듈 확장
새로운 네트워크 추가 예시
새로운 블록체인 네트워크를 추가하려면:
- 모듈 생성
nest generate module networks/polygon
nest generate controller networks/polygon
nest generate service networks/polygon
- 서비스 구현
// apps/api/src/networks/polygon/polygon.service.ts
@Injectable()
export class PolygonService {
// Polygon 네트워크 로직 구현
}
- 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