970 lines
24 KiB
Markdown
970 lines
24 KiB
Markdown
|
|
# 🚀 Prisma → Raw Query 완전 전환 계획서
|
||
|
|
|
||
|
|
## 📋 프로젝트 개요
|
||
|
|
|
||
|
|
### 🎯 목적
|
||
|
|
|
||
|
|
현재 Node.js 백엔드에서 Prisma ORM을 완전히 제거하고 Raw Query 방식으로 전환하여 **완전 동적 테이블 생성 및 관리 시스템**을 구축합니다.
|
||
|
|
|
||
|
|
### 🔍 현재 상황 분석
|
||
|
|
|
||
|
|
- **총 42개 파일**에서 Prisma 사용
|
||
|
|
- **386개의 Prisma 호출** (ORM + Raw Query 혼재)
|
||
|
|
- **150개 이상의 테이블** 정의 (schema.prisma)
|
||
|
|
- **복잡한 트랜잭션 및 동적 쿼리** 다수 존재
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 📊 Prisma 사용 현황 분석
|
||
|
|
|
||
|
|
### 1. **Prisma 사용 파일 분류**
|
||
|
|
|
||
|
|
#### 🔴 **High Priority (핵심 서비스)**
|
||
|
|
|
||
|
|
```
|
||
|
|
backend-node/src/services/
|
||
|
|
├── authService.ts # 인증 (5개 호출)
|
||
|
|
├── dynamicFormService.ts # 동적 폼 (14개 호출)
|
||
|
|
├── dataflowControlService.ts # 제어관리 (6개 호출)
|
||
|
|
├── multiConnectionQueryService.ts # 다중 연결 (4개 호출)
|
||
|
|
├── tableManagementService.ts # 테이블 관리 (34개 호출)
|
||
|
|
├── screenManagementService.ts # 화면 관리 (40개 호출)
|
||
|
|
└── ddlExecutionService.ts # DDL 실행 (4개 호출)
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 🟡 **Medium Priority (관리 기능)**
|
||
|
|
|
||
|
|
```
|
||
|
|
backend-node/src/services/
|
||
|
|
├── adminService.ts # 관리자 (3개 호출)
|
||
|
|
├── multilangService.ts # 다국어 (22개 호출)
|
||
|
|
├── commonCodeService.ts # 공통코드 (13개 호출)
|
||
|
|
├── externalDbConnectionService.ts # 외부DB (15개 호출)
|
||
|
|
├── batchService.ts # 배치 (13개 호출)
|
||
|
|
└── eventTriggerService.ts # 이벤트 (6개 호출)
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 🟢 **Low Priority (부가 기능)**
|
||
|
|
|
||
|
|
```
|
||
|
|
backend-node/src/services/
|
||
|
|
├── layoutService.ts # 레이아웃 (8개 호출)
|
||
|
|
├── componentStandardService.ts # 컴포넌트 (11개 호출)
|
||
|
|
├── templateStandardService.ts # 템플릿 (8개 호출)
|
||
|
|
├── collectionService.ts # 컬렉션 (11개 호출)
|
||
|
|
└── referenceCacheService.ts # 캐시 (3개 호출)
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2. **복잡도별 분류**
|
||
|
|
|
||
|
|
#### 🔥 **매우 복잡 (트랜잭션 + 동적 쿼리)**
|
||
|
|
|
||
|
|
- `dataflowControlService.ts` - 복잡한 제어 로직
|
||
|
|
- `enhancedDataflowControlService.ts` - 다중 연결 제어
|
||
|
|
- `dynamicFormService.ts` - UPSERT 및 동적 테이블 처리
|
||
|
|
- `multiConnectionQueryService.ts` - 외부 DB 연결
|
||
|
|
|
||
|
|
#### 🟠 **복잡 (Raw Query 혼재)**
|
||
|
|
|
||
|
|
- `tableManagementService.ts` - 테이블 메타데이터 관리
|
||
|
|
- `screenManagementService.ts` - 화면 정의 관리
|
||
|
|
- `eventTriggerService.ts` - JSON 검색 쿼리
|
||
|
|
|
||
|
|
#### 🟡 **중간 (단순 CRUD)**
|
||
|
|
|
||
|
|
- `authService.ts` - 사용자 인증
|
||
|
|
- `adminService.ts` - 관리자 메뉴
|
||
|
|
- `commonCodeService.ts` - 코드 관리
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🏗️ Raw Query 아키텍처 설계
|
||
|
|
|
||
|
|
### 1. **새로운 데이터베이스 매니저**
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// config/databaseManager.ts
|
||
|
|
import { Pool, PoolClient } from "pg";
|
||
|
|
|
||
|
|
export class DatabaseManager {
|
||
|
|
private static pool: Pool;
|
||
|
|
|
||
|
|
static initialize() {
|
||
|
|
this.pool = new Pool({
|
||
|
|
host: process.env.DB_HOST,
|
||
|
|
port: parseInt(process.env.DB_PORT || "5432"),
|
||
|
|
database: process.env.DB_NAME,
|
||
|
|
user: process.env.DB_USER,
|
||
|
|
password: process.env.DB_PASSWORD,
|
||
|
|
max: 20,
|
||
|
|
idleTimeoutMillis: 30000,
|
||
|
|
connectionTimeoutMillis: 2000,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// 기본 쿼리 실행
|
||
|
|
static async query(text: string, params?: any[]): Promise<any[]> {
|
||
|
|
const client = await this.pool.connect();
|
||
|
|
try {
|
||
|
|
const result = await client.query(text, params);
|
||
|
|
return result.rows;
|
||
|
|
} finally {
|
||
|
|
client.release();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 트랜잭션 실행
|
||
|
|
static async transaction<T>(
|
||
|
|
callback: (client: PoolClient) => Promise<T>
|
||
|
|
): Promise<T> {
|
||
|
|
const client = await this.pool.connect();
|
||
|
|
try {
|
||
|
|
await client.query("BEGIN");
|
||
|
|
const result = await callback(client);
|
||
|
|
await client.query("COMMIT");
|
||
|
|
return result;
|
||
|
|
} catch (error) {
|
||
|
|
await client.query("ROLLBACK");
|
||
|
|
throw error;
|
||
|
|
} finally {
|
||
|
|
client.release();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 연결 종료
|
||
|
|
static async close() {
|
||
|
|
await this.pool.end();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2. **동적 쿼리 빌더**
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// utils/queryBuilder.ts
|
||
|
|
export class QueryBuilder {
|
||
|
|
// SELECT 쿼리 빌더
|
||
|
|
static select(
|
||
|
|
tableName: string,
|
||
|
|
options: {
|
||
|
|
columns?: string[];
|
||
|
|
where?: Record<string, any>;
|
||
|
|
orderBy?: string;
|
||
|
|
limit?: number;
|
||
|
|
offset?: number;
|
||
|
|
joins?: Array<{
|
||
|
|
type: "INNER" | "LEFT" | "RIGHT";
|
||
|
|
table: string;
|
||
|
|
on: string;
|
||
|
|
}>;
|
||
|
|
} = {}
|
||
|
|
) {
|
||
|
|
const {
|
||
|
|
columns = ["*"],
|
||
|
|
where = {},
|
||
|
|
orderBy,
|
||
|
|
limit,
|
||
|
|
offset,
|
||
|
|
joins = [],
|
||
|
|
} = options;
|
||
|
|
|
||
|
|
let query = `SELECT ${columns.join(", ")} FROM ${tableName}`;
|
||
|
|
const params: any[] = [];
|
||
|
|
let paramIndex = 1;
|
||
|
|
|
||
|
|
// JOIN 처리
|
||
|
|
joins.forEach((join) => {
|
||
|
|
query += ` ${join.type} JOIN ${join.table} ON ${join.on}`;
|
||
|
|
});
|
||
|
|
|
||
|
|
// WHERE 조건
|
||
|
|
if (Object.keys(where).length > 0) {
|
||
|
|
const whereClause = Object.keys(where)
|
||
|
|
.map((key) => `${key} = $${paramIndex++}`)
|
||
|
|
.join(" AND ");
|
||
|
|
query += ` WHERE ${whereClause}`;
|
||
|
|
params.push(...Object.values(where));
|
||
|
|
}
|
||
|
|
|
||
|
|
// ORDER BY
|
||
|
|
if (orderBy) {
|
||
|
|
query += ` ORDER BY ${orderBy}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
// LIMIT/OFFSET
|
||
|
|
if (limit) {
|
||
|
|
query += ` LIMIT $${paramIndex++}`;
|
||
|
|
params.push(limit);
|
||
|
|
}
|
||
|
|
if (offset) {
|
||
|
|
query += ` OFFSET $${paramIndex++}`;
|
||
|
|
params.push(offset);
|
||
|
|
}
|
||
|
|
|
||
|
|
return { query, params };
|
||
|
|
}
|
||
|
|
|
||
|
|
// INSERT 쿼리 빌더
|
||
|
|
static insert(
|
||
|
|
tableName: string,
|
||
|
|
data: Record<string, any>,
|
||
|
|
options: {
|
||
|
|
returning?: string[];
|
||
|
|
onConflict?: {
|
||
|
|
columns: string[];
|
||
|
|
action: "DO NOTHING" | "DO UPDATE";
|
||
|
|
updateSet?: string[];
|
||
|
|
};
|
||
|
|
} = {}
|
||
|
|
) {
|
||
|
|
const columns = Object.keys(data);
|
||
|
|
const values = Object.values(data);
|
||
|
|
const placeholders = values.map((_, index) => `$${index + 1}`).join(", ");
|
||
|
|
|
||
|
|
let query = `INSERT INTO ${tableName} (${columns.join(
|
||
|
|
", "
|
||
|
|
)}) VALUES (${placeholders})`;
|
||
|
|
|
||
|
|
// ON CONFLICT 처리 (UPSERT)
|
||
|
|
if (options.onConflict) {
|
||
|
|
const {
|
||
|
|
columns: conflictColumns,
|
||
|
|
action,
|
||
|
|
updateSet,
|
||
|
|
} = options.onConflict;
|
||
|
|
query += ` ON CONFLICT (${conflictColumns.join(", ")}) ${action}`;
|
||
|
|
|
||
|
|
if (action === "DO UPDATE" && updateSet) {
|
||
|
|
const setClause = updateSet
|
||
|
|
.map((col) => `${col} = EXCLUDED.${col}`)
|
||
|
|
.join(", ");
|
||
|
|
query += ` SET ${setClause}`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// RETURNING 처리
|
||
|
|
if (options.returning) {
|
||
|
|
query += ` RETURNING ${options.returning.join(", ")}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
return { query, params: values };
|
||
|
|
}
|
||
|
|
|
||
|
|
// UPDATE 쿼리 빌더
|
||
|
|
static update(
|
||
|
|
tableName: string,
|
||
|
|
data: Record<string, any>,
|
||
|
|
where: Record<string, any>
|
||
|
|
) {
|
||
|
|
const setClause = Object.keys(data)
|
||
|
|
.map((key, index) => `${key} = $${index + 1}`)
|
||
|
|
.join(", ");
|
||
|
|
|
||
|
|
const whereClause = Object.keys(where)
|
||
|
|
.map((key, index) => `${key} = $${Object.keys(data).length + index + 1}`)
|
||
|
|
.join(" AND ");
|
||
|
|
|
||
|
|
const query = `UPDATE ${tableName} SET ${setClause} WHERE ${whereClause} RETURNING *`;
|
||
|
|
const params = [...Object.values(data), ...Object.values(where)];
|
||
|
|
|
||
|
|
return { query, params };
|
||
|
|
}
|
||
|
|
|
||
|
|
// DELETE 쿼리 빌더
|
||
|
|
static delete(tableName: string, where: Record<string, any>) {
|
||
|
|
const whereClause = Object.keys(where)
|
||
|
|
.map((key, index) => `${key} = $${index + 1}`)
|
||
|
|
.join(" AND ");
|
||
|
|
|
||
|
|
const query = `DELETE FROM ${tableName} WHERE ${whereClause} RETURNING *`;
|
||
|
|
const params = Object.values(where);
|
||
|
|
|
||
|
|
return { query, params };
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3. **타입 안전성 보장**
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// types/database.ts
|
||
|
|
export interface QueryResult<T = any> {
|
||
|
|
rows: T[];
|
||
|
|
rowCount: number;
|
||
|
|
command: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface TableSchema {
|
||
|
|
tableName: string;
|
||
|
|
columns: ColumnDefinition[];
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface ColumnDefinition {
|
||
|
|
name: string;
|
||
|
|
type: string;
|
||
|
|
nullable?: boolean;
|
||
|
|
defaultValue?: string;
|
||
|
|
isPrimaryKey?: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 런타임 검증
|
||
|
|
export class DatabaseValidator {
|
||
|
|
static validateTableName(tableName: string): boolean {
|
||
|
|
return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName) && tableName.length <= 63;
|
||
|
|
}
|
||
|
|
|
||
|
|
static validateColumnName(columnName: string): boolean {
|
||
|
|
return (
|
||
|
|
/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(columnName) && columnName.length <= 63
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
static sanitizeInput(input: any): any {
|
||
|
|
if (typeof input === "string") {
|
||
|
|
return input.replace(/[';--]/g, "");
|
||
|
|
}
|
||
|
|
return input;
|
||
|
|
}
|
||
|
|
|
||
|
|
static validateWhereClause(where: Record<string, any>): boolean {
|
||
|
|
return Object.keys(where).every((key) => this.validateColumnName(key));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 📅 단계별 마이그레이션 계획
|
||
|
|
|
||
|
|
### **Phase 1: 기반 구조 구축 (1주)**
|
||
|
|
|
||
|
|
#### 1.1 새로운 데이터베이스 아키텍처 구현
|
||
|
|
|
||
|
|
- [ ] `DatabaseManager` 클래스 구현
|
||
|
|
- [ ] `QueryBuilder` 유틸리티 구현
|
||
|
|
- [ ] 타입 정의 및 검증 로직 구현
|
||
|
|
- [ ] 연결 풀 및 트랜잭션 관리
|
||
|
|
|
||
|
|
#### 1.2 테스트 환경 구축
|
||
|
|
|
||
|
|
- [ ] 단위 테스트 작성
|
||
|
|
- [ ] 통합 테스트 환경 구성
|
||
|
|
- [ ] 성능 벤치마크 도구 준비
|
||
|
|
|
||
|
|
### **Phase 2: 핵심 서비스 전환 (2주)**
|
||
|
|
|
||
|
|
#### 2.1 인증 서비스 전환 (우선순위 1)
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 기존 Prisma 코드
|
||
|
|
const userInfo = await prisma.user_info.findUnique({
|
||
|
|
where: { user_id: userId },
|
||
|
|
});
|
||
|
|
|
||
|
|
// 새로운 Raw Query 코드
|
||
|
|
const { query, params } = QueryBuilder.select("user_info", {
|
||
|
|
where: { user_id: userId },
|
||
|
|
});
|
||
|
|
const userInfo = await DatabaseManager.query(query, params);
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 2.2 동적 폼 서비스 전환 (우선순위 2)
|
||
|
|
|
||
|
|
- [ ] UPSERT 로직 Raw Query로 전환
|
||
|
|
- [ ] 동적 테이블 처리 로직 개선
|
||
|
|
- [ ] 트랜잭션 처리 최적화
|
||
|
|
|
||
|
|
#### 2.3 제어관리 서비스 전환 (우선순위 3)
|
||
|
|
|
||
|
|
- [ ] 복잡한 조건부 쿼리 전환
|
||
|
|
- [ ] 다중 테이블 업데이트 로직 개선
|
||
|
|
- [ ] 에러 핸들링 강화
|
||
|
|
|
||
|
|
### **Phase 3: 관리 기능 전환 (1.5주)**
|
||
|
|
|
||
|
|
#### 3.1 테이블 관리 서비스
|
||
|
|
|
||
|
|
- [ ] 메타데이터 조회 쿼리 전환
|
||
|
|
- [ ] 동적 컬럼 추가/삭제 로직
|
||
|
|
- [ ] 인덱스 관리 기능
|
||
|
|
|
||
|
|
#### 3.2 화면 관리 서비스
|
||
|
|
|
||
|
|
- [ ] JSON 데이터 처리 최적화
|
||
|
|
- [ ] 복잡한 조인 쿼리 전환
|
||
|
|
- [ ] 캐싱 메커니즘 구현
|
||
|
|
|
||
|
|
#### 3.3 다국어 서비스
|
||
|
|
|
||
|
|
- [ ] 재귀 쿼리 (WITH RECURSIVE) 전환
|
||
|
|
- [ ] 번역 데이터 관리 최적화
|
||
|
|
|
||
|
|
### **Phase 4: 부가 기능 전환 (1주)**
|
||
|
|
|
||
|
|
#### 4.1 배치 및 외부 연결
|
||
|
|
|
||
|
|
- [ ] 배치 스케줄러 전환
|
||
|
|
- [ ] 외부 DB 연결 관리
|
||
|
|
- [ ] 로그 및 모니터링
|
||
|
|
|
||
|
|
#### 4.2 표준 관리 기능
|
||
|
|
|
||
|
|
- [ ] 컴포넌트 표준 관리
|
||
|
|
- [ ] 템플릿 표준 관리
|
||
|
|
- [ ] 레이아웃 관리
|
||
|
|
|
||
|
|
### **Phase 5: Prisma 완전 제거 (0.5주)**
|
||
|
|
|
||
|
|
#### 5.1 Prisma 의존성 제거
|
||
|
|
|
||
|
|
- [ ] `package.json`에서 Prisma 제거
|
||
|
|
- [ ] `schema.prisma` 파일 삭제
|
||
|
|
- [ ] 관련 설정 파일 정리
|
||
|
|
|
||
|
|
#### 5.2 최종 검증 및 최적화
|
||
|
|
|
||
|
|
- [ ] 전체 기능 테스트
|
||
|
|
- [ ] 성능 최적화
|
||
|
|
- [ ] 문서화 업데이트
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🔄 마이그레이션 전략
|
||
|
|
|
||
|
|
### 1. **점진적 전환 방식**
|
||
|
|
|
||
|
|
#### 단계별 전환
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 1단계: 기존 Prisma 코드 유지하면서 Raw Query 병행
|
||
|
|
class AuthService {
|
||
|
|
// 기존 방식 (임시 유지)
|
||
|
|
async loginWithPrisma(userId: string) {
|
||
|
|
return await prisma.user_info.findUnique({
|
||
|
|
where: { user_id: userId },
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// 새로운 방식 (점진적 도입)
|
||
|
|
async loginWithRawQuery(userId: string) {
|
||
|
|
const { query, params } = QueryBuilder.select("user_info", {
|
||
|
|
where: { user_id: userId },
|
||
|
|
});
|
||
|
|
return await DatabaseManager.query(query, params);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 2단계: 기존 메서드를 새로운 방식으로 교체
|
||
|
|
class AuthService {
|
||
|
|
async login(userId: string) {
|
||
|
|
return await this.loginWithRawQuery(userId);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 3단계: 기존 코드 완전 제거
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2. **호환성 레이어**
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// utils/prismaCompatibility.ts
|
||
|
|
export class PrismaCompatibilityLayer {
|
||
|
|
// 기존 Prisma 호출을 Raw Query로 변환하는 어댑터
|
||
|
|
static async findUnique(model: string, options: any) {
|
||
|
|
const { where } = options;
|
||
|
|
const { query, params } = QueryBuilder.select(model, { where });
|
||
|
|
const results = await DatabaseManager.query(query, params);
|
||
|
|
return results[0] || null;
|
||
|
|
}
|
||
|
|
|
||
|
|
static async findMany(model: string, options: any = {}) {
|
||
|
|
const { where, orderBy, take: limit, skip: offset } = options;
|
||
|
|
const { query, params } = QueryBuilder.select(model, {
|
||
|
|
where,
|
||
|
|
orderBy,
|
||
|
|
limit,
|
||
|
|
offset,
|
||
|
|
});
|
||
|
|
return await DatabaseManager.query(query, params);
|
||
|
|
}
|
||
|
|
|
||
|
|
static async create(model: string, options: any) {
|
||
|
|
const { data } = options;
|
||
|
|
const { query, params } = QueryBuilder.insert(model, data, {
|
||
|
|
returning: ["*"],
|
||
|
|
});
|
||
|
|
const results = await DatabaseManager.query(query, params);
|
||
|
|
return results[0];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3. **테스트 전략**
|
||
|
|
|
||
|
|
#### 병렬 테스트
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// tests/migration.test.ts
|
||
|
|
describe("Prisma to Raw Query Migration", () => {
|
||
|
|
test("AuthService: 동일한 결과 반환", async () => {
|
||
|
|
const userId = "test_user";
|
||
|
|
|
||
|
|
// 기존 Prisma 결과
|
||
|
|
const prismaResult = await authService.loginWithPrisma(userId);
|
||
|
|
|
||
|
|
// 새로운 Raw Query 결과
|
||
|
|
const rawQueryResult = await authService.loginWithRawQuery(userId);
|
||
|
|
|
||
|
|
// 결과 비교
|
||
|
|
expect(rawQueryResult).toEqual(prismaResult);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🚨 위험 요소 및 대응 방안
|
||
|
|
|
||
|
|
### 1. **데이터 일관성 위험**
|
||
|
|
|
||
|
|
#### 위험 요소
|
||
|
|
|
||
|
|
- 트랜잭션 처리 미스
|
||
|
|
- 타입 변환 오류
|
||
|
|
- NULL 처리 차이
|
||
|
|
|
||
|
|
#### 대응 방안
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 엄격한 트랜잭션 관리
|
||
|
|
export class TransactionManager {
|
||
|
|
static async executeInTransaction<T>(
|
||
|
|
operations: ((client: PoolClient) => Promise<T>)[]
|
||
|
|
): Promise<T[]> {
|
||
|
|
return await DatabaseManager.transaction(async (client) => {
|
||
|
|
const results: T[] = [];
|
||
|
|
for (const operation of operations) {
|
||
|
|
const result = await operation(client);
|
||
|
|
results.push(result);
|
||
|
|
}
|
||
|
|
return results;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 타입 안전성 검증
|
||
|
|
export class TypeConverter {
|
||
|
|
static toPostgresType(value: any, expectedType: string): any {
|
||
|
|
switch (expectedType) {
|
||
|
|
case "integer":
|
||
|
|
return parseInt(value) || null;
|
||
|
|
case "decimal":
|
||
|
|
return parseFloat(value) || null;
|
||
|
|
case "boolean":
|
||
|
|
return Boolean(value);
|
||
|
|
case "timestamp":
|
||
|
|
return value ? new Date(value) : null;
|
||
|
|
default:
|
||
|
|
return value;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2. **성능 저하 위험**
|
||
|
|
|
||
|
|
#### 위험 요소
|
||
|
|
|
||
|
|
- 연결 풀 관리 미흡
|
||
|
|
- 쿼리 최적화 부족
|
||
|
|
- 캐싱 메커니즘 부재
|
||
|
|
|
||
|
|
#### 대응 방안
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 연결 풀 최적화
|
||
|
|
export class ConnectionPoolManager {
|
||
|
|
private static readonly DEFAULT_POOL_CONFIG = {
|
||
|
|
min: 2,
|
||
|
|
max: 20,
|
||
|
|
acquireTimeoutMillis: 30000,
|
||
|
|
createTimeoutMillis: 30000,
|
||
|
|
destroyTimeoutMillis: 5000,
|
||
|
|
idleTimeoutMillis: 30000,
|
||
|
|
reapIntervalMillis: 1000,
|
||
|
|
createRetryIntervalMillis: 200,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
// 쿼리 캐싱
|
||
|
|
export class QueryCache {
|
||
|
|
private static cache = new Map<string, { data: any; timestamp: number }>();
|
||
|
|
private static readonly CACHE_TTL = 5 * 60 * 1000; // 5분
|
||
|
|
|
||
|
|
static get(key: string): any | null {
|
||
|
|
const cached = this.cache.get(key);
|
||
|
|
if (cached && Date.now() - cached.timestamp < this.CACHE_TTL) {
|
||
|
|
return cached.data;
|
||
|
|
}
|
||
|
|
this.cache.delete(key);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
static set(key: string, data: any): void {
|
||
|
|
this.cache.set(key, { data, timestamp: Date.now() });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3. **개발 생산성 저하**
|
||
|
|
|
||
|
|
#### 위험 요소
|
||
|
|
|
||
|
|
- 타입 안전성 부족
|
||
|
|
- 디버깅 어려움
|
||
|
|
- 코드 복잡성 증가
|
||
|
|
|
||
|
|
#### 대응 방안
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 개발자 친화적 인터페이스
|
||
|
|
export class DatabaseORM {
|
||
|
|
// Prisma와 유사한 인터페이스 제공
|
||
|
|
user_info = {
|
||
|
|
findUnique: (options: { where: Record<string, any> }) =>
|
||
|
|
PrismaCompatibilityLayer.findUnique("user_info", options),
|
||
|
|
|
||
|
|
findMany: (options?: any) =>
|
||
|
|
PrismaCompatibilityLayer.findMany("user_info", options),
|
||
|
|
|
||
|
|
create: (options: { data: Record<string, any> }) =>
|
||
|
|
PrismaCompatibilityLayer.create("user_info", options),
|
||
|
|
};
|
||
|
|
|
||
|
|
// 다른 테이블들도 동일한 패턴으로 구현
|
||
|
|
}
|
||
|
|
|
||
|
|
// 디버깅 도구
|
||
|
|
export class QueryLogger {
|
||
|
|
static log(query: string, params: any[], executionTime: number) {
|
||
|
|
if (process.env.NODE_ENV === "development") {
|
||
|
|
console.log(`🔍 Query: ${query}`);
|
||
|
|
console.log(`📊 Params: ${JSON.stringify(params)}`);
|
||
|
|
console.log(`⏱️ Time: ${executionTime}ms`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 📈 성능 최적화 전략
|
||
|
|
|
||
|
|
### 1. **연결 풀 최적화**
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// config/optimizedPool.ts
|
||
|
|
export class OptimizedPoolConfig {
|
||
|
|
static getConfig() {
|
||
|
|
return {
|
||
|
|
// 환경별 최적화된 설정
|
||
|
|
max: process.env.NODE_ENV === "production" ? 20 : 5,
|
||
|
|
min: process.env.NODE_ENV === "production" ? 5 : 2,
|
||
|
|
|
||
|
|
// 연결 타임아웃 최적화
|
||
|
|
acquireTimeoutMillis: 30000,
|
||
|
|
createTimeoutMillis: 30000,
|
||
|
|
|
||
|
|
// 유휴 연결 관리
|
||
|
|
idleTimeoutMillis: 600000, // 10분
|
||
|
|
|
||
|
|
// 연결 검증
|
||
|
|
testOnBorrow: true,
|
||
|
|
validationQuery: "SELECT 1",
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2. **쿼리 최적화**
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// utils/queryOptimizer.ts
|
||
|
|
export class QueryOptimizer {
|
||
|
|
// 인덱스 힌트 추가
|
||
|
|
static addIndexHint(query: string, indexName: string): string {
|
||
|
|
return query.replace(
|
||
|
|
/FROM\s+(\w+)/i,
|
||
|
|
`FROM $1 /*+ INDEX($1 ${indexName}) */`
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 쿼리 분석 및 최적화 제안
|
||
|
|
static analyzeQuery(query: string): QueryAnalysis {
|
||
|
|
return {
|
||
|
|
hasIndex: this.checkIndexUsage(query),
|
||
|
|
estimatedRows: this.estimateRowCount(query),
|
||
|
|
suggestions: this.generateOptimizationSuggestions(query),
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3. **캐싱 전략**
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// utils/smartCache.ts
|
||
|
|
export class SmartCache {
|
||
|
|
private static redis: Redis; // Redis 클라이언트
|
||
|
|
|
||
|
|
// 테이블별 캐시 전략
|
||
|
|
static async get(key: string, tableName: string): Promise<any> {
|
||
|
|
const cacheConfig = this.getCacheConfig(tableName);
|
||
|
|
|
||
|
|
if (!cacheConfig.enabled) return null;
|
||
|
|
|
||
|
|
const cached = await this.redis.get(key);
|
||
|
|
return cached ? JSON.parse(cached) : null;
|
||
|
|
}
|
||
|
|
|
||
|
|
static async set(key: string, data: any, tableName: string): Promise<void> {
|
||
|
|
const cacheConfig = this.getCacheConfig(tableName);
|
||
|
|
|
||
|
|
if (cacheConfig.enabled) {
|
||
|
|
await this.redis.setex(key, cacheConfig.ttl, JSON.stringify(data));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private static getCacheConfig(tableName: string) {
|
||
|
|
const configs = {
|
||
|
|
user_info: { enabled: true, ttl: 300 }, // 5분
|
||
|
|
menu_info: { enabled: true, ttl: 600 }, // 10분
|
||
|
|
dynamic_tables: { enabled: false, ttl: 0 }, // 동적 테이블은 캐시 안함
|
||
|
|
};
|
||
|
|
|
||
|
|
return configs[tableName] || { enabled: false, ttl: 0 };
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🧪 테스트 전략
|
||
|
|
|
||
|
|
### 1. **단위 테스트**
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// tests/unit/queryBuilder.test.ts
|
||
|
|
describe("QueryBuilder", () => {
|
||
|
|
test("SELECT 쿼리 생성", () => {
|
||
|
|
const { query, params } = QueryBuilder.select("user_info", {
|
||
|
|
where: { user_id: "test" },
|
||
|
|
limit: 10,
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(query).toBe("SELECT * FROM user_info WHERE user_id = $1 LIMIT $2");
|
||
|
|
expect(params).toEqual(["test", 10]);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("복잡한 JOIN 쿼리", () => {
|
||
|
|
const { query, params } = QueryBuilder.select("user_info", {
|
||
|
|
joins: [
|
||
|
|
{
|
||
|
|
type: "LEFT",
|
||
|
|
table: "dept_info",
|
||
|
|
on: "user_info.dept_code = dept_info.dept_code",
|
||
|
|
},
|
||
|
|
],
|
||
|
|
where: { "user_info.status": "active" },
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(query).toContain("LEFT JOIN dept_info");
|
||
|
|
expect(query).toContain("WHERE user_info.status = $1");
|
||
|
|
});
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2. **통합 테스트**
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// tests/integration/migration.test.ts
|
||
|
|
describe("Migration Integration Tests", () => {
|
||
|
|
let prismaService: any;
|
||
|
|
let rawQueryService: any;
|
||
|
|
|
||
|
|
beforeAll(async () => {
|
||
|
|
// 테스트 데이터베이스 설정
|
||
|
|
await setupTestDatabase();
|
||
|
|
});
|
||
|
|
|
||
|
|
test("동일한 결과 반환 - 사용자 조회", async () => {
|
||
|
|
const testUserId = "integration_test_user";
|
||
|
|
|
||
|
|
const prismaResult = await prismaService.getUser(testUserId);
|
||
|
|
const rawQueryResult = await rawQueryService.getUser(testUserId);
|
||
|
|
|
||
|
|
expect(normalizeResult(rawQueryResult)).toEqual(
|
||
|
|
normalizeResult(prismaResult)
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("트랜잭션 일관성 - 복잡한 업데이트", async () => {
|
||
|
|
const testData = {
|
||
|
|
/* 테스트 데이터 */
|
||
|
|
};
|
||
|
|
|
||
|
|
// Prisma 트랜잭션
|
||
|
|
const prismaResult = await prismaService.complexUpdate(testData);
|
||
|
|
|
||
|
|
// Raw Query 트랜잭션
|
||
|
|
const rawQueryResult = await rawQueryService.complexUpdate(testData);
|
||
|
|
|
||
|
|
expect(rawQueryResult.success).toBe(prismaResult.success);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3. **성능 테스트**
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// tests/performance/benchmark.test.ts
|
||
|
|
describe("Performance Benchmarks", () => {
|
||
|
|
test("대량 데이터 조회 성능", async () => {
|
||
|
|
const iterations = 1000;
|
||
|
|
|
||
|
|
// Prisma 성능 측정
|
||
|
|
const prismaStart = Date.now();
|
||
|
|
for (let i = 0; i < iterations; i++) {
|
||
|
|
await prismaService.getLargeDataset();
|
||
|
|
}
|
||
|
|
const prismaTime = Date.now() - prismaStart;
|
||
|
|
|
||
|
|
// Raw Query 성능 측정
|
||
|
|
const rawQueryStart = Date.now();
|
||
|
|
for (let i = 0; i < iterations; i++) {
|
||
|
|
await rawQueryService.getLargeDataset();
|
||
|
|
}
|
||
|
|
const rawQueryTime = Date.now() - rawQueryStart;
|
||
|
|
|
||
|
|
console.log(`Prisma: ${prismaTime}ms, Raw Query: ${rawQueryTime}ms`);
|
||
|
|
|
||
|
|
// Raw Query가 더 빠르거나 비슷해야 함
|
||
|
|
expect(rawQueryTime).toBeLessThanOrEqual(prismaTime * 1.1);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 📋 체크리스트
|
||
|
|
|
||
|
|
### **Phase 1: 기반 구조 (1주)**
|
||
|
|
|
||
|
|
- [ ] DatabaseManager 클래스 구현
|
||
|
|
- [ ] QueryBuilder 유틸리티 구현
|
||
|
|
- [ ] 타입 정의 및 검증 로직
|
||
|
|
- [ ] 연결 풀 설정 및 최적화
|
||
|
|
- [ ] 트랜잭션 관리 시스템
|
||
|
|
- [ ] 에러 핸들링 메커니즘
|
||
|
|
- [ ] 로깅 및 모니터링 도구
|
||
|
|
- [ ] 단위 테스트 작성
|
||
|
|
|
||
|
|
### **Phase 2: 핵심 서비스 (2주)**
|
||
|
|
|
||
|
|
- [ ] AuthService 전환 및 테스트
|
||
|
|
- [ ] DynamicFormService 전환 (UPSERT 포함)
|
||
|
|
- [ ] DataflowControlService 전환 (복잡한 로직)
|
||
|
|
- [ ] MultiConnectionQueryService 전환
|
||
|
|
- [ ] TableManagementService 전환
|
||
|
|
- [ ] ScreenManagementService 전환
|
||
|
|
- [ ] DDLExecutionService 전환
|
||
|
|
- [ ] 통합 테스트 실행
|
||
|
|
|
||
|
|
### **Phase 3: 관리 기능 (1.5주)**
|
||
|
|
|
||
|
|
- [ ] AdminService 전환
|
||
|
|
- [ ] MultiLangService 전환 (재귀 쿼리)
|
||
|
|
- [ ] CommonCodeService 전환
|
||
|
|
- [ ] ExternalDbConnectionService 전환
|
||
|
|
- [ ] BatchService 및 관련 서비스 전환
|
||
|
|
- [ ] EventTriggerService 전환
|
||
|
|
- [ ] 기능별 테스트 완료
|
||
|
|
|
||
|
|
### **Phase 4: 부가 기능 (1주)**
|
||
|
|
|
||
|
|
- [ ] LayoutService 전환
|
||
|
|
- [ ] ComponentStandardService 전환
|
||
|
|
- [ ] TemplateStandardService 전환
|
||
|
|
- [ ] CollectionService 전환
|
||
|
|
- [ ] ReferenceCacheService 전환
|
||
|
|
- [ ] 기타 컨트롤러 전환
|
||
|
|
- [ ] 전체 기능 테스트
|
||
|
|
|
||
|
|
### **Phase 5: 완전 제거 (0.5주)**
|
||
|
|
|
||
|
|
- [ ] Prisma 의존성 제거
|
||
|
|
- [ ] schema.prisma 삭제
|
||
|
|
- [ ] 관련 설정 파일 정리
|
||
|
|
- [ ] 문서 업데이트
|
||
|
|
- [ ] 최종 성능 테스트
|
||
|
|
- [ ] 배포 준비
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🎯 성공 기준
|
||
|
|
|
||
|
|
### **기능적 요구사항**
|
||
|
|
|
||
|
|
- [ ] 모든 기존 기능이 동일하게 작동
|
||
|
|
- [ ] 동적 테이블 생성/관리 완벽 지원
|
||
|
|
- [ ] 트랜잭션 일관성 보장
|
||
|
|
- [ ] 에러 처리 및 복구 메커니즘
|
||
|
|
|
||
|
|
### **성능 요구사항**
|
||
|
|
|
||
|
|
- [ ] 기존 대비 성능 저하 없음 (±10% 이내)
|
||
|
|
- [ ] 메모리 사용량 최적화
|
||
|
|
- [ ] 연결 풀 효율성 개선
|
||
|
|
- [ ] 쿼리 실행 시간 단축
|
||
|
|
|
||
|
|
### **품질 요구사항**
|
||
|
|
|
||
|
|
- [ ] 코드 커버리지 90% 이상
|
||
|
|
- [ ] 모든 테스트 케이스 통과
|
||
|
|
- [ ] 타입 안전성 보장
|
||
|
|
- [ ] 보안 검증 완료
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 📚 참고 자료
|
||
|
|
|
||
|
|
### **기술 문서**
|
||
|
|
|
||
|
|
- [PostgreSQL 공식 문서](https://www.postgresql.org/docs/)
|
||
|
|
- [Node.js pg 라이브러리](https://node-postgres.com/)
|
||
|
|
- [SQL 쿼리 최적화 가이드](https://use-the-index-luke.com/)
|
||
|
|
|
||
|
|
### **내부 문서**
|
||
|
|
|
||
|
|
- [현재 데이터베이스 스키마](backend-node/prisma/schema.prisma)
|
||
|
|
- [기존 Java 시스템 구조](src/com/pms/)
|
||
|
|
- [동적 테이블 생성 계획서](테이블_동적_생성_기능_개발_계획서.md)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## ⚠️ 주의사항
|
||
|
|
|
||
|
|
1. **데이터 백업**: 마이그레이션 전 전체 데이터베이스 백업 필수
|
||
|
|
2. **점진적 전환**: 한 번에 모든 것을 바꾸지 말고 단계별로 진행
|
||
|
|
3. **철저한 테스트**: 각 단계마다 충분한 테스트 수행
|
||
|
|
4. **롤백 계획**: 문제 발생 시 즉시 롤백할 수 있는 계획 수립
|
||
|
|
5. **모니터링**: 전환 후 성능 및 안정성 지속 모니터링
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
**총 예상 기간: 6주**
|
||
|
|
**핵심 개발자: 2-3명**
|
||
|
|
**위험도: 중간 (적절한 계획과 테스트로 관리 가능)**
|
||
|
|
|
||
|
|
이 계획을 통해 Prisma를 완전히 제거하고 진정한 동적 데이터베이스 시스템을 구축할 수 있습니다! 🚀
|