DocumentLibrary 데이터베이스 전수 감사 보고서
감사 대상: hohyun94/DocumentLibrary
감사 기준: MySQL/MariaDB와 PostgreSQL의 선택적 지원, 빈 데이터베이스에서의 초기화, 배포 플랫폼 배포 기동 안정성
판정: 수정 완료 및 양 방언 실제 엔진 검증 통과
범위와 방법
이번 감사는 배포 로그에 나타난 relation "organization_departments" does not exist 오류를 시작점으로 삼았다. 단일 누락 테이블만 보완하는 대신, 서버의 연결 생성, 방언 판별, 직접 SQL, 트랜잭션, 결과 행 변환, Drizzle 스키마와 마이그레이션 체인, 최초 설치 API, 설치 화면, 배포 플랫폼 시작 명령, SSL 연결 모드까지 추적했다.
| 점검 영역 | 확인 범위 | 결과 |
|---|---|---|
| 연결 및 드라이버 | pg, mysql2, URL 프로토콜, DATABASE_DIALECT | 양 방언 선택 지원으로 정비 |
| 마이그레이션 | PostgreSQL 1개, MySQL 11개 파일, 저널 및 메타테이블 | 빈 DB 및 재실행 검증 통과 |
| 런타임 SQL | 6개 직접 SQL 모듈과 부트스트랩 쿼리 | 방언 의존 구문을 명시 분기 |
| 초기 설치 | /api/setup/status, validate, install, 관리자 프로비저닝 | PostgreSQL·MySQL 선택 지원 |
| 배포 | 배포 설정, 시작 명령, health check | 마이그레이션 선행 후 서버 기동 |
| 데이터 정합성 | UPSERT, 블롭 참조 수, 갱신 시각, 집계 API | PostgreSQL과 MySQL 동작 일치 보강 |
주요 발견 사항과 조치
| 심각도 | 발견 사항 | 영향 | 조치 |
|---|---|---|---|
| 차단 | 배포 플랫폼가 빌드 후 pnpm start만 실행했다. PostgreSQL 마이그레이션이 실행되지 않아 organization_departments가 없었다. | 서버 시작 직후 종료 | start:with-migrations를 추가하고 배포 플랫폼 시작 명령을 변경했다. |
| 높음 | 런타임 어댑터가 PostgreSQL만 허용해 기존 MySQL 코드를 실제로 사용할 수 없었다. | MySQL/MariaDB 배포 불가 | mysql2 기반 공통 풀·트랜잭션 어댑터와 URL 기반 방언 선택을 구현했다. |
| 높음 | 설치 화면은 MySQL/MariaDB를 안내했지만 설치 API와 부트스트랩은 PostgreSQL 전용이었다. | UI와 서버 계약 불일치 | 설치 화면, validate, install, 마이그레이션 경로를 양 방언 기준으로 통일했다. |
| 높음 | ON CONFLICT, EXCLUDED, JSONB, GROUP_CONCAT가 공통 경로에 혼재했다. | 선택한 DB에 따라 설치 또는 API가 실패 | UPSERT·문자열 집계·버전 조회를 방언별 SQL로 분기했다. |
| 높음 | MySQL 마이그레이션에 빈 마지막 breakpoint와 64자를 초과하는 생성 제약 이름이 있었다. | MariaDB 초기화 실패 | 원본 해시는 유지한 채 실행 시 빈 구문을 제거하고 제약 이름을 64자로 안전하게 축약했다. |
| 중간 | MySQL에는 portal_installation_config 스키마와 마이그레이션이 없었다. | 최초 설치 설정 저장 실패 | MySQL 스키마와 0010_add_portal_installation_config.sql을 추가했다. |
| 중간 | PostgreSQL의 단순 ? 치환기는 SQL 문자열·주석 내부의 물음표까지 $n으로 바꿀 수 있었다. | 일부 쿼리 손상 위험 | 문자열, 식별자, 주석, dollar-quoted body를 인지하는 변환기로 교체하고 회귀 테스트를 추가했다. |
| 중간 | PostgreSQL에서 sslmode=require 경고가 발생할 수 있었다. | 차기 pg 버전에서 TLS 의미 변경 위험 | 기존 안전 동작을 유지하도록 verify-full을 명시한다. |
| 중간 | MySQL 자동 updated_at 갱신과 달리 PostgreSQL 블롭 갱신에는 갱신 시각 기록이 없었다. | 파일 대장의 최신성 정렬 불일치 | 블롭 참조 수 변경 시 updated_at = CURRENT_TIMESTAMP를 명시했다. |
구현 결과
방언 선택 및 연결
DATABASE_URL은 postgresql:// 또는 mysql:// 형식만 허용한다. DATABASE_DIALECT는 postgresql 또는 mysql이며, 설정된 경우 URL 프로토콜과 반드시 일치해야 한다. 일치하지 않으면 서버 또는 마이그레이션은 즉시 실패하므로 잘못된 드라이버로 연결되는 묵시적 장애를 방지한다.
| 구성 | PostgreSQL | MySQL/MariaDB |
|---|---|---|
| 런타임 드라이버 | pg | mysql2 |
| 마이그레이션 폴더 | drizzle/postgres-migrations | drizzle/legacy/mysql/migrations |
| 설치 화면 선택값 | postgresql | mysql |
| 상태 저장 UPSERT | ON CONFLICT ... EXCLUDED | ON DUPLICATE KEY UPDATE |
| 파일 대장 문자열 집계 | STRING_AGG | GROUP_CONCAT |
배포 경로
pnpm start:with-migrations는 pnpm db:migrate를 먼저 실행하고 성공한 경우에만 pnpm start를 실행한다. 따라서 스키마가 없는 새 데이터베이스에서는 서버가 테이블을 참조하기 전에 해당 방언의 전체 마이그레이션이 적용된다. 배포 설정은 PostgreSQL을 기본 배포 방언으로 명시하고 이 시작 명령을 사용한다.
이번 배포 장애의 직접 원인은 데이터베이스 연결 자체가 아니라 테이블 생성 전 서버가 기동된 순서 문제였다. 수정 후에는 마이그레이션 실패가 서버 기동 실패로 명확히 노출되며, 마이그레이션이 성공한 경우에만 readiness endpoint가 제공된다.
MySQL 레거시 체인 호환성
기존 MySQL SQL 파일의 원문과 SHA-256 해시는 보존한다. 실행기는 Drizzle 메타데이터 규약에 맞춰 원본 해시와 저널 시각을 기록하되, 실행 직전에만 빈 breakpoint 구간을 제외하고 MariaDB 제한을 넘는 제약 이름을 축약한다. 따라서 기존 설치의 이력을 임의로 재작성하지 않으면서, 새 MySQL/MariaDB 설치가 실제로 통과하도록 한다.
실제 검증 결과
검증에는 PostgreSQL 16.15 및 MariaDB 10.11.14의 빈 로컬 데이터베이스를 사용했다. 각 방언에서 최초 마이그레이션, 반복 마이그레이션, 실제 배포 시작 명령, readiness endpoint 및 파일 대장 API를 실행했다.
| 검증 항목 | PostgreSQL | MySQL/MariaDB | 결과 |
|---|---|---|---|
| TypeScript 정적 검사 | 공통 | 공통 | 통과 |
| 기존 클라이언트 테스트 | 공통 | 공통 | 44개 통과, 3개 기존 skip |
| 새 서버 어댑터 테스트 | 공통 | 공통 | 5개 통과 |
| 프로덕션 번들 생성 | 공통 | 공통 | 통과 (dist/index.js, dist/run-migrations.js) |
| 빈 DB 최초 마이그레이션 | 14개 애플리케이션 테이블 | 14개 애플리케이션 테이블 | 통과 |
| 반복 마이그레이션 | 1개 마이그레이션 레코드 유지 | 11개 마이그레이션 레코드 유지 | 통과 |
pnpm start:with-migrations | readiness ready: true | readiness ready: true | 통과 |
/api/archive/file-ledger | [] 응답 및 STRING_AGG 실행 | [] 응답 및 GROUP_CONCAT 실행 | 통과 |
운영 주의 사항
개발 서버는 데이터 보존이 필요 없다는 현재 전제에 따라, 배포가 계속 실패할 경우 개발용 데이터베이스를 비운 뒤 최신 커밋으로 재배포해도 된다. 구체적인 선택 방언, 환경 변수, 재초기화 절차는 이중 데이터베이스 배포 가이드에 정리했다.
| 항목 | 현재 처리 | 주의 사항 |
|---|---|---|
배포 플랫폼 doclib 환경 전달 | DATABASE_URL을 그대로 수신 | URL이 PostgreSQL URL인지, DATABASE_DIALECT=postgresql과 일치하는지 확인해야 한다. |
| MySQL과 PostgreSQL 사이의 데이터 이전 | 범위 외 | 두 DB를 모두 지원하지만 자동 데이터 변환 도구는 제공하지 않는다. 현재 개발 서버는 데이터 재생성이 허용된다. |
| 다중 인스턴스 동시 배포 | 단일 인스턴스 기준 검증 | 다중 replica 운영으로 바꿀 경우 별도 migration job 또는 배포 잠금이 필요하다. |
| 운영 DB 초기화 | 금지 | 운영 환경은 백업과 롤백 계획이 있는 별도 변경 절차를 사용해야 한다. |
변경 파일 요약
| 구분 | 주요 파일 |
|---|---|
| 드라이버 및 방언 추상화 | server/db-adapter.ts, server/db.ts |
| 마이그레이션 | server/database-migrations.ts, server/run-migrations.ts, drizzle.config.ts |
| MySQL 스키마 보완 | drizzle/legacy/mysql/schema.ts, drizzle/legacy/mysql/migrations/0010_add_portal_installation_config.sql |
| 설치 API 및 UI | server/setupBootstrap.ts, server/portalIntegrationConfig.ts, client/src/pages/Setup.tsx |
| 런타임 쿼리 호환성 | server/archive-api-db.ts |
| 배포와 문서 | package.json, 배포 설정, .env.example, docs/DATABASE_DIALECT_DEPLOYMENT.md |
결론적으로, DocumentLibrary는 이제 PostgreSQL과 MySQL/MariaDB를 명시적으로 선택해 설치·마이그레이션·기동할 수 있으며, 최초 장애를 유발한 PostgreSQL 스키마 미적용 경로는 배포 시작 단계에서 차단된다.