418 lines
9.2 KiB
Markdown
418 lines
9.2 KiB
Markdown
# 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. 기본 쿼리 실행
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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. 트랜잭션 사용
|
|
|
|
```typescript
|
|
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)
|
|
|
|
```typescript
|
|
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. 동적 테이블 쿼리
|
|
|
|
```typescript
|
|
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 사용**
|
|
|
|
```typescript
|
|
// ❌ 위험: 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. **식별자 검증**
|
|
|
|
```typescript
|
|
import { DatabaseValidator } from '../utils/databaseValidator';
|
|
|
|
// 테이블명/컬럼명 검증
|
|
if (!DatabaseValidator.validateTableName(tableName)) {
|
|
throw new Error('유효하지 않은 테이블명입니다.');
|
|
}
|
|
|
|
if (!DatabaseValidator.validateColumnName(columnName)) {
|
|
throw new Error('유효하지 않은 컬럼명입니다.');
|
|
}
|
|
```
|
|
|
|
### 3. **입력 값 Sanitize**
|
|
|
|
```typescript
|
|
import { DatabaseValidator } from '../utils/databaseValidator';
|
|
|
|
const sanitizedData = DatabaseValidator.sanitizeInput(userInput);
|
|
```
|
|
|
|
---
|
|
|
|
## 📊 성능 최적화 팁
|
|
|
|
### 1. **연결 풀 모니터링**
|
|
|
|
```typescript
|
|
import { getPoolStatus } from '../database/db';
|
|
|
|
const status = getPoolStatus();
|
|
console.log('연결 풀 상태:', {
|
|
total: status.totalCount,
|
|
idle: status.idleCount,
|
|
waiting: status.waitingCount,
|
|
});
|
|
```
|
|
|
|
### 2. **배치 INSERT**
|
|
|
|
```typescript
|
|
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. **인덱스 활용 쿼리**
|
|
|
|
```typescript
|
|
// WHERE 절에 인덱스 컬럼 사용
|
|
const { query: sql, params } = QueryBuilder.select('users', {
|
|
where: {
|
|
user_id: 'user123', // 인덱스 컬럼
|
|
},
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## 🧪 테스트 실행
|
|
|
|
```bash
|
|
# 테스트 실행
|
|
npm test -- database.test.ts
|
|
|
|
# 특정 테스트만 실행
|
|
npm test -- database.test.ts -t "QueryBuilder"
|
|
```
|
|
|
|
---
|
|
|
|
## 🚨 에러 핸들링
|
|
|
|
```typescript
|
|
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 |