feat: Complete Phase 1 of Prisma to Raw Query migration
Phase 1 완료: Raw Query 기반 데이터베이스 아키텍처 구축 ✅ 구현 완료 내용: - DatabaseManager 클래스 구현 (연결 풀, 트랜잭션 관리) - QueryBuilder 유틸리티 (동적 쿼리 생성) - 타입 정의 및 검증 로직 (database.ts, databaseValidator.ts) - 단위 테스트 작성 및 통과 🔧 전환 완료 서비스: - externalCallConfigService.ts (Raw Query 전환) - multiConnectionQueryService.ts (Raw Query 전환) 📚 문서: - PHASE1_USAGE_GUIDE.md (사용 가이드) - DETAILED_FILE_MIGRATION_PLAN.md (상세 계획) - PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md (Phase 1 완료 표시) 🧪 테스트: - database.test.ts (핵심 기능 테스트) - 모든 테스트 통과 확인 이제 Phase 2 (핵심 서비스 전환)로 진행 가능 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
271
backend-node/src/database/db.ts
Normal file
271
backend-node/src/database/db.ts
Normal file
@@ -0,0 +1,271 @@
|
||||
/**
|
||||
* PostgreSQL Raw Query 기반 데이터베이스 매니저
|
||||
*
|
||||
* Prisma → Raw Query 전환의 핵심 모듈
|
||||
* - Connection Pool 기반 안정적인 연결 관리
|
||||
* - 트랜잭션 지원
|
||||
* - 타입 안전성 보장
|
||||
* - 자동 재연결 및 에러 핸들링
|
||||
*/
|
||||
|
||||
import {
|
||||
Pool,
|
||||
PoolClient,
|
||||
QueryResult as PgQueryResult,
|
||||
QueryResultRow,
|
||||
} from "pg";
|
||||
import config from "../config/environment";
|
||||
|
||||
// PostgreSQL 연결 풀
|
||||
let pool: Pool | null = null;
|
||||
|
||||
/**
|
||||
* 데이터베이스 연결 풀 초기화
|
||||
*/
|
||||
export const initializePool = (): Pool => {
|
||||
if (pool) {
|
||||
return pool;
|
||||
}
|
||||
|
||||
// DATABASE_URL 파싱 (postgresql://user:password@host:port/database)
|
||||
const databaseUrl = config.databaseUrl;
|
||||
|
||||
// URL 파싱 로직
|
||||
const dbConfig = parseDatabaseUrl(databaseUrl);
|
||||
|
||||
pool = new Pool({
|
||||
host: dbConfig.host,
|
||||
port: dbConfig.port,
|
||||
database: dbConfig.database,
|
||||
user: dbConfig.user,
|
||||
password: dbConfig.password,
|
||||
|
||||
// 연결 풀 설정
|
||||
min: config.nodeEnv === "production" ? 5 : 2,
|
||||
max: config.nodeEnv === "production" ? 20 : 10,
|
||||
|
||||
// 타임아웃 설정
|
||||
connectionTimeoutMillis: 30000, // 30초
|
||||
idleTimeoutMillis: 600000, // 10분
|
||||
|
||||
// 연결 유지 설정
|
||||
keepAlive: true,
|
||||
keepAliveInitialDelayMillis: 10000,
|
||||
|
||||
// 쿼리 타임아웃
|
||||
statement_timeout: 60000, // 60초 (동적 테이블 생성 등 고려)
|
||||
query_timeout: 60000,
|
||||
|
||||
// Application Name
|
||||
application_name: "WACE-PLM-Backend",
|
||||
});
|
||||
|
||||
// 연결 풀 이벤트 핸들러
|
||||
pool.on("connect", (client) => {
|
||||
if (config.debug) {
|
||||
console.log("✅ PostgreSQL 클라이언트 연결 생성");
|
||||
}
|
||||
});
|
||||
|
||||
pool.on("acquire", (client) => {
|
||||
if (config.debug) {
|
||||
console.log("🔒 PostgreSQL 클라이언트 획득");
|
||||
}
|
||||
});
|
||||
|
||||
pool.on("remove", (client) => {
|
||||
if (config.debug) {
|
||||
console.log("🗑️ PostgreSQL 클라이언트 제거");
|
||||
}
|
||||
});
|
||||
|
||||
pool.on("error", (err, client) => {
|
||||
console.error("❌ PostgreSQL 연결 풀 에러:", err);
|
||||
});
|
||||
|
||||
console.log(
|
||||
`🚀 PostgreSQL 연결 풀 초기화 완료: ${dbConfig.host}:${dbConfig.port}/${dbConfig.database}`
|
||||
);
|
||||
|
||||
return pool;
|
||||
};
|
||||
|
||||
/**
|
||||
* DATABASE_URL 파싱 헬퍼 함수
|
||||
*/
|
||||
function parseDatabaseUrl(url: string) {
|
||||
// postgresql://user:password@host:port/database
|
||||
const regex = /postgresql:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/(.+)/;
|
||||
const match = url.match(regex);
|
||||
|
||||
if (!match) {
|
||||
// URL 파싱 실패 시 기본값 사용
|
||||
console.warn("⚠️ DATABASE_URL 파싱 실패, 기본값 사용");
|
||||
return {
|
||||
host: "localhost",
|
||||
port: 5432,
|
||||
database: "ilshin",
|
||||
user: "postgres",
|
||||
password: "postgres",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
user: decodeURIComponent(match[1]),
|
||||
password: decodeURIComponent(match[2]),
|
||||
host: match[3],
|
||||
port: parseInt(match[4], 10),
|
||||
database: match[5],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 연결 풀 가져오기
|
||||
*/
|
||||
export const getPool = (): Pool => {
|
||||
if (!pool) {
|
||||
return initializePool();
|
||||
}
|
||||
return pool;
|
||||
};
|
||||
|
||||
/**
|
||||
* 기본 쿼리 실행 함수
|
||||
*
|
||||
* @param text SQL 쿼리 문자열 (Parameterized Query)
|
||||
* @param params 쿼리 파라미터 배열
|
||||
* @returns 쿼리 결과 배열
|
||||
*
|
||||
* @example
|
||||
* const users = await query<User>('SELECT * FROM users WHERE user_id = $1', ['user123']);
|
||||
*/
|
||||
export async function query<T extends QueryResultRow = any>(
|
||||
text: string,
|
||||
params?: any[]
|
||||
): Promise<T[]> {
|
||||
const pool = getPool();
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
const result: PgQueryResult<T> = await client.query(text, params);
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
if (config.debug) {
|
||||
console.log("🔍 쿼리 실행:", {
|
||||
query: text,
|
||||
params,
|
||||
rowCount: result.rowCount,
|
||||
duration: `${duration}ms`,
|
||||
});
|
||||
}
|
||||
|
||||
return result.rows;
|
||||
} catch (error: any) {
|
||||
console.error("❌ 쿼리 실행 실패:", {
|
||||
query: text,
|
||||
params,
|
||||
error: error.message,
|
||||
});
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 단일 행 조회 쿼리 (결과가 없으면 null 반환)
|
||||
*
|
||||
* @param text SQL 쿼리 문자열
|
||||
* @param params 쿼리 파라미터
|
||||
* @returns 단일 행 또는 null
|
||||
*
|
||||
* @example
|
||||
* const user = await queryOne<User>('SELECT * FROM users WHERE user_id = $1', ['user123']);
|
||||
*/
|
||||
export async function queryOne<T extends QueryResultRow = any>(
|
||||
text: string,
|
||||
params?: any[]
|
||||
): Promise<T | null> {
|
||||
const rows = await query<T>(text, params);
|
||||
return rows.length > 0 ? rows[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 트랜잭션 실행 함수
|
||||
*
|
||||
* @param callback 트랜잭션 내에서 실행할 함수
|
||||
* @returns 콜백 함수의 반환값
|
||||
*
|
||||
* @example
|
||||
* const result = await transaction(async (client) => {
|
||||
* await client.query('INSERT INTO users (...) VALUES (...)', []);
|
||||
* await client.query('INSERT INTO user_roles (...) VALUES (...)', []);
|
||||
* return { success: true };
|
||||
* });
|
||||
*/
|
||||
export async function transaction<T>(
|
||||
callback: (client: PoolClient) => Promise<T>
|
||||
): Promise<T> {
|
||||
const pool = getPool();
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
|
||||
if (config.debug) {
|
||||
console.log("🔄 트랜잭션 시작");
|
||||
}
|
||||
|
||||
const result = await callback(client);
|
||||
|
||||
await client.query("COMMIT");
|
||||
|
||||
if (config.debug) {
|
||||
console.log("✅ 트랜잭션 커밋 완료");
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
await client.query("ROLLBACK");
|
||||
|
||||
console.error("❌ 트랜잭션 롤백:", error.message);
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 연결 풀 종료 (앱 종료 시 호출)
|
||||
*/
|
||||
export async function closePool(): Promise<void> {
|
||||
if (pool) {
|
||||
await pool.end();
|
||||
pool = null;
|
||||
console.log("🛑 PostgreSQL 연결 풀 종료");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 연결 풀 상태 확인
|
||||
*/
|
||||
export function getPoolStatus() {
|
||||
const pool = getPool();
|
||||
return {
|
||||
totalCount: pool.totalCount,
|
||||
idleCount: pool.idleCount,
|
||||
waitingCount: pool.waitingCount,
|
||||
};
|
||||
}
|
||||
|
||||
// 기본 익스포트 (편의성)
|
||||
export default {
|
||||
query,
|
||||
queryOne,
|
||||
transaction,
|
||||
getPool,
|
||||
initializePool,
|
||||
closePool,
|
||||
getPoolStatus,
|
||||
};
|
||||
Reference in New Issue
Block a user