ERP-node/PRISMA_TO_RAW_QUERY_MIGRATI...

40 KiB

🚀 Prisma → Raw Query 완전 전환 계획서

📋 프로젝트 개요

🎯 목적

현재 Node.js 백엔드에서 Prisma ORM을 완전히 제거하고 Raw Query 방식으로 전환하여 완전 동적 테이블 생성 및 관리 시스템을 구축합니다.

🔍 현재 상황 분석

  • 총 52개 파일에서 Prisma 사용
  • 490개의 Prisma 호출 (ORM + Raw Query 혼재)
  • 150개 이상의 테이블 정의 (schema.prisma)
  • 복잡한 트랜잭션 및 동적 쿼리 다수 존재

📊 Prisma 사용 현황 분석

총 42개 파일에서 444개의 Prisma 호출 발견 (Scripts 제외)

1. Prisma 사용 파일 분류

🔴 High Priority (핵심 서비스) - 107개 호출

backend-node/src/services/
├── screenManagementService.ts        # 화면 관리 (46개 호출) ⭐ 최우선
├── tableManagementService.ts         # 테이블 관리 (35개 호출) ⭐ 최우선
├── dataflowService.ts                # 데이터플로우 (31개 호출) ⭐ 신규 발견
├── dynamicFormService.ts             # 동적 폼 (15개 호출)
├── externalDbConnectionService.ts    # 외부DB (15개 호출)
├── dataflowControlService.ts         # 제어관리 (6개 호출)
├── ddlExecutionService.ts            # DDL 실행 (6개 호출)
├── authService.ts                    # 인증 (5개 호출)
└── multiConnectionQueryService.ts    # 다중 연결 (4개 호출)

🟡 Medium Priority (관리 기능) - 142개 호출

backend-node/src/services/
├── multilangService.ts               # 다국어 (25개 호출)
├── batchService.ts                   # 배치 (16개 호출)
├── componentStandardService.ts       # 컴포넌트 (16개 호출)
├── commonCodeService.ts              # 공통코드 (15개 호출)
├── dataflowDiagramService.ts         # 데이터플로우 다이어그램 (12개 호출) ⭐ 신규 발견
├── collectionService.ts              # 컬렉션 (11개 호출)
├── layoutService.ts                  # 레이아웃 (10개 호출)
├── dbTypeCategoryService.ts          # DB 타입 카테고리 (10개 호출) ⭐ 신규 발견
├── templateStandardService.ts        # 템플릿 (9개 호출)
├── ddlAuditLogger.ts                 # DDL 감사 로그 (8개 호출) ⭐ 신규 발견
├── externalCallConfigService.ts      # 외부 호출 설정 (8개 호출) ⭐ 신규 발견
├── batchExternalDbService.ts         # 배치 외부DB (8개 호출) ⭐ 신규 발견
├── batchExecutionLogService.ts       # 배치 실행 로그 (7개 호출) ⭐ 신규 발견
├── eventTriggerService.ts            # 이벤트 (6개 호출)
├── enhancedDynamicFormService.ts     # 확장 동적 폼 (6개 호출) ⭐ 신규 발견
├── entityJoinService.ts              # 엔티티 조인 (5개 호출) ⭐ 신규 발견
├── dataMappingService.ts             # 데이터 매핑 (5개 호출) ⭐ 신규 발견
├── batchManagementService.ts         # 배치 관리 (5개 호출) ⭐ 신규 발견
├── batchSchedulerService.ts          # 배치 스케줄러 (4개 호출) ⭐ 신규 발견
├── dataService.ts                    # 데이터 서비스 (4개 호출) ⭐ 신규 발견
├── adminService.ts                   # 관리자 (3개 호출)
└── referenceCacheService.ts          # 캐시 (3개 호출)

🟢 Low Priority (컨트롤러 & 라우트) - 188개 호출

backend-node/src/controllers/
├── adminController.ts                # 관리자 컨트롤러 (28개 호출) ⭐ 신규 발견
├── webTypeStandardController.ts      # 웹타입 표준 (11개 호출) ⭐ 신규 발견
├── fileController.ts                 # 파일 컨트롤러 (11개 호출) ⭐ 신규 발견
├── buttonActionStandardController.ts # 버튼 액션 표준 (11개 호출) ⭐ 신규 발견
├── entityReferenceController.ts      # 엔티티 참조 (4개 호출) ⭐ 신규 발견
├── dataflowExecutionController.ts    # 데이터플로우 실행 (3개 호출) ⭐ 신규 발견
└── screenFileController.ts           # 화면 파일 (2개 호출) ⭐ 신규 발견

backend-node/src/routes/
├── ddlRoutes.ts                      # DDL 라우트 (2개 호출) ⭐ 신규 발견
└── companyManagementRoutes.ts        # 회사 관리 라우트 (2개 호출) ⭐ 신규 발견

backend-node/src/config/
└── database.ts                       # 데이터베이스 설정 (4개 호출)

#### 🗑️ **삭제 예정 Scripts - 60개 호출** ⚠️ 사용하지 않음

backend-node/scripts/ (삭제 예정) ├── install-dataflow-indexes.js # 인덱스 설치 (10개 호출) 🗑️ 삭제 ├── add-missing-columns.js # 컬럼 추가 (8개 호출) 🗑️ 삭제 ├── test-template-creation.js # 템플릿 테스트 (6개 호출) 🗑️ 삭제 ├── create-component-table.js # 컴포넌트 테이블 생성 (5개 호출) 🗑️ 삭제 ├── seed-ui-components.js # UI 컴포넌트 시드 (3개 호출) 🗑️ 삭제 ├── seed-templates.js # 템플릿 시드 (3개 호출) 🗑️ 삭제 ├── init-layout-standards.js # 레이아웃 표준 초기화 (3개 호출) 🗑️ 삭제 ├── add-data-mapping-column.js # 데이터 매핑 컬럼 추가 (3개 호출) 🗑️ 삭제 ├── add-button-webtype.js # 버튼 웹타입 추가 (3개 호출) 🗑️ 삭제 └── list-components.js # 컴포넌트 목록 (2개 호출) 🗑️ 삭제

backend-node/ (루트) └── clean-screen-tables.js # 화면 테이블 정리 (7개 호출) 🗑️ 삭제


**⚠️ 삭제 계획**: 이 스크립트들은 개발/배포 도구로 운영 시스템에서 사용하지 않으므로 마이그레이션 전에 삭제 예정

### 2. **복잡도별 분류**

#### 🔥 **매우 복잡 (트랜잭션 + 동적 쿼리) - 최우선 처리**

- `screenManagementService.ts` (46개) - 화면 정의 관리, JSON 처리
- `tableManagementService.ts` (35개) - 테이블 메타데이터 관리, DDL 실행
- `dataflowService.ts` (31개) - 복잡한 관계 관리, 트랜잭션 처리 ⭐ 신규 발견
- `dynamicFormService.ts` (15개) - UPSERT 및 동적 테이블 처리
- `externalDbConnectionService.ts` (15개) - 외부 DB 연결 관리
- `dataflowControlService.ts` (6개) - 복잡한 제어 로직
- `enhancedDataflowControlService.ts` (0개) - 다중 연결 제어 (Raw Query만 사용)
- `multiConnectionQueryService.ts` (4개) - 외부 DB 연결

#### 🟠 **복잡 (Raw Query 혼재) - 2순위**

- `multilangService.ts` (25개) - 재귀 쿼리, 다국어 처리
- `batchService.ts` (16개) - 배치 작업 관리
- `componentStandardService.ts` (16개) - 컴포넌트 표준 관리
- `commonCodeService.ts` (15개) - 코드 관리, 계층 구조
- `dataflowDiagramService.ts` (12개) - 다이어그램 관리 ⭐ 신규 발견
- `collectionService.ts` (11개) - 컬렉션 관리
- `layoutService.ts` (10개) - 레이아웃 관리
- `dbTypeCategoryService.ts` (10개) - DB 타입 분류 ⭐ 신규 발견
- `templateStandardService.ts` (9개) - 템플릿 표준
- `eventTriggerService.ts` (6개) - JSON 검색 쿼리

#### 🟡 **중간 (단순 CRUD) - 3순위**

- `ddlAuditLogger.ts` (8개) - DDL 감사 로그 ⭐ 신규 발견
- `externalCallConfigService.ts` (8개) - 외부 호출 설정 ⭐ 신규 발견
- `batchExternalDbService.ts` (8개) - 배치 외부DB ⭐ 신규 발견
- `batchExecutionLogService.ts` (7개) - 배치 실행 로그 ⭐ 신규 발견
- `enhancedDynamicFormService.ts` (6개) - 확장 동적 폼 ⭐ 신규 발견
- `ddlExecutionService.ts` (6개) - DDL 실행
- `entityJoinService.ts` (5개) - 엔티티 조인 ⭐ 신규 발견
- `dataMappingService.ts` (5개) - 데이터 매핑 ⭐ 신규 발견
- `batchManagementService.ts` (5개) - 배치 관리 ⭐ 신규 발견
- `authService.ts` (5개) - 사용자 인증
- `batchSchedulerService.ts` (4개) - 배치 스케줄러 ⭐ 신규 발견
- `dataService.ts` (4개) - 데이터 서비스 ⭐ 신규 발견
- `adminService.ts` (3개) - 관리자 메뉴
- `referenceCacheService.ts` (3개) - 캐시 관리

#### 🟢 **단순 (컨트롤러 레이어) - 4순위**

- `adminController.ts` (28개) - 관리자 컨트롤러 ⭐ 신규 발견
- `webTypeStandardController.ts` (11개) - 웹타입 표준 ⭐ 신규 발견
- `fileController.ts` (11개) - 파일 컨트롤러 ⭐ 신규 발견
- `buttonActionStandardController.ts` (11개) - 버튼 액션 표준 ⭐ 신규 발견
- `entityReferenceController.ts` (4개) - 엔티티 참조 ⭐ 신규 발견
- `database.ts` (4개) - 데이터베이스 설정
- `dataflowExecutionController.ts` (3개) - 데이터플로우 실행 ⭐ 신규 발견
- `screenFileController.ts` (2개) - 화면 파일 ⭐ 신규 발견
- `ddlRoutes.ts` (2개) - DDL 라우트 ⭐ 신규 발견
- `companyManagementRoutes.ts` (2개) - 회사 관리 라우트 ⭐ 신규 발견

#### 🗑️ **삭제 예정 Scripts (마이그레이션 대상 아님)**

- `install-dataflow-indexes.js` (10개) - 인덱스 설치 스크립트 🗑️
- `add-missing-columns.js` (8개) - 컬럼 추가 스크립트 🗑️
- `clean-screen-tables.js` (7개) - 테이블 정리 스크립트 🗑️
- `test-template-creation.js` (6개) - 템플릿 테스트 스크립트 🗑️
- `create-component-table.js` (5개) - 컴포넌트 테이블 생성 🗑️
- 기타 시드 스크립트들 (14개) - 개발용 데이터 시드 🗑️

**⚠️ 중요**: 이 스크립트들은 사용하지 않으므로 마이그레이션 전에 삭제하여 작업량을 60개 호출만큼 줄일 수 있습니다.

---

## 🏗️ 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. 동적 쿼리 빌더

// 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. 타입 안전성 보장

// 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: 핵심 서비스 전환 (3주) - 최우선

2.1 화면 관리 서비스 전환 (우선순위 1) - 46개 호출

// 기존 Prisma 코드 (복잡한 JSON 처리)
const screenData = await prisma.screen_definitions.findMany({
  where: {
    company_code: companyCode,
    screen_config: { path: ["type"], equals: "form" },
  },
  include: { screen_components: true },
});

// 새로운 Raw Query 코드
const { query, params } = QueryBuilder.select("screen_definitions", {
  columns: ["*", "screen_config::jsonb"],
  where: {
    company_code: companyCode,
    "screen_config->>'type'": "form",
  },
  joins: [
    {
      type: "LEFT",
      table: "screen_components",
      on: "screen_definitions.id = screen_components.screen_id",
    },
  ],
});
const screenData = await DatabaseManager.query(query, params);

2.2 테이블 관리 서비스 전환 (우선순위 2) - 35개 호출

  • 동적 테이블 생성/삭제 로직 전환
  • 메타데이터 관리 시스템 개선
  • DDL 실행 트랜잭션 처리
  • 컬럼 타입 변환 로직 최적화

2.3 데이터플로우 서비스 전환 (우선순위 3) - 31개 호출 신규 발견

  • 복잡한 관계 관리 로직 전환
  • 트랜잭션 기반 데이터 이동 처리
  • JSON 기반 설정 관리 개선
  • 다중 테이블 조인 최적화

2.4 동적 폼 서비스 전환 (우선순위 4) - 15개 호출

  • UPSERT 로직 Raw Query로 전환
  • 동적 테이블 처리 로직 개선
  • 트랜잭션 처리 최적화

2.5 외부 DB 연결 서비스 전환 (우선순위 5) - 15개 호출

  • 다중 DB 연결 관리 로직
  • 연결 풀 관리 시스템
  • 외부 DB 스키마 동기화

Phase 3: 관리 기능 전환 (2.5주)

3.1 다국어 서비스 전환 - 25개 호출

  • 재귀 쿼리 (WITH RECURSIVE) 전환
  • 번역 데이터 관리 최적화
  • 다국어 캐시 시스템 구현

3.2 배치 관련 서비스 전환 - 40개 호출 대규모 신규 발견

  • batchService.ts (16개) - 배치 작업 관리
  • batchExternalDbService.ts (8개) - 배치 외부DB
  • batchExecutionLogService.ts (7개) - 배치 실행 로그
  • batchManagementService.ts (5개) - 배치 관리
  • batchSchedulerService.ts (4개) - 배치 스케줄러

3.3 표준 관리 서비스 전환 - 41개 호출

  • componentStandardService.ts (16개) - 컴포넌트 표준 관리
  • commonCodeService.ts (15개) - 코드 관리, 계층 구조
  • layoutService.ts (10개) - 레이아웃 관리

3.4 데이터플로우 관련 서비스 - 18개 호출 신규 발견

  • dataflowDiagramService.ts (12개) - 다이어그램 관리
  • dataflowControlService.ts (6개) - 복잡한 제어 로직

3.5 기타 중요 서비스 - 38개 호출 신규 발견

  • collectionService.ts (11개) - 컬렉션 관리
  • dbTypeCategoryService.ts (10개) - DB 타입 분류
  • templateStandardService.ts (9개) - 템플릿 표준
  • ddlAuditLogger.ts (8개) - DDL 감사 로그

Phase 4: 확장 기능 전환 (2.5주) 대폭 확장

4.1 외부 연동 서비스 - 51개 호출 신규 발견

  • externalCallConfigService.ts (8개) - 외부 호출 설정
  • eventTriggerService.ts (6개) - JSON 검색 쿼리
  • enhancedDynamicFormService.ts (6개) - 확장 동적 폼
  • ddlExecutionService.ts (6개) - DDL 실행
  • entityJoinService.ts (5개) - 엔티티 조인
  • dataMappingService.ts (5개) - 데이터 매핑
  • authService.ts (5개) - 사용자 인증
  • multiConnectionQueryService.ts (4개) - 외부 DB 연결
  • dataService.ts (4개) - 데이터 서비스
  • adminService.ts (3개) - 관리자 메뉴
  • referenceCacheService.ts (3개) - 캐시 관리

4.2 컨트롤러 레이어 전환 - 72개 호출 대규모 신규 발견

  • adminController.ts (28개) - 관리자 컨트롤러
  • webTypeStandardController.ts (11개) - 웹타입 표준
  • fileController.ts (11개) - 파일 컨트롤러
  • buttonActionStandardController.ts (11개) - 버튼 액션 표준
  • entityReferenceController.ts (4개) - 엔티티 참조
  • dataflowExecutionController.ts (3개) - 데이터플로우 실행
  • screenFileController.ts (2개) - 화면 파일
  • ddlRoutes.ts (2개) - DDL 라우트

4.3 설정 및 기반 구조 - 6개 호출

  • database.ts (4개) - 데이터베이스 설정
  • companyManagementRoutes.ts (2개) - 회사 관리 라우트

Phase 5: 사용하지 않는 Scripts 삭제 (0.5주) 🗑️

5.1 불필요한 스크립트 파일 삭제 - 60개 호출 제거

  • backend-node/scripts/ 전체 폴더 삭제 (53개 호출)
  • backend-node/clean-screen-tables.js 삭제 (7개 호출)
  • 관련 package.json 스크립트 정리
  • 문서에서 스크립트 참조 제거

효과: 60개 Prisma 호출을 마이그레이션 없이 제거하여 작업량 대폭 감소

Phase 6: Prisma 완전 제거 (0.5주)

6.1 Prisma 의존성 제거

  • package.json에서 Prisma 제거
  • schema.prisma 파일 삭제
  • 관련 설정 파일 정리

6.2 최종 검증 및 최적화

  • 전체 기능 테스트
  • 성능 최적화
  • 문서화 업데이트

🔄 마이그레이션 전략

1. 점진적 전환 방식

단계별 전환

// 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. 호환성 레이어

// 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. 테스트 전략

병렬 테스트

// 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 처리 차이

대응 방안

// 엄격한 트랜잭션 관리
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. 성능 저하 위험

위험 요소

  • 연결 풀 관리 미흡
  • 쿼리 최적화 부족
  • 캐싱 메커니즘 부재

대응 방안

// 연결 풀 최적화
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. 개발 생산성 저하

위험 요소

  • 타입 안전성 부족
  • 디버깅 어려움
  • 코드 복잡성 증가

대응 방안

// 개발자 친화적 인터페이스
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. 연결 풀 최적화

// 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. 쿼리 최적화

// 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. 캐싱 전략

// 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. 단위 테스트

// 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. 통합 테스트

// 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. 성능 테스트

// 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 클래스 구현 (backend-node/src/database/db.ts)
  • QueryBuilder 유틸리티 구현 (backend-node/src/utils/queryBuilder.ts)
  • 타입 정의 및 검증 로직 (backend-node/src/types/database.ts)
  • 연결 풀 설정 및 최적화 (pg Pool 사용)
  • 트랜잭션 관리 시스템 (transaction 함수 구현)
  • 에러 핸들링 메커니즘 (try-catch 및 rollback 처리)
  • 로깅 및 모니터링 도구 (쿼리 로그 포함)
  • 단위 테스트 작성 (backend-node/src/tests/)
  • 테스트 성공 확인 (multiConnectionQueryService, externalCallConfigService)

Phase 2: 핵심 서비스 (3주) - 107개 호출

  • ScreenManagementService 전환 (46개) - 최우선
  • TableManagementService 전환 (35개) - 최우선
  • DataflowService 전환 (31개) 신규 발견
  • DynamicFormService 전환 (15개) - UPSERT 포함
  • ExternalDbConnectionService 전환 (15개)
  • DataflowControlService 전환 (6개) - 복잡한 로직
  • DDLExecutionService 전환 (6개)
  • AuthService 전환 (5개)
  • MultiConnectionQueryService 전환 (4개)
  • 통합 테스트 실행

Phase 3: 관리 기능 (2.5주) - 162개 호출

  • MultiLangService 전환 (25개) - 재귀 쿼리
  • 배치 관련 서비스 전환 (40개) 대규모 신규 발견
    • BatchService (16개), BatchExternalDbService (8개)
    • BatchExecutionLogService (7개), BatchManagementService (5개)
    • BatchSchedulerService (4개)
  • 표준 관리 서비스 전환 (41개)
    • ComponentStandardService (16개), CommonCodeService (15개)
    • LayoutService (10개)
  • 데이터플로우 관련 서비스 (18개) 신규 발견
    • DataflowDiagramService (12개), DataflowControlService (6개)
  • 기타 중요 서비스 (38개) 신규 발견
    • CollectionService (11개), DbTypeCategoryService (10개)
    • TemplateStandardService (9개), DDLAuditLogger (8개)
  • 기능별 테스트 완료

Phase 4: 확장 기능 (2.5주) - 129개 호출 대폭 확장

  • 외부 연동 서비스 전환 (51개) 신규 발견
    • ExternalCallConfigService (8개), EventTriggerService (6개)
    • EnhancedDynamicFormService (6개), EntityJoinService (5개)
    • DataMappingService (5개), DataService (4개)
    • AdminService (3개), ReferenceCacheService (3개)
  • 컨트롤러 레이어 전환 (72개) 대규모 신규 발견
    • AdminController (28개), WebTypeStandardController (11개)
    • FileController (11개), ButtonActionStandardController (11개)
    • EntityReferenceController (4개), DataflowExecutionController (3개)
    • ScreenFileController (2개), DDLRoutes (2개)
  • 설정 및 기반 구조 (6개)
    • Database.ts (4개), CompanyManagementRoutes (2개)
  • 전체 기능 테스트

Phase 5: Scripts 삭제 (0.5주) - 60개 호출 제거 🗑️

  • 불필요한 스크립트 파일 삭제 (60개) 🗑️ 마이그레이션 불필요
    • backend-node/scripts/ 전체 폴더 삭제 (53개)
    • backend-node/clean-screen-tables.js 삭제 (7개)
    • package.json 스크립트 정리
  • 문서에서 스크립트 참조 제거

Phase 6: 완전 제거 (0.5주)

  • Prisma 의존성 제거
  • schema.prisma 삭제
  • 관련 설정 파일 정리
  • 문서 업데이트
  • 최종 성능 테스트
  • 배포 준비

🎯 성공 기준

기능적 요구사항

  • 모든 기존 기능이 동일하게 작동
  • 동적 테이블 생성/관리 완벽 지원
  • 트랜잭션 일관성 보장
  • 에러 처리 및 복구 메커니즘

성능 요구사항

  • 기존 대비 성능 저하 없음 (±10% 이내)
  • 메모리 사용량 최적화
  • 연결 풀 효율성 개선
  • 쿼리 실행 시간 단축

품질 요구사항

  • 코드 커버리지 90% 이상
  • 모든 테스트 케이스 통과
  • 타입 안전성 보장
  • 보안 검증 완료

📚 참고 자료

기술 문서

내부 문서


⚠️ 주의사항

  1. 데이터 백업: 마이그레이션 전 전체 데이터베이스 백업 필수
  2. 점진적 전환: 한 번에 모든 것을 바꾸지 말고 단계별로 진행
  3. 철저한 테스트: 각 단계마다 충분한 테스트 수행
  4. 롤백 계획: 문제 발생 시 즉시 롤백할 수 있는 계획 수립
  5. 모니터링: 전환 후 성능 및 안정성 지속 모니터링


📈 업데이트된 마이그레이션 규모

🔍 최종 Prisma 사용 현황 (Scripts 삭제 후)

  • 기존 계획: 42개 파일, 386개 호출
  • Scripts 포함: 52개 파일, 490개 호출 (+104개 호출 발견)
  • Scripts 삭제 후: 42개 파일, 444개 호출 (+58개 호출 실제 증가)

주요 신규 발견 서비스들

  1. dataflowService.ts (31개) - 데이터플로우 관리 핵심 서비스
  2. 배치 관련 서비스들 (40개) - 5개 서비스로 분산된 대규모 배치 시스템
  3. dataflowDiagramService.ts (12개) - 다이어그램 관리
  4. dbTypeCategoryService.ts (10개) - DB 타입 분류 시스템
  5. 컨트롤러 레이어 (72개) - 7개 컨트롤러에서 대규모 Prisma 사용
  6. 감사 및 로깅 서비스들 (15개) - DDL 감사, 배치 실행 로그
  7. 확장 기능들 (26개) - 엔티티 조인, 데이터 매핑, 외부 호출 설정
  8. 🗑️ Scripts 삭제 (60개) - 사용하지 않는 개발/배포 스크립트 (마이그레이션 불필요)

📊 우선순위 재조정

🔴 최우선 (Phase 2) - 107개 호출

  • 화면관리 (46개), 테이블관리 (35개), 데이터플로우 (31개)

🟡 고우선순위 (Phase 3) - 162개 호출

  • 다국어 (25개), 배치 시스템 (40개), 표준 관리 (41개)

🟢 중간우선순위 (Phase 4) - 129개 호출

  • 외부 연동 (51개), 컨트롤러 레이어 (72개), 기타 (6개)

🗑️ Scripts 삭제 (Phase 5) - 60개 호출 🗑️ 마이그레이션 불필요

  • 사용하지 않는 개발/배포 스크립트 (60개) - 삭제로 작업량 감소

🎯 최종 마이그레이션 계획

총 예상 기간: 8주 ⬆️ (+2주 연장, Scripts 삭제로 1주 단축) 핵심 개발자: 3-4명 ⬆️ (+1명 추가) 실제 마이그레이션 대상: 444개 호출 (Scripts 60개 제외) 위험도: 중간-높음 ⬇️ (Scripts 삭제로 위험도 일부 감소)

⚠️ 주요 위험 요소

  1. 배치 시스템 복잡성: 5개 서비스 40개 호출의 복잡한 의존성
  2. 컨트롤러 레이어 규모: 72개 호출의 대규모 API 전환
  3. 데이터플로우 시스템: 신규 발견된 핵심 서비스 (31개 호출)
  4. 트랜잭션 복잡성: 다중 서비스 간 데이터 일관성 보장
  5. Scripts 삭제: 60개 호출 제거로 작업량 대폭 감소

🚀 성공을 위한 핵심 전략

  1. 단계별 점진적 전환: 절대 한 번에 모든 것을 바꾸지 않기
  2. 철저한 테스트: 각 Phase마다 완전한 기능 테스트
  3. 롤백 계획: 각 단계별 즉시 롤백 가능한 계획 수립
  4. 모니터링 강화: 전환 후 성능 및 안정성 지속 모니터링
  5. 팀 확대: 복잡성 증가로 인한 개발팀 확대 필요

완전한 분석을 통해 Prisma를 완전히 제거하고 진정한 동적 데이터베이스 시스템을 구축할 수 있습니다! 🚀

중요: 이제 모든 Prisma 사용 부분이 파악되었으므로, 누락 없는 완전한 마이그레이션이 가능합니다.