Skip to content

DocumentLibrary PostgreSQL 완전 전환 계획 ​

작성자: Manus AI 작성일: 2026-08-26 대상: hohyun94/DocumentLibrary

1. 전환 목표와 현재 상태 ​

DocumentLibrary는 현재 mysql2/promise pool, Drizzle mysqlTable, MySQL 전용 migration, ON DUPLICATE KEY UPDATE, beginTransaction()·PoolConnection에 의존한다. 따라서 DATABASE_URL만 PostgreSQL 주소로 바꾸는 방식은 지원하지 않는다. 목표는 애플리케이션 코드·스키마·migration·데이터·배포 설정을 모두 PostgreSQL 기준으로 전환하되, 검증 기간에는 기존 MySQL 운영을 보존하고 명확한 롤백 지점을 유지하는 것이다.

현재 schema의 핵심 테이블은 users, portal_state, organization_departments, managed_users, user_groups, user_group_departments, user_group_memberships, archive_posts, archive_files, archive_file_blobs, archive_file_versions, archive_post_files, archive_file_deletion_logs다. 실제 repository와 server/db.ts, server/archive-db.ts, server/setupBootstrap.ts, server/portalIntegrationConfig.ts가 직접 SQL을 실행하므로 Drizzle schema만 바꾸어서는 전환이 완료되지 않는다.

원칙: MySQL과 PostgreSQL을 동시에 지원해야 하는 기간에는 provider·schema·migration·query helper를 명시적으로 분리한다. 운영 DB에서 방언을 자동 추측하거나 한 dialect의 query builder를 다른 dialect에 재사용하지 않는다.

2. 권장 전환 전략 ​

권장 방식은 expand → backfill → dual-read 검증 → 짧은 write freeze → cutover → observe → decommission 순서의 별도 PostgreSQL 전환이다. 기존 MySQL을 직접 변환하거나 애플리케이션에서 장기간 dual-write를 유지하는 방식은 두 데이터베이스의 일관성 문제가 커지므로 피한다.

단계운영 DB애플리케이션목적
개발 전환MySQL fixture + PostgreSQL fixtureprovider별 테스트변환 계약 확인
스테이징MySQL 원본 복제본과 PostgreSQL 이관본PostgreSQL candidate실제 데이터 검증
Cutover 직전MySQL primarywrite freeze 또는 maintenance최종 delta 추출
Cutover 이후PostgreSQL primaryPostgreSQL only관찰·안정화
롤백보존한 MySQL snapshot이전 MySQL deployment장애 시 즉시 복귀

3. 단계별 실행 계획 ​

Phase 0 — 사전 조건과 기준선 고정 ​

먼저 현재 main의 애플리케이션 버전, MySQL engine/version, timezone, character set/collation, row count, database size, 최대 파일·게시물·조직 데이터, 최근 30일 read/write 오류율을 기록한다. 운영 백업은 논리 덤프와 provider snapshot을 모두 생성하고, 복원 테스트가 실제로 성공하는지 확인한다.

다음 환경변수를 분리한다.

변수PostgreSQL 용도
DATABASE_DIALECT=postgresqladapter·schema·migration 선택을 명시
DATABASE_URL=postgresql://...PostgreSQL primary 연결
DATABASE_SSL_MODE=verify-fullRailway 등 외부 PostgreSQL TLS 정책
DATABASE_POOL_MAXpool 상한
DATABASE_STATEMENT_TIMEOUT_MS쿼리 runaway 방지
DATABASE_MIGRATION_LOCK_KEYmigration 단일 실행 보장

.env와 실제 비밀값은 Git에 저장하지 않는다. 각 배포 환경에서 MySQL과 PostgreSQL connection string을 별도 secret으로 관리한다.

Phase 1 — Provider와 adapter 분리 ​

server/db.ts를 다음 구조로 나눈다.

text
server/db/
  index.ts              # DATABASE_DIALECT 선택, 공통 API export
  types.ts              # DbClient, DbTransaction, DbDialect
  mysql.ts              # mysql2 pool, PoolConnection adapter
  postgres.ts           # pg Pool, PoolClient adapter
  result.ts             # rows, rowCount, insert id 정규화
  transaction.ts        # begin/commit/rollback 공통 wrapper

공통 API는 databaseConfigured, configureDatabase, withTransaction, readPortalState, writePortalState, readDirectory, writeDirectory, archive read/write로 구성하되, SQL 문법은 provider 파일 안에 둔다. PostgreSQL의 pg.Pool·PoolClient를 MySQL 타입과 섞지 않는다.

PostgreSQL 연결은 pg의 Pool을 사용하고 애플리케이션 종료 시 pool.end()를 호출한다. 연결 검증은 SELECT 1 AS connected로 통일한다. transaction wrapper는 예외 발생 시 rollback을 시도하고 원래 예외를 보존한다. pool acquire timeout, idle timeout, statement timeout과 TLS 설정을 운영값으로 명시한다.

Phase 2 — PostgreSQL schema 작성 ​

drizzle/schema.pg.ts를 새로 작성하고 기존 schema.ts는 명시적인 provider re-export로 바꾼다. MySQL schema를 그대로 타입 캐스팅하지 않는다.

MySQL 표현PostgreSQL 표현변환 규칙
mysqlTablepgTable테이블 정의를 별도 파일로 작성
int().autoincrement()integer().generatedAlwaysAsIdentity()기존 ID가 있는 경우 sequence를 데이터 최대값 이후로 재설정
tinyint/int boolean flagboolean0/1을 false/true로 변환
jsonjsonb유효 JSON만 적재, GIN index는 사용 패턴 확인 후 추가
timestamp().onUpdateNow()timestamp().defaultNow() + 명시적 updatePostgreSQL에는 MySQL의 ON UPDATE가 없으므로 update code가 updatedAt을 설정
backtick identifierdouble-quote identifier 또는 lowercase namecamelCase 컬럼은 Drizzle column mapping으로 명시
ON DUPLICATE KEY UPDATEON CONFLICT (...) DO UPDATE반드시 conflict target과 unique constraint를 명시
? placeholder$1, $2, ...pg query builder/Drizzle parameterization 사용
insertIdINSERT ... RETURNING id생성 후 재조회로 추정하지 않음
affectedRowsrowCount공통 result helper에서 정규화

managed_users.is_admin, is_active, must_change_password, archive_files.is_deleted, archive_file_versions.is_current는 애플리케이션 계약을 먼저 확인한 뒤 PostgreSQL boolean으로 바꾸는 것이 바람직하다. 기존 외부 API가 숫자 0/1을 요구하면 DB는 boolean으로 보관하고 boundary에서만 변환한다.

portal_state.state와 portal_installation_config.settings는 jsonb NOT NULL로 정의한다. JSON object인지, 필수 키가 존재하는지는 Zod 또는 서버 validation에서 검증한다. JSON 내부 구조를 query하지 않는다면 GIN index는 추가하지 않는다.

Phase 3 — Migration 체계 구축 ​

기존 MySQL migration 폴더와 분리해 drizzle/postgres-migrations를 사용한다.

text
drizzle/
  schema.mysql.ts
  schema.pg.ts
  schema.ts
  migrations/                 # MySQL 전용 보존
  postgres-migrations/        # PostgreSQL 전용
  drizzle.config.mysql.ts
  drizzle.config.postgres.ts

초기 PostgreSQL baseline은 빈 DB에서 한 번에 재현 가능해야 한다. 이후 변경은 PostgreSQL migration만으로 추가한다. migration runner는 DATABASE_DIALECT를 확인해 맞는 디렉터리만 실행하고, 다른 dialect의 SQL을 실행하면 즉시 실패해야 한다.

Migration 순서는 다음과 같다.

  1. 독립 테이블(users, portal_state, user_groups, archive_posts, archive_files, archive_file_blobs)을 생성한다.
  2. 자기 참조가 있는 organization_departments를 생성하고 parent foreign key를 추가한다.
  3. managed_users를 생성한 뒤 조직 foreign key를 추가한다.
  4. junction table과 archive foreign key를 생성한다.
  5. unique/index를 생성한다.
  6. portal installation/config tables를 생성한다.
  7. migration ledger와 health-check table을 생성한다.

모든 migration은 idempotent 실행 여부를 명확히 한다. 운영에서는 IF NOT EXISTS를 무분별하게 사용하지 말고 migration ledger와 checksum으로 이미 적용된 변경을 식별한다.

Phase 4 — 데이터 이관 ​

MySQL 원본에서 테이블별로 추출하고 PostgreSQL staging에 적재한다. 파일 바이너리는 DB가 아니라 기존 S3/object storage를 유지하며, DB에는 storage_key, checksum, size, MIME type과 참조관계만 이관한다.

이관 순서는 foreign key 의존성 순서를 따른다.

text
users
portal_state
organization_departments
managed_users
user_groups
archive_posts
archive_files
archive_file_blobs
archive_file_versions
user_group_departments
user_group_memberships
archive_post_files
archive_file_deletion_logs
portal_installation_config

각 테이블에 대해 다음 검증을 수행한다.

검증기준
행 수source와 target이 테이블별로 일치
primary keynull·중복·누락 없음
unique keydept_code, hr_user_key, knox_id, group name, blob sha256 충돌 없음
foreign keyorphan 0건
JSON모든 portal_state·settings가 유효한 JSON object
시간timezone 정책을 적용하고 UTC 저장·표시 계약 확인
boolean0/1 외 값 없음, target boolean 값과 의미 일치
archivefile/version/blob/post link 수와 checksum 일치
sequenceidentity sequence가 각 numeric PK 최대값보다 큼

이관 스크립트는 재실행 가능하도록 checkpoint와 source primary key 기반 upsert를 사용한다. 실패한 batch만 재시도할 수 있어야 하며, 원문 값과 변환 후 값의 hash를 비교하는 audit manifest를 생성한다.

Phase 5 — 애플리케이션 query 전환 ​

server/db.ts, server/archive-db.ts, server/portalIntegrationConfig.ts, server/setupBootstrap.ts의 raw SQL을 provider별로 분리한다. PostgreSQL 경로에서는 $1 placeholder, jsonb, RETURNING, ON CONFLICT, pg result shape를 사용한다.

핵심 수정 대상은 다음과 같다.

파일/영역필요한 전환
server/db.tspool·transaction·portal state·directory read/write provider 분리
server/archive-db.tsarchive transaction과 모든 insert/query를 PostgreSQL adapter로 전환
server/portalIntegrationConfig.tssettings jsonb 저장과 PostgreSQL upsert
server/setupBootstrap.tsSHOW TABLES, MySQL DDL, ON DUPLICATE KEY 제거; provider별 bootstrap
repository 전반insertId를 returning()으로 변경
result handling[rows, fields]와 { rows, rowCount }를 공통 API로 변환
boolean boundaryDB boolean과 기존 API 숫자 flag의 변환 위치를 고정
timestampsupdatedAt을 모든 update set에서 명시

PostgreSQL에서는 identifier alias를 camelCase로 반환할 때 double quote 또는 Drizzle mapping을 사용한다. SELECT dept_code AS deptCode처럼 unquoted alias를 사용하면 PostgreSQL이 소문자로 접어 deptcode가 될 수 있으므로 금지한다.

Phase 6 — 테스트와 검증 자동화 ​

테스트는 MySQL과 PostgreSQL 각각에 대해 동일한 계약을 실행한다. SQLite를 PostgreSQL 대체품으로 사용하지 않는다.

필수 테스트 영역은 다음과 같다.

  1. schema baseline을 빈 PostgreSQL DB에 적용한다.
  2. setup bootstrap을 두 번 실행해 중복 생성 없이 성공하는지 확인한다.
  3. portal state와 installation config의 create/update/read를 확인한다.
  4. directory parent-child·group·membership transaction을 확인한다.
  5. archive post/file/blob/version/link/deletion log를 확인한다.
  6. unique 충돌은 409 또는 domain conflict로 분류하고 일반 DB 오류는 500으로 오인하지 않는지 확인한다.
  7. rollback 중 부분 데이터가 남지 않는지 확인한다.
  8. PostgreSQL rowCount, RETURNING, jsonb, boolean, timestamp를 확인한다.
  9. migration을 빈 DB와 기존 이관 DB에 각각 적용한다.
  10. pnpm check, pnpm test, pnpm build를 CI 필수 단계로 둔다.

Phase 7 — Staging cutover ​

Staging에서 production과 동일한 PostgreSQL major version, TLS, pool, environment variables를 사용한다. MySQL snapshot을 복원한 뒤 이관하고, 실제 애플리케이션을 PostgreSQL candidate로 연결한다. 주요 화면과 API를 read/write 모두 점검한다.

Cutover 승인 기준은 다음과 같다.

기준통과 조건
데이터 parity모든 테이블 row count·checksum·FK 검증 통과
기능setup, login, directory, archive, upload metadata, portal state 통과
오류PostgreSQL SQLSTATE가 domain error로 정확히 매핑
성능대표 query p95가 기존 기준 대비 허용 범위
안정성transaction rollback·pool 재연결·재시작 통과
관측DB error, slow query, pool usage, migration version 로그 존재
복구MySQL snapshot 복원과 이전 deployment 재기동 성공

Phase 8 — Production cutover과 롤백 ​

  1. PostgreSQL 최종 snapshot과 MySQL 최종 logical dump를 생성한다.
  2. 애플리케이션을 maintenance mode 또는 write freeze로 전환한다.
  3. MySQL에서 마지막 delta를 추출하고 PostgreSQL에 반영한다.
  4. row count·checksum·FK·sequence 검증을 다시 실행한다.
  5. PostgreSQL migration ledger를 확인한다.
  6. DATABASE_DIALECT=postgresql과 PostgreSQL DATABASE_URL을 설정한다.
  7. 새 deployment를 배포하고 health/readiness probe를 확인한다.
  8. 관리자 login, 조직도, 문서 목록, 업로드·다운로드 metadata, portal state를 smoke test한다.
  9. 오류율·latency·pool·connection·deadlock을 관찰한다.
  10. 안정화 기간 동안 MySQL을 read-only 보존한다.

롤백 조건은 migration 실패, parity 불일치, 지속적인 5xx 증가, 데이터 쓰기 실패, archive 참조 손상, 인증 장애다. 롤백 시 신규 PostgreSQL write를 차단하고, 마지막 MySQL snapshot과 cutover 전 deployment로 복귀한다. cutover 이후 PostgreSQL에 생성된 데이터가 있다면 먼저 별도 export해 유실 원인을 보존한 뒤 복귀한다.

4. 구현 순서와 커밋 분리 ​

커밋은 다음 단위로 분리한다.

커밋내용
1schema.pg.ts, provider 타입, config skeleton
2PostgreSQL migration baseline
3PostgreSQL pool·transaction·result adapter
4portal state·installation config 전환
5directory read/write 전환
6archive DB 전환
7bootstrap·setup 전환
8cross-dialect contract tests
9migration/data transfer tooling
10deployment·runbook·rollback documentation

각 커밋은 독립적으로 타입 검사 가능하게 유지하며, 운영 DB 변경과 애플리케이션 배포를 한 커밋에 섞지 않는다.

5. 완료 정의 ​

전환은 다음 조건을 모두 만족할 때 완료로 판정한다.

  • PostgreSQL 빈 DB에서 baseline과 모든 migration이 성공한다.
  • DATABASE_DIALECT=postgresql로 setup부터 archive 기능까지 동작한다.
  • MySQL provider 테스트도 계속 통과한다.
  • MySQL 전용 SQL은 MySQL provider 파일 외에 남아 있지 않다.
  • insertId, affectedRows, [rows, fields], ON DUPLICATE KEY, SHOW TABLES, ? placeholder의 PostgreSQL 경로 의존성이 없다.
  • source·target 데이터 parity와 foreign key 검증이 통과한다.
  • PostgreSQL sequence와 timezone 정책이 검증된다.
  • smoke test와 rollback rehearsal가 완료된다.
  • CI에서 check·test·build·migration validation이 필수화된다.
  • 운영 runbook과 secret rotation·backup·restore 절차가 저장소에 문서화된다.

6. 현실적인 우선순위 ​

가장 먼저 해야 할 일은 PostgreSQL adapter와 schema baseline을 만드는 것이 아니라, 현재 MySQL 데이터의 실제 규모와 archive object storage 참조 무결성을 측정하는 것이다. 데이터 양과 파일 참조 상태가 확정되어야 downtime과 cutover 방식을 결정할 수 있다.

그 다음 P0는 server/setupBootstrap.ts와 server/db.ts의 MySQL 고정 경로를 분리하는 것이다. 이 두 영역이 PostgreSQL 경로로 안전하게 전환되지 않으면 migration이 성공해도 애플리케이션은 시작 또는 setup 단계에서 실패한다. P1은 archive transaction과 portal state, P2는 성능 최적화·index tuning·MySQL decommission이다.

References ​