diff --git a/PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md b/PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md new file mode 100644 index 00000000..dfa30a01 --- /dev/null +++ b/PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md @@ -0,0 +1,969 @@ +# ๐Ÿš€ 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 { + const client = await this.pool.connect(); + try { + const result = await client.query(text, params); + return result.rows; + } finally { + client.release(); + } + } + + // ํŠธ๋žœ์žญ์…˜ ์‹คํ–‰ + static async transaction( + callback: (client: PoolClient) => Promise + ): Promise { + 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; + 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, + 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, + where: Record + ) { + 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) { + 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 { + 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): 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( + operations: ((client: PoolClient) => Promise)[] + ): Promise { + 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(); + 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 }) => + PrismaCompatibilityLayer.findUnique("user_info", options), + + findMany: (options?: any) => + PrismaCompatibilityLayer.findMany("user_info", options), + + create: (options: { data: Record }) => + 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 { + 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 { + 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๋ฅผ ์™„์ „ํžˆ ์ œ๊ฑฐํ•˜๊ณ  ์ง„์ •ํ•œ ๋™์  ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค! ๐Ÿš€