외부 DB 연결 끊김 오류 해결

This commit is contained in:
dohyeons
2025-12-11 15:25:48 +09:00
parent b03132595c
commit ab9ddaa190
2 changed files with 159 additions and 63 deletions

View File

@@ -113,6 +113,7 @@ class MySQLPoolWrapper implements ConnectionPoolWrapper {
lastUsedAt: Date;
activeConnections = 0;
maxConnections: number;
private isPoolClosed = false;
constructor(config: ExternalDbConnection) {
this.connectionId = config.id!;
@@ -131,6 +132,9 @@ class MySQLPoolWrapper implements ConnectionPoolWrapper {
waitForConnections: true,
queueLimit: 0,
connectTimeout: (config.connection_timeout || 30) * 1000,
// 연결 유지 및 자동 재연결 설정
enableKeepAlive: true,
keepAliveInitialDelay: 10000, // 10초마다 keep-alive 패킷 전송
ssl:
config.ssl_enabled === "Y" ? { rejectUnauthorized: false } : undefined,
});
@@ -149,15 +153,46 @@ class MySQLPoolWrapper implements ConnectionPoolWrapper {
`[${this.dbType.toUpperCase()}] 연결 반환 (${this.activeConnections}/${this.maxConnections})`
);
});
// 연결 오류 이벤트 처리
this.pool.on("error", (err) => {
logger.error(`[${this.dbType.toUpperCase()}] 연결 풀 오류:`, err);
// 연결이 닫힌 경우 플래그 설정
if (err.message.includes("closed state")) {
this.isPoolClosed = true;
}
});
}
async query(sql: string, params?: any[]): Promise<any> {
this.lastUsedAt = new Date();
const [rows] = await this.pool.execute(sql, params);
return rows;
// 연결 풀이 닫힌 상태인지 확인
if (this.isPoolClosed) {
throw new Error("연결 풀이 닫힌 상태입니다. 재연결이 필요합니다.");
}
try {
const [rows] = await this.pool.execute(sql, params);
return rows;
} catch (error: any) {
// 연결 닫힘 오류 감지
if (
error.message.includes("closed state") ||
error.code === "PROTOCOL_CONNECTION_LOST" ||
error.code === "ECONNRESET"
) {
this.isPoolClosed = true;
logger.warn(
`[${this.dbType.toUpperCase()}] 연결 끊김 감지 (ID: ${this.connectionId})`
);
}
throw error;
}
}
async disconnect(): Promise<void> {
this.isPoolClosed = true;
await this.pool.end();
logger.info(
`[${this.dbType.toUpperCase()}] 연결 풀 종료 (ID: ${this.connectionId})`
@@ -165,6 +200,10 @@ class MySQLPoolWrapper implements ConnectionPoolWrapper {
}
isHealthy(): boolean {
// 연결 풀이 닫혔으면 비정상
if (this.isPoolClosed) {
return false;
}
return this.activeConnections < this.maxConnections;
}
}
@@ -230,9 +269,11 @@ export class ExternalDbConnectionPoolService {
): Promise<ConnectionPoolWrapper> {
logger.info(`🔧 새 연결 풀 생성 중 (ID: ${connectionId})...`);
// DB 연결 정보 조회
// DB 연결 정보 조회 (실제 비밀번호 포함)
const connectionResult =
await ExternalDbConnectionService.getConnectionById(connectionId);
await ExternalDbConnectionService.getConnectionByIdWithPassword(
connectionId
);
if (!connectionResult.success || !connectionResult.data) {
throw new Error(`연결 정보를 찾을 수 없습니다 (ID: ${connectionId})`);
@@ -296,16 +337,19 @@ export class ExternalDbConnectionPoolService {
}
/**
* 쿼리 실행 (자동으로 연결 풀 관리)
* 쿼리 실행 (자동으로 연결 풀 관리 + 재시도 로직)
*/
async executeQuery(
connectionId: number,
sql: string,
params?: any[]
params?: any[],
retryCount = 0
): Promise<any> {
const pool = await this.getPool(connectionId);
const MAX_RETRIES = 2;
try {
const pool = await this.getPool(connectionId);
logger.debug(
`📊 쿼리 실행 (ID: ${connectionId}): ${sql.substring(0, 100)}...`
);
@@ -314,7 +358,29 @@ export class ExternalDbConnectionPoolService {
`✅ 쿼리 완료 (ID: ${connectionId}), 결과: ${result.length}`
);
return result;
} catch (error) {
} catch (error: any) {
// 연결 끊김 오류인 경우 재시도
const isConnectionError =
error.message?.includes("closed state") ||
error.message?.includes("연결 풀이 닫힌 상태") ||
error.code === "PROTOCOL_CONNECTION_LOST" ||
error.code === "ECONNRESET" ||
error.code === "ETIMEDOUT";
if (isConnectionError && retryCount < MAX_RETRIES) {
logger.warn(
`🔄 연결 오류 감지, 재시도 중... (${retryCount + 1}/${MAX_RETRIES}) (ID: ${connectionId})`
);
// 기존 풀 제거 후 새로 생성
await this.removePool(connectionId);
// 잠시 대기 후 재시도
await new Promise((resolve) => setTimeout(resolve, 500));
return this.executeQuery(connectionId, sql, params, retryCount + 1);
}
logger.error(`❌ 쿼리 실행 실패 (ID: ${connectionId}):`, error);
throw error;
}