ERP-node/PHASE2.3_DATAFLOW_SERVICE_M...

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. 1단계: 단순 CRUD 전환 (findFirst, findMany, create, update, delete)
  2. 2단계: 복잡한 조회 전환 (groupBy, count, 조건부 조회)
  3. 3단계: 트랜잭션 전환
  4. 4단계: Raw Query 개선

전략 2: 함수별 전환 우선순위

🔴 최우선 (기본 CRUD)

  • createRelationship() - Line 83
  • getRelationships() - Line 128
  • getRelationshipById() - Line 164
  • updateRelationship() - Line 209
  • deleteRelationship() - Line 248

🟡 2순위 (브리지 관리)

  • createDataLink() - Line 425
  • getLinkedData() - Line 471
  • getLinkedDataByRecord() - Line 512
  • updateDataLink() - Line 554
  • deleteDataLink() - Line 595

🟢 3순위 (통계 & 조회)

  • getRelationshipStats() - Line 362-376
  • getAllRelationshipsByCompany() - Line 287
  • getRelationshipsByTable() - Line 326
  • getDiagrams() - Line 784

🔵 4순위 (복잡한 기능)

  • copyDiagram() - Line 968 (트랜잭션)
  • deleteDiagram() - Line 1015
  • getRelationshipsForDiagram() - 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 ")}` : "";

📊 전환 완료 요약

성공적으로 전환된 항목

  1. 기본 CRUD (8개): 모든 테이블 관계 CRUD 작업을 Raw Query로 전환
  2. 브리지 관리 (6개): 데이터 연결 브리지의 모든 작업 전환
  3. 통계 & 조회 (4개): COUNT, GROUP BY 등 복잡한 통계 쿼리 전환
  4. 복잡한 기능 (3개): 트랜잭션 기반 관계도 복사 등 고급 기능 전환

🔧 주요 기술적 해결 사항

  1. 트랜잭션 처리: transaction() 함수 내에서 client.query().rows 사용
  2. 동적 WHERE 조건: 파라미터 인덱스를 동적으로 관리하여 유연한 쿼리 생성
  3. GROUP BY 전환: Prisma의 groupBy를 PostgreSQL의 네이티브 GROUP BY로 전환
  4. 타입 안전성: 모든 쿼리 결과에 TypeScript 타입 지정

📈 다음 단계

  • 단위 테스트 작성 및 실행
  • 통합 테스트 시나리오 구현
  • 성능 벤치마크 테스트
  • 프로덕션 배포 준비

작성일: 2025-09-30 완료일: 2025-10-01 소요 시간: 1일 담당자: 백엔드 개발팀 우선순위: 🔴 최우선 (Phase 2.3) 상태: 전환 완료 (테스트 필요)