Files
vexplor/backend-node/PHASE1_USAGE_GUIDE.md
kjs ed78ef184d 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>
2025-09-30 15:29:20 +09:00

9.2 KiB

Phase 1: Raw Query 기반 구조 사용 가이드

📋 개요

Phase 1에서 구현한 Raw Query 기반 데이터베이스 아키텍처 사용 방법입니다.


🏗️ 구현된 모듈

1. DatabaseManager (src/database/db.ts)

PostgreSQL 연결 풀 기반 핵심 모듈

주요 함수:

  • query<T>(sql, params) - 기본 쿼리 실행
  • queryOne<T>(sql, params) - 단일 행 조회
  • transaction(callback) - 트랜잭션 실행
  • getPool() - 연결 풀 가져오기
  • getPoolStatus() - 연결 풀 상태 확인

2. QueryBuilder (src/utils/queryBuilder.ts)

동적 쿼리 생성 유틸리티

주요 메서드:

  • QueryBuilder.select(tableName, options) - SELECT 쿼리
  • QueryBuilder.insert(tableName, data, options) - INSERT 쿼리
  • QueryBuilder.update(tableName, data, where, options) - UPDATE 쿼리
  • QueryBuilder.delete(tableName, where, options) - DELETE 쿼리
  • QueryBuilder.count(tableName, where) - COUNT 쿼리
  • QueryBuilder.exists(tableName, where) - EXISTS 쿼리

3. DatabaseValidator (src/utils/databaseValidator.ts)

SQL Injection 방지 및 입력 검증

주요 메서드:

  • validateTableName(tableName) - 테이블명 검증
  • validateColumnName(columnName) - 컬럼명 검증
  • validateWhereClause(where) - WHERE 조건 검증
  • sanitizeInput(input) - 입력 값 Sanitize

4. 타입 정의 (src/types/database.ts)

TypeScript 타입 안전성 보장


🚀 사용 예제

1. 기본 쿼리 실행

import { query, queryOne } from '../database/db';

// 여러 행 조회
const users = await query<User>(
  'SELECT * FROM users WHERE status = $1',
  ['active']
);

// 단일 행 조회
const user = await queryOne<User>(
  'SELECT * FROM users WHERE user_id = $1',
  ['user123']
);

if (!user) {
  throw new Error('사용자를 찾을 수 없습니다.');
}

2. QueryBuilder 사용

SELECT

import { query } from '../database/db';
import { QueryBuilder } from '../utils/queryBuilder';

// 기본 SELECT
const { query: sql, params } = QueryBuilder.select('users', {
  where: { status: 'active' },
  orderBy: 'created_at DESC',
  limit: 10,
});

const users = await query(sql, params);

// 복잡한 SELECT (JOIN, WHERE, ORDER BY)
const { query: sql2, params: params2 } = QueryBuilder.select('users', {
  columns: ['users.user_id', 'users.username', 'departments.dept_name'],
  joins: [
    {
      type: 'LEFT',
      table: 'departments',
      on: 'users.dept_id = departments.dept_id',
    },
  ],
  where: { 'users.status': 'active' },
  orderBy: ['users.created_at DESC', 'users.username ASC'],
  limit: 20,
  offset: 0,
});

const result = await query(sql2, params2);

INSERT

import { query } from '../database/db';
import { QueryBuilder } from '../utils/queryBuilder';

// 기본 INSERT
const { query: sql, params } = QueryBuilder.insert(
  'users',
  {
    user_id: 'new_user',
    username: 'John Doe',
    email: 'john@example.com',
    status: 'active',
  },
  {
    returning: ['id', 'user_id'],
  }
);

const [newUser] = await query(sql, params);
console.log('생성된 사용자 ID:', newUser.id);

// UPSERT (INSERT ... ON CONFLICT)
const { query: sql2, params: params2 } = QueryBuilder.insert(
  'users',
  {
    user_id: 'user123',
    username: 'Jane',
    email: 'jane@example.com',
  },
  {
    onConflict: {
      columns: ['user_id'],
      action: 'DO UPDATE',
      updateSet: ['username', 'email'],
    },
    returning: ['*'],
  }
);

const [upsertedUser] = await query(sql2, params2);

UPDATE

import { query } from '../database/db';
import { QueryBuilder } from '../utils/queryBuilder';

const { query: sql, params } = QueryBuilder.update(
  'users',
  {
    username: 'Updated Name',
    email: 'updated@example.com',
    updated_at: new Date(),
  },
  {
    user_id: 'user123',
  },
  {
    returning: ['*'],
  }
);

const [updatedUser] = await query(sql, params);

DELETE

import { query } from '../database/db';
import { QueryBuilder } from '../utils/queryBuilder';

const { query: sql, params } = QueryBuilder.delete(
  'users',
  {
    user_id: 'user_to_delete',
  },
  {
    returning: ['user_id', 'username'],
  }
);

const [deletedUser] = await query(sql, params);
console.log('삭제된 사용자:', deletedUser.username);

3. 트랜잭션 사용

import { transaction } from '../database/db';

// 복잡한 트랜잭션 처리
const result = await transaction(async (client) => {
  // 1. 사용자 생성
  const userResult = await client.query(
    'INSERT INTO users (user_id, username, email) VALUES ($1, $2, $3) RETURNING id',
    ['new_user', 'John', 'john@example.com']
  );

  const userId = userResult.rows[0].id;

  // 2. 역할 할당
  await client.query(
    'INSERT INTO user_roles (user_id, role_id) VALUES ($1, $2)',
    [userId, 'admin']
  );

  // 3. 로그 생성
  await client.query(
    'INSERT INTO audit_logs (action, user_id, details) VALUES ($1, $2, $3)',
    ['USER_CREATED', userId, JSON.stringify({ username: 'John' })]
  );

  return { success: true, userId };
});

console.log('트랜잭션 완료:', result);

4. JSON 필드 쿼리 (JSONB)

import { query } from '../database/db';
import { QueryBuilder } from '../utils/queryBuilder';

// JSON 필드 쿼리 (config->>'type' = 'form')
const { query: sql, params } = QueryBuilder.select('screen_management', {
  columns: ['*'],
  where: {
    company_code: 'COMPANY_001',
    "config->>'type'": 'form',
  },
});

const screens = await query(sql, params);

5. 동적 테이블 쿼리

import { query } from '../database/db';
import { DatabaseValidator } from '../utils/databaseValidator';

async function queryDynamicTable(tableName: string, filters: Record<string, any>) {
  // 테이블명 검증 (SQL Injection 방지)
  if (!DatabaseValidator.validateTableName(tableName)) {
    throw new Error('유효하지 않은 테이블명입니다.');
  }

  // WHERE 조건 검증
  if (!DatabaseValidator.validateWhereClause(filters)) {
    throw new Error('유효하지 않은 WHERE 조건입니다.');
  }

  const { query: sql, params } = QueryBuilder.select(tableName, {
    where: filters,
  });

  return await query(sql, params);
}

// 사용 예
const data = await queryDynamicTable('company_data_001', {
  status: 'active',
  region: 'Seoul',
});

🔐 보안 고려사항

1. 항상 Parameterized Query 사용

// ❌ 위험: SQL Injection 취약
const userId = req.params.userId;
const sql = `SELECT * FROM users WHERE user_id = '${userId}'`;
const users = await query(sql);

// ✅ 안전: Parameterized Query
const userId = req.params.userId;
const users = await query('SELECT * FROM users WHERE user_id = $1', [userId]);

2. 식별자 검증

import { DatabaseValidator } from '../utils/databaseValidator';

// 테이블명/컬럼명 검증
if (!DatabaseValidator.validateTableName(tableName)) {
  throw new Error('유효하지 않은 테이블명입니다.');
}

if (!DatabaseValidator.validateColumnName(columnName)) {
  throw new Error('유효하지 않은 컬럼명입니다.');
}

3. 입력 값 Sanitize

import { DatabaseValidator } from '../utils/databaseValidator';

const sanitizedData = DatabaseValidator.sanitizeInput(userInput);

📊 성능 최적화 팁

1. 연결 풀 모니터링

import { getPoolStatus } from '../database/db';

const status = getPoolStatus();
console.log('연결 풀 상태:', {
  total: status.totalCount,
  idle: status.idleCount,
  waiting: status.waitingCount,
});

2. 배치 INSERT

import { transaction } from '../database/db';

// 대량 데이터 삽입 시 트랜잭션 사용
await transaction(async (client) => {
  for (const item of largeDataset) {
    await client.query('INSERT INTO items (name, value) VALUES ($1, $2)', [
      item.name,
      item.value,
    ]);
  }
});

3. 인덱스 활용 쿼리

// WHERE 절에 인덱스 컬럼 사용
const { query: sql, params } = QueryBuilder.select('users', {
  where: {
    user_id: 'user123', // 인덱스 컬럼
  },
});

🧪 테스트 실행

# 테스트 실행
npm test -- database.test.ts

# 특정 테스트만 실행
npm test -- database.test.ts -t "QueryBuilder"

🚨 에러 핸들링

import { query } from '../database/db';

try {
  const users = await query('SELECT * FROM users WHERE status = $1', ['active']);
  return users;
} catch (error: any) {
  console.error('쿼리 실행 실패:', error.message);

  // PostgreSQL 에러 코드 확인
  if (error.code === '23505') {
    throw new Error('중복된 값이 존재합니다.');
  }

  if (error.code === '23503') {
    throw new Error('외래 키 제약 조건 위반입니다.');
  }

  throw error;
}

📝 다음 단계 (Phase 2)

Phase 1 기반 구조가 완성되었으므로, Phase 2에서는:

  1. screenManagementService.ts 전환 (46개 호출)
  2. tableManagementService.ts 전환 (35개 호출)
  3. dataflowService.ts 전환 (31개 호출)

등 핵심 서비스를 Raw Query로 전환합니다.


작성일: 2025-09-30 버전: 1.0.0 담당: Backend Development Team