9.2 KiB
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에서는:
- screenManagementService.ts 전환 (46개 호출)
- tableManagementService.ts 전환 (35개 호출)
- dataflowService.ts 전환 (31개 호출)
등 핵심 서비스를 Raw Query로 전환합니다.
작성일: 2025-09-30 버전: 1.0.0 담당: Backend Development Team