492 lines
12 KiB
Markdown
492 lines
12 KiB
Markdown
|
|
# 외부 DB 연결 풀 관리 가이드
|
||
|
|
|
||
|
|
## 📋 개요
|
||
|
|
|
||
|
|
외부 DB 연결 풀 서비스는 여러 외부 데이터베이스와의 연결을 효율적으로 관리하여 **연결 풀 고갈을 방지**하고 성능을 최적화합니다.
|
||
|
|
|
||
|
|
### 주요 기능
|
||
|
|
|
||
|
|
- ✅ **자동 연결 풀 관리**: 연결 생성, 재사용, 정리 자동화
|
||
|
|
- ✅ **연결 풀 고갈 방지**: 최대 연결 수 제한 및 모니터링
|
||
|
|
- ✅ **유휴 연결 정리**: 10분 이상 사용되지 않은 풀 자동 제거
|
||
|
|
- ✅ **헬스 체크**: 1분마다 모든 풀 상태 검사
|
||
|
|
- ✅ **다중 DB 지원**: PostgreSQL, MySQL, MariaDB
|
||
|
|
- ✅ **싱글톤 패턴**: 전역적으로 단일 인스턴스 사용
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🏗️ 아키텍처
|
||
|
|
|
||
|
|
```
|
||
|
|
┌─────────────────────────────────────────┐
|
||
|
|
│ NodeFlowExecutionService │
|
||
|
|
│ (외부 DB 소스/액션 노드) │
|
||
|
|
└──────────────┬──────────────────────────┘
|
||
|
|
│
|
||
|
|
▼
|
||
|
|
┌─────────────────────────────────────────┐
|
||
|
|
│ ExternalDbConnectionPoolService │
|
||
|
|
│ (싱글톤 인스턴스) │
|
||
|
|
│ │
|
||
|
|
│ ┌─────────────────────────────────┐ │
|
||
|
|
│ │ Connection Pool Map │ │
|
||
|
|
│ │ ┌──────────────────────────┐ │ │
|
||
|
|
│ │ │ ID: 1 → PostgresPool │ │ │
|
||
|
|
│ │ │ ID: 2 → MySQLPool │ │ │
|
||
|
|
│ │ │ ID: 3 → MariaDBPool │ │ │
|
||
|
|
│ │ └──────────────────────────┘ │ │
|
||
|
|
│ └─────────────────────────────────┘ │
|
||
|
|
│ │
|
||
|
|
│ - 자동 풀 생성/제거 │
|
||
|
|
│ - 헬스 체크 (1분마다) │
|
||
|
|
│ - 유휴 풀 정리 (10분) │
|
||
|
|
└─────────────────────────────────────────┘
|
||
|
|
│
|
||
|
|
▼
|
||
|
|
┌─────────────────────────────────────────┐
|
||
|
|
│ External Databases │
|
||
|
|
│ - PostgreSQL │
|
||
|
|
│ - MySQL │
|
||
|
|
│ - MariaDB │
|
||
|
|
└─────────────────────────────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🔧 연결 풀 설정
|
||
|
|
|
||
|
|
### PostgreSQL 연결 풀
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
{
|
||
|
|
max: 10, // 최대 연결 수
|
||
|
|
min: 2, // 최소 연결 수
|
||
|
|
idleTimeoutMillis: 30000, // 30초 유휴 시 연결 해제
|
||
|
|
connectionTimeoutMillis: 30000, // 연결 타임아웃 30초
|
||
|
|
statement_timeout: 60000, // 쿼리 타임아웃 60초
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### MySQL/MariaDB 연결 풀
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
{
|
||
|
|
connectionLimit: 10, // 최대 연결 수
|
||
|
|
waitForConnections: true,
|
||
|
|
queueLimit: 0, // 대기열 무제한
|
||
|
|
connectTimeout: 30000, // 연결 타임아웃 30초
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 📊 연결 풀 라이프사이클
|
||
|
|
|
||
|
|
### 1. 풀 생성
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 첫 요청 시 자동 생성
|
||
|
|
const pool = await poolService.getPool(connectionId);
|
||
|
|
```
|
||
|
|
|
||
|
|
**생성 시점**:
|
||
|
|
|
||
|
|
- 외부 DB 소스 노드 첫 실행 시
|
||
|
|
- 외부 DB 액션 노드 첫 실행 시
|
||
|
|
|
||
|
|
**생성 과정**:
|
||
|
|
|
||
|
|
1. DB 연결 정보 조회 (`external_db_connections` 테이블)
|
||
|
|
2. 비밀번호 복호화
|
||
|
|
3. DB 타입에 맞는 연결 풀 생성 (PostgreSQL, MySQL, MariaDB)
|
||
|
|
4. 이벤트 리스너 등록 (연결 획득/해제 추적)
|
||
|
|
|
||
|
|
### 2. 풀 재사용
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 기존 풀이 있으면 재사용
|
||
|
|
if (this.pools.has(connectionId)) {
|
||
|
|
const pool = this.pools.get(connectionId)!;
|
||
|
|
pool.lastUsedAt = new Date(); // 사용 시간 갱신
|
||
|
|
return pool;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**재사용 조건**:
|
||
|
|
|
||
|
|
- 동일한 `connectionId`로 요청
|
||
|
|
- 풀이 정상 상태 (`isHealthy()` 통과)
|
||
|
|
|
||
|
|
### 3. 자동 정리
|
||
|
|
|
||
|
|
**유휴 시간 초과 (10분)**:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
const IDLE_TIMEOUT = 10 * 60 * 1000; // 10분
|
||
|
|
|
||
|
|
if (now - pool.lastUsedAt.getTime() > IDLE_TIMEOUT) {
|
||
|
|
await this.removePool(connectionId);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**헬스 체크 실패**:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
if (!pool.isHealthy()) {
|
||
|
|
await this.removePool(connectionId);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🔍 헬스 체크 시스템
|
||
|
|
|
||
|
|
### 주기적 헬스 체크
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
const HEALTH_CHECK_INTERVAL = 60 * 1000; // 1분마다
|
||
|
|
|
||
|
|
setInterval(() => {
|
||
|
|
this.pools.forEach(async (pool, connectionId) => {
|
||
|
|
// 유휴 시간 체크
|
||
|
|
const idleTime = now - pool.lastUsedAt.getTime();
|
||
|
|
if (idleTime > IDLE_TIMEOUT) {
|
||
|
|
await this.removePool(connectionId);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 헬스 체크
|
||
|
|
if (!pool.isHealthy()) {
|
||
|
|
await this.removePool(connectionId);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}, HEALTH_CHECK_INTERVAL);
|
||
|
|
```
|
||
|
|
|
||
|
|
### 헬스 체크 조건
|
||
|
|
|
||
|
|
#### PostgreSQL
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
isHealthy(): boolean {
|
||
|
|
return this.pool.totalCount > 0
|
||
|
|
&& this.activeConnections < this.maxConnections;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### MySQL/MariaDB
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
isHealthy(): boolean {
|
||
|
|
return this.activeConnections < this.maxConnections;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 💻 사용 방법
|
||
|
|
|
||
|
|
### 1. 외부 DB 소스 노드에서 사용
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// nodeFlowExecutionService.ts
|
||
|
|
private static async executeExternalDBSource(
|
||
|
|
node: FlowNode,
|
||
|
|
context: ExecutionContext
|
||
|
|
): Promise<any[]> {
|
||
|
|
const { connectionId, tableName } = node.data;
|
||
|
|
|
||
|
|
// 연결 풀 서비스 사용
|
||
|
|
const { ExternalDbConnectionPoolService } = await import(
|
||
|
|
"./externalDbConnectionPoolService"
|
||
|
|
);
|
||
|
|
const poolService = ExternalDbConnectionPoolService.getInstance();
|
||
|
|
|
||
|
|
const sql = `SELECT * FROM ${tableName}`;
|
||
|
|
const result = await poolService.executeQuery(connectionId, sql);
|
||
|
|
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2. 외부 DB 액션 노드에서 사용
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 기존 createExternalConnector가 자동으로 연결 풀 사용
|
||
|
|
const connector = await this.createExternalConnector(connectionId, dbType);
|
||
|
|
|
||
|
|
// executeQuery 호출 시 내부적으로 연결 풀 사용
|
||
|
|
const result = await connector.executeQuery(sql, params);
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3. 연결 풀 상태 조회
|
||
|
|
|
||
|
|
**API 엔드포인트**:
|
||
|
|
|
||
|
|
```
|
||
|
|
GET /api/external-db-connections/pool-status
|
||
|
|
```
|
||
|
|
|
||
|
|
**응답 예시**:
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"success": true,
|
||
|
|
"data": {
|
||
|
|
"totalPools": 3,
|
||
|
|
"activePools": 2,
|
||
|
|
"pools": [
|
||
|
|
{
|
||
|
|
"connectionId": 1,
|
||
|
|
"dbType": "postgresql",
|
||
|
|
"activeConnections": 2,
|
||
|
|
"maxConnections": 10,
|
||
|
|
"createdAt": "2025-01-13T10:00:00.000Z",
|
||
|
|
"lastUsedAt": "2025-01-13T10:05:00.000Z",
|
||
|
|
"idleSeconds": 45
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"connectionId": 2,
|
||
|
|
"dbType": "mysql",
|
||
|
|
"activeConnections": 0,
|
||
|
|
"maxConnections": 10,
|
||
|
|
"createdAt": "2025-01-13T09:50:00.000Z",
|
||
|
|
"lastUsedAt": "2025-01-13T09:55:00.000Z",
|
||
|
|
"idleSeconds": 600
|
||
|
|
}
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"message": "3개의 연결 풀 상태를 조회했습니다."
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🚨 연결 풀 고갈 방지 메커니즘
|
||
|
|
|
||
|
|
### 1. 최대 연결 수 제한
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 데이터베이스 설정 기준
|
||
|
|
max_connections: config.max_connections || 10;
|
||
|
|
```
|
||
|
|
|
||
|
|
각 외부 DB 연결마다 최대 연결 수를 설정하여 무제한 연결 방지.
|
||
|
|
|
||
|
|
### 2. 연결 재사용
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 동일한 connectionId 요청 시 기존 풀 재사용
|
||
|
|
const pool = await poolService.getPool(connectionId);
|
||
|
|
```
|
||
|
|
|
||
|
|
매번 새 연결을 생성하지 않고 기존 풀 재사용.
|
||
|
|
|
||
|
|
### 3. 자동 연결 해제
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// PostgreSQL: 30초 유휴 시 자동 해제
|
||
|
|
idleTimeoutMillis: 30000;
|
||
|
|
```
|
||
|
|
|
||
|
|
사용되지 않는 연결은 자동으로 해제하여 리소스 절약.
|
||
|
|
|
||
|
|
### 4. 전역 풀 정리
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 10분 이상 미사용 풀 제거
|
||
|
|
if (idleTime > IDLE_TIMEOUT) {
|
||
|
|
await this.removePool(connectionId);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
장시간 사용되지 않는 풀 자체를 제거.
|
||
|
|
|
||
|
|
### 5. 애플리케이션 종료 시 정리
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
process.on("SIGINT", async () => {
|
||
|
|
await ExternalDbConnectionPoolService.getInstance().closeAll();
|
||
|
|
process.exit(0);
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
프로세스 종료 시 모든 연결 정상 종료.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 📈 모니터링 및 로깅
|
||
|
|
|
||
|
|
### 연결 이벤트 로깅
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 연결 획득
|
||
|
|
pool.on("acquire", () => {
|
||
|
|
logger.debug(`[PostgreSQL] 연결 획득 (2/10)`);
|
||
|
|
});
|
||
|
|
|
||
|
|
// 연결 반환
|
||
|
|
pool.on("release", () => {
|
||
|
|
logger.debug(`[PostgreSQL] 연결 반환 (1/10)`);
|
||
|
|
});
|
||
|
|
|
||
|
|
// 에러 발생
|
||
|
|
pool.on("error", (err) => {
|
||
|
|
logger.error(`[PostgreSQL] 연결 풀 오류:`, err);
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### 정기 상태 로깅
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 1분마다 상태 출력
|
||
|
|
logger.debug(`📊 연결 풀 상태: 총 3개, 활성: 2개`);
|
||
|
|
```
|
||
|
|
|
||
|
|
### 주요 로그 메시지
|
||
|
|
|
||
|
|
| 레벨 | 메시지 | 의미 |
|
||
|
|
| ------- | ---------------------------------------------------------- | --------------- |
|
||
|
|
| `info` | `🔧 새 연결 풀 생성 중 (ID: 1)...` | 새 풀 생성 시작 |
|
||
|
|
| `info` | `✅ 연결 풀 생성 완료 (ID: 1, 타입: postgresql, 최대: 10)` | 풀 생성 완료 |
|
||
|
|
| `debug` | `✅ 기존 연결 풀 재사용 (ID: 1)` | 기존 풀 재사용 |
|
||
|
|
| `info` | `🧹 유휴 연결 풀 정리 (ID: 2, 유휴: 620초)` | 유휴 풀 제거 |
|
||
|
|
| `warn` | `⚠️ 연결 풀 비정상 감지 (ID: 3), 재생성 중...` | 헬스 체크 실패 |
|
||
|
|
| `error` | `❌ 쿼리 실행 실패 (ID: 1)` | 쿼리 오류 |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🔒 보안 고려사항
|
||
|
|
|
||
|
|
### 1. 비밀번호 보호
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 비밀번호 복호화는 풀 생성 시에만 수행
|
||
|
|
config.password = PasswordEncryption.decrypt(config.password);
|
||
|
|
```
|
||
|
|
|
||
|
|
메모리에 평문 비밀번호를 최소한으로 유지.
|
||
|
|
|
||
|
|
### 2. 연결 정보 검증
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
if (config.is_active !== "Y") {
|
||
|
|
throw new Error(`비활성화된 연결입니다 (ID: ${connectionId})`);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
비활성화된 연결은 사용 불가.
|
||
|
|
|
||
|
|
### 3. 타임아웃 설정
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
connectionTimeoutMillis: 30000, // 30초
|
||
|
|
statement_timeout: 60000, // 60초
|
||
|
|
```
|
||
|
|
|
||
|
|
무한 대기 방지.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🐛 트러블슈팅
|
||
|
|
|
||
|
|
### 문제 1: 연결 풀 고갈
|
||
|
|
|
||
|
|
**증상**: "Connection pool exhausted" 오류
|
||
|
|
|
||
|
|
**원인**:
|
||
|
|
|
||
|
|
- 동시 요청이 최대 연결 수 초과
|
||
|
|
- 쿼리가 너무 오래 실행되어 연결 점유
|
||
|
|
|
||
|
|
**해결**:
|
||
|
|
|
||
|
|
1. `max_connections` 값 증가 (`external_db_connections` 테이블)
|
||
|
|
2. 쿼리 최적화 (인덱스, LIMIT 추가)
|
||
|
|
3. `query_timeout` 값 조정
|
||
|
|
|
||
|
|
### 문제 2: 메모리 누수
|
||
|
|
|
||
|
|
**증상**: 메모리 사용량 지속 증가
|
||
|
|
|
||
|
|
**원인**:
|
||
|
|
|
||
|
|
- 연결 풀이 정리되지 않음
|
||
|
|
- 헬스 체크 실패
|
||
|
|
|
||
|
|
**해결**:
|
||
|
|
|
||
|
|
1. 연결 풀 상태 확인: `GET /api/external-db-connections/pool-status`
|
||
|
|
2. 수동 재시작으로 모든 풀 정리
|
||
|
|
3. 로그에서 `🧹 유휴 연결 풀 정리` 메시지 확인
|
||
|
|
|
||
|
|
### 문제 3: 연결 시간 초과
|
||
|
|
|
||
|
|
**증상**: "Connection timeout" 오류
|
||
|
|
|
||
|
|
**원인**:
|
||
|
|
|
||
|
|
- DB 서버 응답 없음
|
||
|
|
- 네트워크 문제
|
||
|
|
- 방화벽 차단
|
||
|
|
|
||
|
|
**해결**:
|
||
|
|
|
||
|
|
1. DB 서버 상태 확인
|
||
|
|
2. 네트워크 연결 확인
|
||
|
|
3. `connection_timeout` 값 증가
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## ⚙️ 설정 권장사항
|
||
|
|
|
||
|
|
### 소규모 시스템 (동시 사용자 < 50)
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
{
|
||
|
|
max_connections: 5,
|
||
|
|
connection_timeout: 30,
|
||
|
|
query_timeout: 60,
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 중규모 시스템 (동시 사용자 50-200)
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
{
|
||
|
|
max_connections: 10,
|
||
|
|
connection_timeout: 30,
|
||
|
|
query_timeout: 90,
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 대규모 시스템 (동시 사용자 > 200)
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
{
|
||
|
|
max_connections: 20,
|
||
|
|
connection_timeout: 60,
|
||
|
|
query_timeout: 120,
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 📚 참고 자료
|
||
|
|
|
||
|
|
- [PostgreSQL Connection Pooling](https://node-postgres.com/features/pooling)
|
||
|
|
- [MySQL Connection Pool](https://github.com/mysqljs/mysql#pooling-connections)
|
||
|
|
- [Node.js Best Practices - Database Connection Management](https://github.com/goldbergyoni/nodebestpractices)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🎯 결론
|
||
|
|
|
||
|
|
외부 DB 연결 풀 서비스는 다음을 보장합니다:
|
||
|
|
|
||
|
|
✅ **효율성**: 연결 재사용으로 성능 향상
|
||
|
|
✅ **안정성**: 연결 풀 고갈 방지
|
||
|
|
✅ **자동화**: 생성/정리/모니터링 자동화
|
||
|
|
✅ **확장성**: 다중 DB 및 대규모 트래픽 지원
|
||
|
|
|
||
|
|
**최소한의 설정**으로 **최대한의 안정성**을 제공합니다! 🚀
|