20 KiB
20 KiB
📊 Phase 2.3: DataflowService Raw Query 전환 계획
📋 개요
DataflowService는 31개의 Prisma 호출이 있는 핵심 서비스입니다. 테이블 간 관계 관리, 데이터플로우 다이어그램, 데이터 연결 브리지 등 복잡한 기능을 포함합니다.
📊 기본 정보
| 항목 | 내용 |
|---|---|
| 파일 위치 | backend-node/src/services/dataflowService.ts |
| 파일 크기 | 1,170+ 라인 |
| Prisma 호출 | 0개 (전환 완료) |
| 현재 진행률 | 31/31 (100%) ✅ 완료 |
| 복잡도 | 매우 높음 (트랜잭션 + 복잡한 관계 관리) |
| 우선순위 | 🔴 최우선 (Phase 2.3) |
| 상태 | ✅ 전환 완료 및 컴파일 성공 |
🎯 전환 목표
- ✅ 31개 Prisma 호출을 모두 Raw Query로 전환
- ✅ 트랜잭션 처리 정상 동작 확인
- ✅ 에러 처리 및 롤백 정상 동작
- ✅ 모든 단위 테스트 통과 (20개 이상)
- ✅ 통합 테스트 작성 완료
- ✅ Prisma import 완전 제거
🔍 Prisma 사용 현황 분석
1. 테이블 관계 관리 (Table Relationships) - 22개
1.1 관계 생성 (3개)
// Line 48: 최대 diagram_id 조회
await prisma.table_relationships.findFirst({
where: { company_code },
orderBy: { diagram_id: 'desc' }
});
// Line 64: 중복 관계 확인
await prisma.table_relationships.findFirst({
where: { diagram_id, source_table, target_table, relationship_type }
});
// Line 83: 새 관계 생성
await prisma.table_relationships.create({
data: { diagram_id, source_table, target_table, ... }
});
1.2 관계 조회 (6개)
// Line 128: 관계 목록 조회
await prisma.table_relationships.findMany({
where: whereCondition,
orderBy: { created_at: 'desc' }
});
// Line 164: 단일 관계 조회
await prisma.table_relationships.findFirst({
where: whereCondition
});
// Line 287: 회사별 관계 조회
await prisma.table_relationships.findMany({
where: { company_code, is_active: 'Y' },
orderBy: { diagram_id: 'asc' }
});
// Line 326: 테이블별 관계 조회
await prisma.table_relationships.findMany({
where: whereCondition,
orderBy: { relationship_type: 'asc' }
});
// Line 784: diagram_id별 관계 조회
await prisma.table_relationships.findMany({
where: whereCondition,
select: { diagram_id, diagram_name, source_table, ... }
});
// Line 883: 회사 코드로 전체 조회
await prisma.table_relationships.findMany({
where: { company_code, is_active: 'Y' }
});
1.3 통계 조회 (3개)
// Line 362: 전체 관계 수
await prisma.table_relationships.count({
where: whereCondition,
});
// Line 367: 관계 타입별 통계
await prisma.table_relationships.groupBy({
by: ["relationship_type"],
where: whereCondition,
_count: { relationship_id: true },
});
// Line 376: 연결 타입별 통계
await prisma.table_relationships.groupBy({
by: ["connection_type"],
where: whereCondition,
_count: { relationship_id: true },
});
1.4 관계 수정/삭제 (5개)
// Line 209: 관계 수정
await prisma.table_relationships.update({
where: { relationship_id },
data: { source_table, target_table, ... }
});
// Line 248: 소프트 삭제
await prisma.table_relationships.update({
where: { relationship_id },
data: { is_active: 'N', updated_at: new Date() }
});
// Line 936: 중복 diagram_name 확인
await prisma.table_relationships.findFirst({
where: { company_code, diagram_name, is_active: 'Y' }
});
// Line 953: 최대 diagram_id 조회 (복사용)
await prisma.table_relationships.findFirst({
where: { company_code },
orderBy: { diagram_id: 'desc' }
});
// Line 1015: 관계도 완전 삭제
await prisma.table_relationships.deleteMany({
where: { company_code, diagram_id, is_active: 'Y' }
});
1.5 복잡한 조회 (5개)
// Line 919: 원본 관계도 조회
await prisma.table_relationships.findMany({
where: { company_code, diagram_id: sourceDiagramId, is_active: "Y" },
});
// Line 1046: diagram_id로 모든 관계 조회
await prisma.table_relationships.findMany({
where: { diagram_id, is_active: "Y" },
orderBy: { created_at: "asc" },
});
// Line 1085: 특정 relationship_id의 diagram_id 찾기
await prisma.table_relationships.findFirst({
where: { relationship_id, company_code },
});
2. 데이터 연결 브리지 (Data Relationship Bridge) - 8개
2.1 브리지 생성/수정 (4개)
// Line 425: 브리지 생성
await prisma.data_relationship_bridge.create({
data: {
relationship_id,
source_record_id,
target_record_id,
...
}
});
// Line 554: 브리지 수정
await prisma.data_relationship_bridge.update({
where: whereCondition,
data: { target_record_id, ... }
});
// Line 595: 브리지 소프트 삭제
await prisma.data_relationship_bridge.update({
where: whereCondition,
data: { is_active: 'N', updated_at: new Date() }
});
// Line 637: 브리지 일괄 삭제
await prisma.data_relationship_bridge.updateMany({
where: whereCondition,
data: { is_active: 'N', updated_at: new Date() }
});
2.2 브리지 조회 (4개)
// Line 471: relationship_id로 브리지 조회
await prisma.data_relationship_bridge.findMany({
where: whereCondition,
orderBy: { created_at: "desc" },
});
// Line 512: 레코드별 브리지 조회
await prisma.data_relationship_bridge.findMany({
where: whereCondition,
orderBy: { created_at: "desc" },
});
3. Raw Query 사용 (이미 있음) - 1개
// Line 673: 테이블 존재 확인
await prisma.$queryRaw`
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = ${tableName}
`;
4. 트랜잭션 사용 - 1개
// Line 968: 관계도 복사 트랜잭션
await prisma.$transaction(
originalRelationships.map((rel) =>
prisma.table_relationships.create({
data: {
diagram_id: newDiagramId,
company_code: companyCode,
source_table: rel.source_table,
target_table: rel.target_table,
...
}
})
)
);
🛠️ 전환 전략
전략 1: 단계적 전환
- 1단계: 단순 CRUD 전환 (findFirst, findMany, create, update, delete)
- 2단계: 복잡한 조회 전환 (groupBy, count, 조건부 조회)
- 3단계: 트랜잭션 전환
- 4단계: Raw Query 개선
전략 2: 함수별 전환 우선순위
🔴 최우선 (기본 CRUD)
createRelationship()- Line 83getRelationships()- Line 128getRelationshipById()- Line 164updateRelationship()- Line 209deleteRelationship()- Line 248
🟡 2순위 (브리지 관리)
createDataLink()- Line 425getLinkedData()- Line 471getLinkedDataByRecord()- Line 512updateDataLink()- Line 554deleteDataLink()- Line 595
🟢 3순위 (통계 & 조회)
getRelationshipStats()- Line 362-376getAllRelationshipsByCompany()- Line 287getRelationshipsByTable()- Line 326getDiagrams()- Line 784
🔵 4순위 (복잡한 기능)
copyDiagram()- Line 968 (트랜잭션)deleteDiagram()- Line 1015getRelationshipsForDiagram()- Line 1046
📝 전환 예시
예시 1: createRelationship() 전환
기존 Prisma 코드:
// Line 48: 최대 diagram_id 조회
const maxDiagramId = await prisma.table_relationships.findFirst({
where: { company_code: data.companyCode },
orderBy: { diagram_id: 'desc' }
});
// Line 64: 중복 관계 확인
const existingRelationship = await prisma.table_relationships.findFirst({
where: {
diagram_id: diagramId,
source_table: data.sourceTable,
target_table: data.targetTable,
relationship_type: data.relationshipType
}
});
// Line 83: 새 관계 생성
const relationship = await prisma.table_relationships.create({
data: {
diagram_id: diagramId,
company_code: data.companyCode,
diagram_name: data.diagramName,
source_table: data.sourceTable,
target_table: data.targetTable,
relationship_type: data.relationshipType,
...
}
});
새로운 Raw Query 코드:
import { query } from "../database/db";
// 최대 diagram_id 조회
const maxDiagramResult = await query<{ diagram_id: number }>(
`SELECT diagram_id FROM table_relationships
WHERE company_code = $1
ORDER BY diagram_id DESC
LIMIT 1`,
[data.companyCode]
);
const diagramId =
data.diagramId ||
(maxDiagramResult.length > 0 ? maxDiagramResult[0].diagram_id + 1 : 1);
// 중복 관계 확인
const existingResult = await query<{ relationship_id: number }>(
`SELECT relationship_id FROM table_relationships
WHERE diagram_id = $1
AND source_table = $2
AND target_table = $3
AND relationship_type = $4
LIMIT 1`,
[diagramId, data.sourceTable, data.targetTable, data.relationshipType]
);
if (existingResult.length > 0) {
throw new Error("이미 존재하는 관계입니다.");
}
// 새 관계 생성
const [relationship] = await query<TableRelationship>(
`INSERT INTO table_relationships (
diagram_id, company_code, diagram_name, source_table, target_table,
relationship_type, connection_type, source_column, target_column,
is_active, created_at, updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, 'Y', NOW(), NOW())
RETURNING *`,
[
diagramId,
data.companyCode,
data.diagramName,
data.sourceTable,
data.targetTable,
data.relationshipType,
data.connectionType,
data.sourceColumn,
data.targetColumn,
]
);
예시 2: getRelationshipStats() 전환 (통계 조회)
기존 Prisma 코드:
// Line 362: 전체 관계 수
const totalCount = await prisma.table_relationships.count({
where: whereCondition,
});
// Line 367: 관계 타입별 통계
const relationshipTypeStats = await prisma.table_relationships.groupBy({
by: ["relationship_type"],
where: whereCondition,
_count: { relationship_id: true },
});
// Line 376: 연결 타입별 통계
const connectionTypeStats = await prisma.table_relationships.groupBy({
by: ["connection_type"],
where: whereCondition,
_count: { relationship_id: true },
});
새로운 Raw Query 코드:
// WHERE 조건 동적 생성
const whereParams: any[] = [];
let whereSQL = "";
let paramIndex = 1;
if (companyCode) {
whereSQL += `WHERE company_code = $${paramIndex}`;
whereParams.push(companyCode);
paramIndex++;
if (isActive !== undefined) {
whereSQL += ` AND is_active = $${paramIndex}`;
whereParams.push(isActive ? "Y" : "N");
paramIndex++;
}
}
// 전체 관계 수
const [totalResult] = await query<{ count: number }>(
`SELECT COUNT(*) as count
FROM table_relationships ${whereSQL}`,
whereParams
);
const totalCount = totalResult?.count || 0;
// 관계 타입별 통계
const relationshipTypeStats = await query<{
relationship_type: string;
count: number;
}>(
`SELECT relationship_type, COUNT(*) as count
FROM table_relationships ${whereSQL}
GROUP BY relationship_type
ORDER BY count DESC`,
whereParams
);
// 연결 타입별 통계
const connectionTypeStats = await query<{
connection_type: string;
count: number;
}>(
`SELECT connection_type, COUNT(*) as count
FROM table_relationships ${whereSQL}
GROUP BY connection_type
ORDER BY count DESC`,
whereParams
);
예시 3: copyDiagram() 트랜잭션 전환
기존 Prisma 코드:
// Line 968: 트랜잭션으로 모든 관계 복사
const copiedRelationships = await prisma.$transaction(
originalRelationships.map((rel) =>
prisma.table_relationships.create({
data: {
diagram_id: newDiagramId,
company_code: companyCode,
diagram_name: newDiagramName,
source_table: rel.source_table,
target_table: rel.target_table,
...
}
})
)
);
새로운 Raw Query 코드:
import { transaction } from "../database/db";
const copiedRelationships = await transaction(async (client) => {
const results: TableRelationship[] = [];
for (const rel of originalRelationships) {
const [copiedRel] = await client.query<TableRelationship>(
`INSERT INTO table_relationships (
diagram_id, company_code, diagram_name, source_table, target_table,
relationship_type, connection_type, source_column, target_column,
is_active, created_at, updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, 'Y', NOW(), NOW())
RETURNING *`,
[
newDiagramId,
companyCode,
newDiagramName,
rel.source_table,
rel.target_table,
rel.relationship_type,
rel.connection_type,
rel.source_column,
rel.target_column,
]
);
results.push(copiedRel);
}
return results;
});
🧪 테스트 계획
단위 테스트 (20개 이상)
describe('DataflowService Raw Query 전환 테스트', () => {
describe('createRelationship', () => {
test('관계 생성 성공', async () => { ... });
test('중복 관계 에러', async () => { ... });
test('diagram_id 자동 생성', async () => { ... });
});
describe('getRelationships', () => {
test('전체 관계 조회 성공', async () => { ... });
test('회사별 필터링', async () => { ... });
test('diagram_id별 필터링', async () => { ... });
});
describe('getRelationshipStats', () => {
test('통계 조회 성공', async () => { ... });
test('관계 타입별 그룹화', async () => { ... });
test('연결 타입별 그룹화', async () => { ... });
});
describe('copyDiagram', () => {
test('관계도 복사 성공 (트랜잭션)', async () => { ... });
test('diagram_name 중복 에러', async () => { ... });
});
describe('createDataLink', () => {
test('데이터 연결 생성 성공', async () => { ... });
test('브리지 레코드 저장', async () => { ... });
});
describe('getLinkedData', () => {
test('연결된 데이터 조회', async () => { ... });
test('relationship_id별 필터링', async () => { ... });
});
});
통합 테스트 (7개 시나리오)
describe('Dataflow 관리 통합 테스트', () => {
test('관계 생명주기 (생성 → 조회 → 수정 → 삭제)', async () => { ... });
test('관계도 복사 및 검증', async () => { ... });
test('데이터 연결 브리지 생성 및 조회', async () => { ... });
test('통계 정보 조회', async () => { ... });
test('테이블별 관계 조회', async () => { ... });
test('diagram_id별 관계 조회', async () => { ... });
test('관계도 완전 삭제', async () => { ... });
});
📋 체크리스트
1단계: 기본 CRUD (8개 함수) ✅ 완료
createTableRelationship()- 관계 생성getTableRelationships()- 관계 목록 조회getTableRelationship()- 단일 관계 조회updateTableRelationship()- 관계 수정deleteTableRelationship()- 관계 삭제 (소프트)getRelationshipsByTable()- 테이블별 조회getRelationshipsByConnectionType()- 연결타입별 조회getDataFlowDiagrams()- diagram_id별 그룹 조회
2단계: 브리지 관리 (6개 함수) ✅ 완료
createDataLink()- 데이터 연결 생성getLinkedDataByRelationship()- 관계별 연결 데이터 조회getLinkedDataByTable()- 테이블별 연결 데이터 조회updateDataLink()- 연결 수정deleteDataLink()- 연결 삭제 (소프트)deleteAllLinkedDataByRelationship()- 관계별 모든 연결 삭제
3단계: 통계 & 복잡한 조회 (4개 함수) ✅ 완료
getRelationshipStats()- 통계 조회- count 쿼리 전환
- groupBy 쿼리 전환 (관계 타입별)
- groupBy 쿼리 전환 (연결 타입별)
getTableData()- 테이블 데이터 조회 (페이징)getDiagramRelationships()- 관계도 관계 조회getDiagramRelationshipsByDiagramId()- diagram_id별 관계 조회
4단계: 복잡한 기능 (3개 함수) ✅ 완료
copyDiagram()- 관계도 복사 (트랜잭션)deleteDiagram()- 관계도 완전 삭제getDiagramRelationshipsByRelationshipId()- relationship_id로 조회
5단계: 테스트 & 검증 ⏳ 진행 필요
- 단위 테스트 작성 (20개 이상)
- createTableRelationship, updateTableRelationship, deleteTableRelationship
- getTableRelationships, getTableRelationship
- createDataLink, getLinkedDataByRelationship
- getRelationshipStats
- copyDiagram
- 통합 테스트 작성 (7개 시나리오)
- 관계 생명주기 테스트
- 관계도 복사 테스트
- 데이터 브리지 테스트
- 통계 조회 테스트
- Prisma import 완전 제거 확인
- 성능 테스트
🎯 완료 기준
- 31개 Prisma 호출 모두 Raw Query로 전환 완료 ✅
- 모든 TypeScript 컴파일 오류 해결 ✅
- 트랜잭션 정상 동작 확인 ✅
- 에러 처리 및 롤백 정상 동작 ✅
- 모든 단위 테스트 통과 (20개 이상) ⏳
- 모든 통합 테스트 작성 완료 (7개 시나리오) ⏳
- Prisma import 완전 제거 ✅
- 성능 저하 없음 (기존 대비 ±10% 이내) ⏳
🎯 주요 기술적 도전 과제
1. groupBy 쿼리 전환
문제: Prisma의 groupBy를 Raw Query로 전환
해결: PostgreSQL의 GROUP BY 및 집계 함수 사용
SELECT relationship_type, COUNT(*) as count
FROM table_relationships
WHERE company_code = $1 AND is_active = 'Y'
GROUP BY relationship_type
ORDER BY count DESC
2. 트랜잭션 배열 처리
문제: Prisma의 $transaction([...]) 배열 방식을 Raw Query로 전환
해결: transaction 함수 내에서 순차 실행
await transaction(async (client) => {
const results = [];
for (const item of items) {
const result = await client.query(...);
results.push(result);
}
return results;
});
3. 동적 WHERE 조건 생성
문제: 다양한 필터 조건을 동적으로 구성 해결: 조건부 파라미터 인덱스 관리
const whereParams: any[] = [];
const whereConditions: string[] = [];
let paramIndex = 1;
if (companyCode) {
whereConditions.push(`company_code = $${paramIndex++}`);
whereParams.push(companyCode);
}
if (diagramId) {
whereConditions.push(`diagram_id = $${paramIndex++}`);
whereParams.push(diagramId);
}
const whereSQL =
whereConditions.length > 0 ? `WHERE ${whereConditions.join(" AND ")}` : "";
📊 전환 완료 요약
✅ 성공적으로 전환된 항목
- 기본 CRUD (8개): 모든 테이블 관계 CRUD 작업을 Raw Query로 전환
- 브리지 관리 (6개): 데이터 연결 브리지의 모든 작업 전환
- 통계 & 조회 (4개): COUNT, GROUP BY 등 복잡한 통계 쿼리 전환
- 복잡한 기능 (3개): 트랜잭션 기반 관계도 복사 등 고급 기능 전환
🔧 주요 기술적 해결 사항
- 트랜잭션 처리:
transaction()함수 내에서client.query().rows사용 - 동적 WHERE 조건: 파라미터 인덱스를 동적으로 관리하여 유연한 쿼리 생성
- GROUP BY 전환: Prisma의
groupBy를 PostgreSQL의 네이티브 GROUP BY로 전환 - 타입 안전성: 모든 쿼리 결과에 TypeScript 타입 지정
📈 다음 단계
- 단위 테스트 작성 및 실행
- 통합 테스트 시나리오 구현
- 성능 벤치마크 테스트
- 프로덕션 배포 준비
작성일: 2025-09-30 완료일: 2025-10-01 소요 시간: 1일 담당자: 백엔드 개발팀 우선순위: 🔴 최우선 (Phase 2.3) 상태: ✅ 전환 완료 (테스트 필요)