DB 무중단 마이그레이션(Henry) - 100-hours-a-week/9-team-Devths-WIKI GitHub Wiki

1. 마이그레이션 계획 및 진행 문서

2. 개요

2.1. 문서 목적

본 문서는 devths 서비스의 데이터베이스를 V1 환경(EC2 내 Self-managed PostgreSQL)에서 V2 환경(AWS RDS PostgreSQL)으로 무중단 마이그레이션하기 위한 전체 계획을 기술한다. 마이그레이션 진행 시 본 문서를 참고하여 사전 준비부터 컷오버, 롤백, 사후 정리까지 전 과정을 일관되게 수행할 수 있도록 하는 것을 목적으로 작성한다.

2.2. 마이그레이션 전략: 2-Phase 컷오버

본 마이그레이션은 단번에 모든 인프라(애플리케이션+DB)를 전환할 때 발생하는 리스크를 완화하기 위해 2단계 컷오버(2-Phase Cutover) 방식을 채택한다.

  1. 1차 컷오버 (서비스 분리): V2 애플리케이션(EC2)을 배포하되 DB는 여전히 V1 PostgreSQL을 바라보게 한다. 이를 통해 애플리케이션 분리 배포의 안정성을 실트래픽으로 먼저 검증한다.
  2. 2차 컷오버 (DB 전환): V2 애플리케이션 안정성 검증 후, 최종적으로 DB 엔드포인트를 V1에서 V2 RDS로 전환한다.

2.2.1. 핵심 제약: DDL 동결 정책

1차 컷오버(V2 서비스 전환) 이후부터 2차 컷오버(RDS 전환) 완료 시점까지 스키마 변경(DDL)을 수반하는 배포를 전면 금지한다. Logical Replication은 DML만 복제하므로, 이 기간 중 V1 DB의 스키마가 변경되면 복제(Subscription)가 중단된다.

2.3. 배경 및 필요성

현재 V1 환경의 한계

V1 환경은 MVP 개발 단계에서 빠른 구축을 위해 애플리케이션 서버와 동일 EC2 인스턴스 내에 PostgreSQL을 설치하여 운영하고있다. 해당 인프라 구성 방식은 다음과 같은 문제를 야기한다.

  • 애플리케이션과 DB간의 리소스 경합 발생
  • 인스턴스 장애 시 애플리케이션과 DB가 동시에 중단되는 단일 장애점(SPOF) 존재
  • 수동 백업 및 복구 구성으로 운영 부담 증가
  • 스케일 아웃 불가로 추후 V2 목표 트래픽 대응 불가

V2 RDS 전환 필요성

AWS RDS로 전환하면 다음과 같은 이점 확보가 가능하다.

  • 자동 백업 및 Point-in-Time-Recovery(PITR) 지원
  • 고가용성 확보
  • 애플리케이션 서버와 DB 서버의 분리로, 독립적 스케일링 가능
  • 모니터링, 패치, 유지보수 자동화로 운영 부담 감소

2.4. 용어 정의

용어 설명
V1 DB 현재 운영 중인 EC2 내 Self-managed PostgreSQL 14.20
V2 DB / RDS 마이그레이션 목표인 AWS RDS PostgreSQL 14.x
Shadow EC2 EBS 스냅샷으로 V1 DB를 복제한 임시 EC2 인스턴스. DMS의 Source로 활용
Full Load DMS가 Shadow EC2의 전체 데이터를 RDS로 초기 복사하는 단계
CDC Change Data Capture. Full Load 이후 V1 DB의 변경분을 실시간으로 RDS에 동기화하는 방식
Lag CDC 동기화 지연시간. 컷오버 조건 판단 기준
컷오버 애플리케이션의 DB 연결을 V1에서 V2 RDS로 전환하는 시점
롤백 컷오버 후 이상 감지 시 V1 DB로 되돌아가는 절차
WAL Write-Ahead Log. PostgreSQL의 변경 로그. CDC의 기반이 되는 데이터
DMS AWS Database Migration Service. Full Load 및 CDC를 관리하는 AWS 관리형 서비스
PITR Point-in-Time Recovery. 특정 시점으로 DB를 복구하는 기능
RPO Recovery Point Objective. 데이터 유실 허용 범위. 본 마이그레이션에서는 0으로 설정
RTO Recovery Time Objective. 서비스 복구 목표 시간. 롤백 기준 30초 이내

3. 현황 분석

3.1. V1(MVP) 환경

  • 단일 EC2 인스턴스에 애플리케이션(BE, FE, AI) 서버와 DB가 함께 구성되어있다.

  • DB: PostgreSQL 14.20 (Ubuntu 22.04, x86_64)

  • 익스텐션: plpgsql 1.0 만 사용 (RDS 완전 호환, 추가 작업 필요 없음)

  • 데이터 용량: 약 10MB(DB에 작성된 사용자 데이터)

  • 트래픽: 실제 운영 중 (사용자 존재)

  • 커넥션 풀: HikariCP (max 20, min-idle 5, connection-timeout: 30,000ms, idle-timeout: 600,000ms)

  • 현재 환경 분석을 위한 스크립트

    #콘솔 psql로 접속 후 확인
    
    #==========#
    #postgreSQL 버전 확인 
    SELECT version();
    
    #==========#
    #현재 설치, 사용중인 익스텐션 확인
    SELECT extname, extversion FROM pg_extension;
    
    #==========#
    #현재 WAL 설정 확인
    -- 현재 WAL 레벨 확인 (마이그레이션 전 반드시 확인)
    SHOW wal_level;
    -- 예상 결과: replica (기본값)
    -- 필요 설정: logical (DMS CDC를 위해 변경 필요)
    
    SHOW max_replication_slots;
    -- 기본값: 10 (충분)
    
    SHOW max_wal_senders;
    -- 기본값: 10 (충분)

3.2. V2 목표 아키텍처

마이그레이션 완료 후 V2 환경은 애플리케이션(EC2 인스턴스)과 DB(RDS)가 분리된 구조로 전환된다.

  • DB: AWS RDS PostgreSQL

3.3. 주요 고려사항

  • 데이터 10MB로 매우 작은편 (빠른 데이터 이전 가능)
  • 운영 트래픽 있음 (데이터 누락 미 허용)
  • 다운타임 미허용 (Graceful Shutdown으로 기존 트랜잭션 유지 및 데이터 유실 방지)

4. 요구사항 정의

4.1. 기능적 요구사항

ID 요구사항 설명
FR-01 전체 데이터 이전 V1 DB의 모든 테이블, 인덱스, 시퀀스, 제약조건을 V2 RDS로 완전 이전
FR-02 데이터 정합성 보장 마이그레이션 완료 후 V1과 V2의 데이터가 완전히 일치함을 검증
FR-03 서비스 연속성 보장 마이그레이션 전 과정에서 사용자 서비스 중단 최소화
FR-04 롤백 가능성 보장 컷오버 후 이상 감지 시 V1으로 즉시 복구 가능(30초 이내)
FR-05 V1 DB 독립 운영 마이그레이션 완료 후 V2 RDS만으로 서비스 운영
FR-06 시퀀스 동기화 컷오버 시점의 모든 시퀀스 값을 V2에 정확히 반영

4.2. 비 기능적 요구사항

  • 서비스 다운타임 0초를 목표로 진행

    단계 다운타임 비고
    WAL 레벨 변경 (사전 작업) 30초 이내 최초 생성 시 해당 옵션 활성화 상태로, 재시작 불필요
    데이터 동기화 기간 0초 운영 병행
    컷오버 (실제 전환) 0초 목표 HikariCP 재연결로 처리
  • 데이터 정합성 조건 확인

    항목 기준 검증 방법
    Row Count V1 = V2 (100% 일치) 테이블별 COUNT 비교
    Checksum V1 = V2 (100% 일치) MD5 해시 비교
    시퀀스 V2 >= V1 (컷오버 시점 기준) pg_sequences 비교
    인덱스 V1과 동일한 인덱스 구성 pg_indexes 비교
    제약조건 V1과 동일한 제약조건 구성 information_schema 비교
  • 성능 요구사항

    항목 기준 비고
    Full Load 완료 시간 1시간 이내 데이터 1GB 미만 기준
    CDC Lag 컷오버 시점 0ms 목표 1초 이내 허용
    컷오버 후 응답시간 V1 대비 110% 이내 성능 저하 허용 범위
    롤백 완료 시간 30초 이내 RTO 기준
  • 가용성 요구사항

    항목 기준
    RPO (데이터 유실 허용 범위) 0 (데이터 유실 없음)
    RTO (서비스 복구 목표 시간) 30초 이내 (롤백 기준)
    PITR 백업 보존 기간 최소 7일

4.3. 제약 사항

  • PostgreSQL → RDS 전환 시 동일 메이저 버전 유지로 호환성 유지

  • HikariCP max-pool-size 20인 현재 상태로 DMS로 인한 부하 고려

  • 기술적 제약

    ID 제약 영향 대응 방안
    TC-01 PostgreSQL 14.x 버전 유지 메이저 버전 업그레이드 불가 RDS 14.x로 동일 버전 생성
    TC-02 WAL 레벨 변경 시 재시작 필요 30초 다운타임 발생 새벽 시간대 진행, 사전 공지
    TC-03 Subscriber가 V1에 직접 연결 복제 전용 커넥션 1개 추가 사용 max-pool-size 20 기준 영향 없음
    TC-04 RDS 슈퍼유저 권한 제한 CREATE SUBSCRIPTION 실행 제한 rds_superuser 역할 부여로 해결 가능
    TC-05 DMS는 시퀀스 자동 복제 미지원 컷오버 시 시퀀스 불일치 가능 컷오버 직전 수동 동기화 스크립트 실행
    TC-06 Logical Replication DDL 미지원 복제 중 스키마 변경 시 복제 중단 가능 마이그레이션 기간 중 스키마 변경 배포 동결
    TC-07 복제 전용 유저 권한 설정 필요 V1에 REPLICATION 권한 유저 없으면 불가 마이그레이션 전 복제 전용 유저 사전 생성
  • 운영적 제약

    ID 제약 영향 대응 방안
    OC-01 실제 운영 트래픽 존재 마이그레이션 중 서비스 영향 최소화 복제 전용 커넥션 분리로 운영 트래픽과 경합 방지
    OC-02 비용 최소화 필요 임시 리소스 장기 운영 불가 Logical Replication은 추가 비용 없음
    OC-03 단독 진행 (소규모 팀) 실시간 모니터링 인력 제한 자동화 스크립트 및 체크리스트로 보완
    OC-04 마이그레이션 기간 배포 동결 신규 기능 배포 일시 중단 마이그레이션 기간 최소화 (목표 D-3 ~ D+1)

4.4. 다운타임 조건

일정 단계 작업 내용 다운타임 진행 시간대
D-3일 사전 작업 WAL 레벨 변경 + PostgreSQL 재시작 30초 이내 새벽 2~4시
D-2일 환경 구성 RDS 생성 + 스키마 적용 + 복제 전용 유저 생성 0초 상시
D-1일 복제 시작 Publication 생성(V1) + Subscription 생성(RDS) 0초 상시
초기 데이터 자동 동기화 시작 (copy_data = true) 0초 상시
D-Day (1차) 앱 컷오버 V2 애플리케이션 배포 시작 (DB 연결은 V1 유지). 실트래픽 검증. 0초 목표 상시
D-Day (중간) 운영/검증 V2 서비스 + V1 DB 구조로 운영. (DDL 변경 배포 절대 금지) 0초 최소 수 시간
D-Day (2차) DB 컷오버 RDS 초기화 후 재동기화 → Lag 0 확인 → 시퀀스 동기화 → HikariCP DB 연결(V1→RDS) 전환 0초 목표 새벽 시간대
D+1 ~ D+3 사후 관리 모니터링 + Publication/Subscription 삭제 0초 상시

4.5. 롤백 트리거 기준

  • 트리거 플로우 차트 스크립트

    flowchart TD
        A[컷오버 완료] --> B{모니터링 이상 감지}
    
        B --> |정상| C[D+1 ~ D+3 모니터링 유지]
        C --> D[V1 DB / Shadow EC2 / DMS 삭제]
        D --> E[마이그레이션 완료 ✅]
    
        B --> |이상 감지| F{트리거 조건 확인}
    
        F --> G[에러율 5% 초과]
        F --> H[응답시간 V1 대비 200% 초과]
        F --> I[DB 연결 실패]
        F --> J[데이터 불일치 감지]
    
        G --> K[롤백 결정]
        H --> K
        I --> K
        J --> K
    
        K --> L[V1 DB read-only 해제]
    L --> M[Spring Boot → V1 재연결]
    M --> N[헬스체크 확인]
    
    N --> |성공| O[롤백 완료 ✅\n소요시간 30초 이내]
    N --> |실패| P[긴급 대응\n온콜 알림]
    
    O --> Q[원인 분석 후 재마이그레이션 계획]
    
    style A fill:#4CAF50,color:#fff
    style E fill:#4CAF50,color:#fff
    style O fill:#2196F3,color:#fff
    style K fill:#FF5722,color:#fff
    style P fill:#B71C1C,color:#fff
    style G fill:#FFF3E0,color:#333
    style H fill:#FFF3E0,color:#333
    style I fill:#FFF3E0,color:#333
    style J fill:#FFF3E0,color:#333
    
    Loading

*(참고: 1차 애플리케이션 컷오버 후 앱 단의 롤백은, 로드밸런서(ALB) 타겟을 다시 V1 EC2로 원복하는 방식으로 처리함.)*

# 5. 리스크 분석

## 5.1. 리스크 목록 및 영향도

| ID | 리스크 | 발생 가능성 | 영향도 | 위험도 |
| --- | --- | --- | --- | --- |
| R-01 | WAL 재시작 중 데이터 유실 | 낮음 | 매우 높음 | 🔴 높음 |
| R-02 | Logical Replication Lag 증가 | 중간 | 높음 | 🔴 높음 |
| R-03 | 시퀀스 불일치로 PK 충돌 | 높음 | 높음 | 🔴 높음 |
| R-04 | 컷오버 후 데이터 불일치 감지 | 중간 | 매우 높음 | 🔴 높음 |
| R-05 | HikariCP 재연결 실패 | 낮음 | 높음 | 🟠 중간 |
| R-06 | 복제 중 DDL 변경으로 복제 중단 | 낮음 | 높음 | 🟠 중간 |
| R-07 | V1 DB 직접 부하로 운영 영향 | 낮음 | 중간 | 🟡 낮음 |

```mermaid
quadrantChart
    title Risk Matrix
    x-axis Low Probability --> High Probability
    y-axis Low Impact --> High Impact
    quadrant-1 Immediate Action
    quadrant-2 Monitor Continuously
    quadrant-3 Low Priority
    quadrant-4 Preventive Action
    R-01 WAL Restart: [0.2, 0.95]
    R-02 Replication Lag: [0.5, 0.80]
    R-03 Sequence Mismatch: [0.75, 0.80]
    R-04 Data Inconsistency: [0.5, 0.90]
    R-05 HikariCP Reconnect: [0.2, 0.70]
    R-06 DDL Change: [0.2, 0.70]
    R-07 V1 Direct Load: [0.2, 0.45]

5.2. 리스크 별 대응 방안

R-01. WAL 재시작 중 데이터 유실 🔴

-- [사전 조치 1] 재시작 전 활성 트랜잭션 확인
SELECT pid, usename, application_name,
       state, wait_event_type, query_start,
       now() - query_start AS duration
FROM pg_stat_activity
WHERE state != 'idle'
AND query_start IS NOT NULL;
# [사전 조치 2] 재시작 전 pg_dump로 백업 (복구 포인트 확보)
pg_dump -U postgres \
    -h localhost \
    -d devths \
    -F c \
    -f "/backup/devths_pre_migration_$(date +%Y%m%d%H%M%S).dump"

# [사전 조치 3] postgresql.conf WAL 레벨 변경
sudo sed -i "s/#wal_level = replica/wal_level = logical/" \
    /etc/postgresql/14/main/postgresql.conf

# [사전 조치 4] PostgreSQL 재시작 (30초 이내)
sudo systemctl restart postgresql

# [사후 확인] WAL 레벨 변경 확인
psql -U postgres -c "SHOW wal_level;"
# 결과: logical

R-02. DMS CDC 지연 증가 🔴

-- [모니터링] V1에서 복제 Lag 실시간 확인
SELECT
    application_name,
    client_addr,
    state,
    pg_size_pretty(pg_wal_lsn_diff(
        pg_current_wal_lsn(), sent_lsn
    )) AS send_lag,
    pg_size_pretty(pg_wal_lsn_diff(
        sent_lsn, write_lsn
    )) AS write_lag,
    pg_size_pretty(pg_wal_lsn_diff(
        write_lsn, replay_lsn
    )) AS replay_lag,
    pg_wal_lsn_diff(
        pg_current_wal_lsn(), replay_lsn
    ) AS total_lag_bytes
FROM pg_stat_replication;

-- [컷오버 조건] total_lag_bytes = 0 확인 후 진행
# [자동화] Lag 모니터링 스크립트 (5초 간격)
while true; do
    LAG=$(psql -U postgres -t -c "
        SELECT pg_wal_lsn_diff(
            pg_current_wal_lsn(), replay_lsn
        )
        FROM pg_stat_replication
        WHERE application_name = 'devths_sub';
    ")
    echo "$(date '+%H:%M:%S') Replication Lag: ${LAG} bytes"
    if [ "${LAG}" -eq 0 ]; then
        echo "✅ Lag 0 확인 - 컷오버 진행 가능"
        break
    fi
    sleep 5
done

R-03. 시퀀스 불일치로 PK 충돌 🔴

-- [컷오버 직전] V2 RDS에서 실행
DO $$
DECLARE
    seq_record RECORD;
    max_val BIGINT;
    seq_query TEXT;
BEGIN
    FOR seq_record IN
        SELECT
            n.nspname AS schema_name,
            s.relname AS seq_name,
            t.relname AS table_name,
            a.attname AS col_name
        FROM pg_class s
        JOIN pg_namespace n ON n.oid = s.relnamespace
        JOIN pg_depend d ON d.objid = s.oid
        JOIN pg_class t ON t.oid = d.refobjid
        JOIN pg_attribute a ON a.attrelid = t.oid
            AND a.attnum = d.refobjsubid
        WHERE s.relkind = 'S'
    LOOP
        seq_query := format(
            'SELECT COALESCE(MAX(%I), 0) FROM %I.%I',
            seq_record.col_name,
            seq_record.schema_name,
            seq_record.table_name
        );
        EXECUTE seq_query INTO max_val;

        PERFORM setval(
            format('%I.%I',
                seq_record.schema_name,
                seq_record.seq_name),
            max_val + 1000
        );

        RAISE NOTICE '✅ 시퀀스 동기화 완료: %.% → %',
            seq_record.table_name,
            seq_record.col_name,
            max_val + 1000;
    END LOOP;
END $$;
-- [검증] 동기화 후 시퀀스 현황 확인
SELECT
    sequencename,
    last_value,
    increment_by,
    max_value
FROM pg_sequences
ORDER BY sequencename;

R-04. 컷오버 후 데이터 불일치 감지 🔴

-- [컷오버 전 검증 1] Row Count 비교
-- V1과 V2 양쪽에서 실행 후 결과 대조
SELECT
    schemaname,
    tablename,
    n_live_tup AS row_count
FROM pg_stat_user_tables
ORDER BY tablename;
-- [컷오버 전 검증 2] Checksum 비교
-- V1과 V2 양쪽에서 실행 후 결과 대조
SELECT
    tablename,
    md5(string_agg(t::text, ',' ORDER BY ctid)) AS checksum
FROM (
    SELECT tablename, tableoid::regclass t
    FROM pg_tables
    WHERE schemaname = 'public'
) sub
GROUP BY tablename;
# [컷오버 후] 애플리케이션 헬스체크 모니터링
while true; do
    STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
        http://localhost:8080/actuator/health)
    echo "$(date '+%H:%M:%S') Health: $STATUS"
    if [ "$STATUS" != "200" ]; then
        echo "🚨 이상 감지 - 롤백 트리거"
        break
    fi
    sleep 5
done

R-05. HikariCP 재연결 실패 🟠

# [컷오버 전] RDS 연결 사전 테스트
psql -h RDS_ENDPOINT \
     -U devths_user \
     -d devths \
     -c "SELECT version();"

# [컷오버 전] Security Group 포트 확인
aws ec2 describe-security-groups \
    --group-ids sg-xxxxxxxxx \
    --query 'SecurityGroups[*].IpPermissions' \
    --region ap-northeast-2
# [Spring Boot] 컷오버 시 적용할 RDS 연결 설정
spring:
  datasource:
    url: jdbc:postgresql://RDS_ENDPOINT:5432/devths
    username: devths_user
    password: ${DB_PASSWORD}
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      connection-timeout: 30000
      idle-timeout: 600000
      initialization-fail-timeout: 0
      keepalive-time: 30000

R-06. 복제 중 DDL 변경으로 복제 중단 🟠

-- [사전 조치] 복제 상태 모니터링
-- DDL 변경 발생 시 복제가 중단되고 아래 쿼리에서 감지 가능
SELECT
    subname,
    subenabled,
    subslotname
FROM pg_subscription;

-- 복제 중단 감지 시 Subscription 재생성 필요
DROP SUBSCRIPTION devths_sub;
CREATE SUBSCRIPTION devths_sub
CONNECTION 'host=V1_IP port=5432 dbname=devths user=repl_user password=<repl-password>'
PUBLICATION devths_pub
WITH (copy_data = false);  -- 데이터 재복사 없이 재연결

[운영 프로세스] 마이그레이션 기간 배포 동결 절차

  1. 마이그레이션 시작 전 팀 전체 공지
  2. D-1일 ~ D+1일 스키마 변경 배포 금지
  3. 긴급 배포가 필요한 경우 마이그레이션 일정 재조정

R-07. V1 DB 직접 부하로 운영 영향 🟡

-- [모니터링] V1 복제 커넥션 확인
SELECT
    pid,
    usename,
    application_name,
    client_addr,
    state,
    backend_type
FROM pg_stat_activity
WHERE backend_type = 'walsender';

-- 복제 전용 커넥션 1개만 존재해야 정상
-- 운영 트래픽 커넥션과 분리되어 있음을 확인

5.2.1. 리스크 대응 우선순위

즉시 대응 필요 (마이그레이션 시작 전 반드시 해소)
├── R-01: WAL 변경 전 pg_dump 백업 완료
├── R-02: Lag 모니터링 스크립트 사전 검증 완료
├── R-03: 시퀀스 동기화 스크립트 사전 검증 완료
└── R-06: 팀 전체 배포 동결 공지 완료

컷오버 시점 실시간 모니터링
├── R-02: Lag 0 확인 후 컷오버 진행
├── R-04: 데이터 검증 2단계 완료 후 진행
└── R-05: RDS 연결 테스트 완료 후 진행

컷오버 후 72시간 모니터링
├── R-04: 데이터 불일치 감지 시 즉시 롤백
└── R-07: 응답시간 이상 감지 시 롤백 트리거

6. 마이그레이션 방식 선택

6.1. 후보 방식 비교

항목 DMS 단독 Shadow EC2 + DMS Logical Replication 단독 (선택)
무중단 여부
데이터 유실 없음
V1 운영 DB 부하 🔴 직접 부하 ✅ 없음 (Shadow 분리) 🟡 복제 커넥션 1개 추가
RDS 호환성 ✅ (rds_superuser로 해결)
시퀀스 자동 복제 ❌ (수동 동기화)
데이터 검증 도구 ✅ DMS 내장 ✅ DMS 내장 🟠 직접 구현 필요
롤백 용이성 🟠 중간 ✅ 높음 🟠 중간
구현 복잡도 낮음 높음 중간
비용 🟠 DMS 유료 🔴 DMS + Shadow EC2 ✅ 무료

6.2. 방식 별 트레이드 오프

6.2.1. DMS 단독

장점

  • 구성이 단순하고 AWS GUI로 진행 가능
  • DMS 내장 데이터 검증 기능 활용 가능

단점

  • DMS가 V1 운영 DB를 직접 읽어 운영 트래픽과 부하 경합
  • HikariCP max-pool-size 20 기준 커넥션 경합 가능성
  • Shadow EC2 없으므로 롤백 포인트 부족

제외 근거

  • 운영 트래픽이 존재하는 상황에서 V1 DB 직접 부하는 서비스 안정성 리스크가 있음
  • 추가 비용 대비 Logical Replication 대비 이점 없음

6.2.2. Shadown + DMS

장점

  • Shadow EC2가 V1 운영 DB를 완전히 보호
  • 단계별 롤백 포인트 명확히 확보
  • DMS 내장 검증 기능 활용 가능

단점

  • 구성 단계가 많아 복잡도 높음
  • DMS + Shadow EC2 비용 이중 발생
  • 시퀀스 수동 동기화 필요

제외 근거

  • 데이터 1GB 미만, MAU 200명 수준의 트래픽에서 Shadow EC2까지 구성하는 것은 과도한 복잡도
  • 비용 대비 효율이 낮음

6.2.3. Logical Replication (최종 선택)

장점

  • 추가 AWS 비용 없이 PostgreSQL 네이티브 기능만으로 구현
  • 복제 커넥션 1개만 추가되어 V1 운영 트래픽 영향 최소
  • WAL, LSN, Publication/Subscription 등 DB 내부 원리를 직접 다루며 제어 가능.
  • 구성 단계가 적어 실수 포인트 최소화

단점

  • DMS 대비 데이터 검증 도구 부재로 직접 구현 필요
  • 복제 중 DDL 변경 시 복제 중단 가능 (배포 동결로 대응)
  • 시퀀스 수동 동기화 필요 (DMS도 동일한 제약)
flowchart TD
    A[마이그레이션 방식 선정] --> B{운영 트래픽 존재?}

    B --> |Yes| C{비용 최소화 필요?}
    B --> |No| D[DMS 단독 가능]

    C --> |Yes| E{RDS 권한 제약 해결 가능?}
    C --> |No| F[Shadow EC2 + DMS]

    E --> |Yes - rds_superuser 활용| G{데이터 검증 직접 구현 가능?}
    E --> |No| F

    G --> |Yes| H[✅ Logical Replication 단독 선택]
    G --> |No| F

    style H fill:#4CAF50,color:#fff
    style F fill:#9E9E9E,color:#fff
    style D fill:#9E9E9E,color:#fff
Loading

6.3. 최종 선택 및 근거

  1. 비용 최소화 DMS Replication Instance + Shadow EC2 비용 없이 PostgreSQL 네이티브 기능만으로 동일한 목표 달성 가능. 데이터 1GB 미만 규모에서 DMS는 과도한 투자임.
  2. V1 운영 트래픽 영향 최소화 복제 전용 커넥션 1개만 추가되며 HikariCP max-pool-size 20 기준으로 운영 트래픽에 실질적 영향 없음.
  3. RDS 권한 제약 해결 가능 rds_superuser 역할 부여로 CREATE SUBSCRIPTION 실행 가능. 실제 운영에서 검증된 방식임.
  4. 데이터 검증 직접 구현 가능 Row Count + Checksum 검증 스크립트를 직접 구현하여 DMS 내장 검증 도구 부재를 보완.

검토했으나 제외한 방식

  • DMS 단독: V1 운영 DB 직접 부하 + 불필요한 비용 발생
  • Shadow EC2 + DMS: 소규모 환경 대비 과도한 복잡도와 비용

7. 마이그레이션 아키텍처

7.1. 전체 구상도

7.1.1. 마이그레이션 전(V1)

[사용자]
    ↓ HTTPS (443)
    ↓ HTTP → HTTPS 리다이렉트 (80)
[devths.com / api.devths.com / ai.devths.com]
    ↓ DNS
[devths-v1-prod (<v1-ec2-instance-id>)]
[<vpc-id> / <subnet-id> / <v1-ec2-private-ip> / <v1-ec2-public-ip>]
    └── Nginx (Local Proxy)
          ├── devths.com / www.devths.com → Next.js  (localhost:3000)
          ├── api.devths.com              → Spring Boot (localhost:8080)
          └── ai.devths.com               → FastAPI  (localhost:8000)
                │
                ↓ localhost:5432
          PostgreSQL 14.20 (Self-managed)
          devths DB (10MB)

7.1.2. 마이그레이션 중

[devths-V1-prod-vpc]
<vpc-id> / <vpc-cidr>
│
│  devths-v1-prod (<v1-ec2-instance-id> / <v1-ec2-private-ip>)
│  ├── Nginx
│  │    ├── api.devths.com → Spring Boot (V1 DB 연결 유지)
│  │    ├── devths.comNext.js
│  │    └── ai.devths.com  → FastAPI
│  └── PostgreSQL 14.20
│       └── Publication: devths_pub (FOR ALL TABLES)
│             │
│             │ WAL 변경분 실시간 전송 (5432)
│             │ VPC Peering (<pcx-id>)
│             │ <vpc-cidr><v2-vpc-cidr>
│             ↓
[devths-V2-prod-vpc]
vpc-0b4e73006f0c0b7e2 / <v2-vpc-cidr>
│
│  devths-v2-prod-rds
│  devths-v2-prod-rds.<rds-endpoint>
│  ├── Subscription: devths_sub (변경분 수신 및 적용)
│  ├── Primary   (ap-northeast-2a / <subnet-id> / 172.16.20.0/24)
│  └── Standby   (ap-northeast-2c / <subnet-id> / 172.16.21.0/24)

7.1.3. 마이그레이션 후(V3)

[사용자]
    ↓ HTTPS (443)
[api.devths.com]
    ↓
[devths-V2-prod-vpc / vpc-0b4e73006f0c0b7e2 / <v2-vpc-cidr>]
│
│  Spring Boot (devths-v2-prod-be-sg / sg-0846764a14cd4ba66)
│       ↓ Private Subnet 내부 통신 (5432)
│
│  devths-v2-prod-rds (PostgreSQL 14.17)
│  devths-v2-prod-rds.<rds-endpoint>
│  ├── Primary  (ap-northeast-2a / <subnet-id>)
│  └── Standby  (ap-northeast-2c / <subnet-id>)
│         ↓
│  자동 백업 + PITR (보존 7일)

7.2. 컴포넌트 별 역할

컴포넌트 역할 운영 기간
devths-v1-prod ( / ) 운영 서버 + Publisher (복제 송신) ~ D+3일
PostgreSQL 14.20 (V1 내부 / localhost:5432) 복제 원본 DB ~ D+3일
Nginx (V1) 단일 EC2 내부 리버스 프록시 ~ 컷오버
devths-v2-prod-rds (PostgreSQL 14.17) Subscriber (복제 수신) → 컷오버 후 운영 DB D-2일 ~ 영구
Publication: devths_pub V1 전체 테이블 복제 정의 D-1일 ~ D+3일
Subscription: devths_sub RDS에서 V1 변경분 수신 및 적용 D-1일 ~ D+3일
VPC Peering () V1 VPC ↔ V2 prod VPC 통신 마이그레이션 기간
devths-prod-rds-sg () RDS 인바운드 5432 허용 () 마이그레이션 기간
devths-V1-prod-ec2 () V1 인바운드 5432 허용 () 마이그레이션 기간

7.3. 네트워크 구성

7.3.1. VPC 구성 및 피어링

VPC VPC ID CIDR 용도
devths-V1-prod-vpc V1 운영 환경
devths-V2-prod-vpc vpc-0b4e73006f0c0b7e2 V2 운영 환경

VPC Peering: (active)

7.3.2. 라우팅 테이블

라우팅 테이블 VPC 목적지 타겟
devths-V1-prod-vpc local
devths-V1-prod-vpc
devths-V2-prod-vpc
devths-V2-prod-vpc local
devths-V2-prod-vpc 0.0.0.0/0

7.3.3. SG 설정

Security Group 방향 포트 소스 목적
devths-prod-rds-sg () Inbound 5432 V1 EC2 → RDS 연결 ✅
devths-V1-prod-ec2 () Inbound 5432 RDS Subscriber → V1 Publisher ✅
devths-V1-prod-ec2 () Inbound 80 0.0.0.0/0 HTTP (HTTPS 리다이렉트)
devths-V1-prod-ec2 () Inbound 443 0.0.0.0/0 HTTPS
devths-V1-prod-ec2 () Inbound 9100 192.168.0.0/16 Node Exporter 모니터링

7.3.4. Logical Replication 통신 흐름

devths-v1-prod (<v1-ec2-private-ip>:5432)
        │
        │ ① RDS Subscriber가 V1 Publisher에 연결 요청
        │   172.16.x.x → <v1-ec2-private-ip>:5432
        │   <v1-sg-id> Inbound 5432 허용 ✅
        │
        │ ② V1 WAL 변경분 전송
        │   <v1-ec2-private-ip> → devths-v2-prod-rds:5432
        │   VPC Peering <pcx-id> 경유
        │   <rds-sg-id> Inbound 5432 허용 ✅
        ↓
devths-v2-prod-rds
(devths-v2-prod-rds.<rds-endpoint>:5432)

7.4. Logical Replication 내부 동작

[V1 devths-v1-prod / <v1-ec2-private-ip>]
        │
        │ wal_level = logical 설정 후
        │ pgoutput 플러그인이 WAL을 논리적 이벤트로 디코딩
        │ INSERT / UPDATE / DELETE 단위로 변환
        ↓
[Publication: devths_pub]
  CREATE PUBLICATION devths_pub FOR ALL TABLES
        │
        │ PostgreSQL Replication Protocol
        │ VPC Peering (<pcx-id>)
        │ <v1-ec2-private-ip>:5432 → devths-v2-prod-rds:5432
        ↓
[Subscription: devths_sub]
  devths-v2-prod-rds에서 수신 및 적용
  Logical Replication Worker가 변경분 처리
        │
        ↓
[LSN 기반 동기화 위치 추적]
-- V1에서 현재 WAL 위치 확인
SELECT pg_current_wal_lsn();

-- RDS에서 복제 수신 위치 확인
SELECT
    subname,
    received_lsn,
    latest_end_lsn,
    latest_end_time
FROM pg_stat_subscription;
-- received_lsn = V1의 pg_current_wal_lsn() 이면 Lag = 0

-- V1에서 복제 지연 모니터링
SELECT
    application_name,
    client_addr,
    state,
    pg_size_pretty(pg_wal_lsn_diff(
        pg_current_wal_lsn(), sent_lsn
    )) AS send_lag,
    pg_size_pretty(pg_wal_lsn_diff(
        sent_lsn, write_lsn
    )) AS write_lag,
    pg_size_pretty(pg_wal_lsn_diff(
        write_lsn, replay_lsn
    )) AS replay_lag
FROM pg_stat_replication;

8. 사전 준비

8.1. RDS 인스턴스 생성 및 설정

복제 완료 된 정보가 저장 될 타켓 RDS를 생성

8.2. 복제 전용 유저 생성 (V1 EC2)

복제 전용 유저는 V1 PostgreSQL에 생성한다. 운영 유저와 분리하여 복제 권한만 부여한다.

-- V1 EC2에서 실행
-- sudo -u postgres psql

-- 복제 전용 유저 생성
CREATE USER repl_user WITH REPLICATION LOGIN PASSWORD '<repl-password>';

-- devths DB 접근 권한 부여
GRANT CONNECT ON DATABASE devths TO repl_user;

-- 스키마 내 모든 테이블 SELECT 권한 부여 (초기 데이터 복사용)
GRANT USAGE ON SCHEMA public TO repl_user;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO repl_user;

-- 이후 생성되는 테이블에도 자동 권한 부여
ALTER DEFAULT PRIVILEGES IN SCHEMA public
    GRANT SELECT ON TABLES TO repl_user;
-- 생성 확인
SELECT usename, userepl FROM pg_user WHERE usename = 'repl_user';
--  usename   | userepl
-- -----------+---------
--  repl_user | t

8.3. pg_hba.conf 설정 (V1 EC2)

RDS Subscriber가 V1 Publisher에 접속할 수 있도록 pg_hba.conf에 허용 규칙을 추가.

# V1 EC2에서 실행
sudo vi /etc/postgresql/14/main/pg_hba.conf

#아래 내용을 파일 하단에 추가
# Logical Replication - RDS Subscriber 허용 (devths-V2-prod-vpc CIDR)
host    replication     repl_user       <v2-vpc-cidr>           scram-sha-256
host    devths          repl_user       <v2-vpc-cidr>           scram-sha-256
# 설정 반영 (재시작 없이 적용)
sudo -u postgres psql -c "SELECT pg_reload_conf();"

# 반영 확인
sudo -u postgres psql -c "SELECT type, database, user_name, address, auth_method FROM pg_hba_file_rules WHERE user_name && ARRAY['repl_user'];"

8.4. WAL 레벨 변경

WAL 레벨을 replica에서 logical로 변경. PostgreSQL 재시작이 필요하므로 새벽 시간대에 진행.

# [사전 확인 1] 활성 트랜잭션 확인 (없어야 진행 가능)
sudo -u postgres psql -c "
SELECT pid, usename, state, query_start,
       now() - query_start AS duration
FROM pg_stat_activity
WHERE state != 'idle'
AND query_start IS NOT NULL;"
# [사전 확인 2] 현재 WAL 레벨 확인
sudo -u postgres psql -c "SHOW wal_level;"
# 결과: replica
# [사전 조치] pg_dump로 백업 (복구 포인트 확보)
sudo -u postgres pg_dump \
    -d devths \
    -F c \
    -f "/tmp/devths_pre_migration_$(date +%Y%m%d%H%M%S).dump"

# 백업 파일 확인
ls -lh /tmp/devths_pre_migration_*.dump
# [WAL 레벨 변경] postgresql.conf 수정
sudo sed -i "s/#wal_level = replica/wal_level = logical/" \
    /etc/postgresql/14/main/postgresql.conf

# 혹시 주석이 없는 경우
sudo sed -i "s/wal_level = replica/wal_level = logical/" \
    /etc/postgresql/14/main/postgresql.conf

# 변경 내용 확인
grep "wal_level" /etc/postgresql/14/main/postgresql.conf
# [PostgreSQL 재시작] (30초 이내)
sudo systemctl restart postgresql

# [재시작 후 확인]
sudo -u postgres psql -c "SHOW wal_level;"
# 결과: logical ✅

8.5. 스키마 적용(RDS)

V1 DB의 스키마를 RDS에 적용. 데이터는 복제하지 않고 스키마(DDL)만 먼저 적용

# V1 EC2에서 실행
# 스키마만 추출 (데이터 제외)
sudo -u postgres pg_dump \
    -d devths \
    --schema-only \
    -F p \
    -f "/tmp/devths_schema_$(date +%Y%m%d%H%M%S).sql"

# 추출 확인
ls -lh /tmp/devths_schema_*.sql
# RDS에 스키마 적용
psql \
    -h devths-v2-prod-rds.<rds-endpoint> \
    -U devths \
    -d devths \
    -f /tmp/devths_schema_*.sql
-- 스키마 적용 확인 (RDS에서 실행)
SELECT tablename
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY tablename;

8.6. RDS 재시작 (rds.logical_replication 적용)

파라미터 그룹의 rds.logical_replication = 1pending-reboot 상태이므로 재시작이 필요

# RDS 재시작
aws rds reboot-db-instance \
    --db-instance-identifier devths-v2-prod-rds \
    --region ap-northeast-2
# 재시작 완료 확인
aws rds describe-db-instances \
    --db-instance-identifier devths-v2-prod-rds \
    --query 'DBInstances[*].{Status:DBInstanceStatus,ParameterApplyStatus:DBParameterGroups[0].ParameterApplyStatus}' \
    --output table \
    --region ap-northeast-2
# ParameterApplyStatus: in-sync 확인
-- RDS에서 logical_replication 설정 확인
SHOW rds.logical_replication;
-- 결과: on ✅

8.7. 사전 준비 체크리스트

8.7.1. 인프라

  • devths-v2-prod-rds 생성 완료 (PostgreSQL 14.17, available)
  • rds.logical_replication = 1 적용 완료 (in-sync)
  • VPC Peering 라우팅 설정 완료 ()
  • Security Group 설정 완료
  • devths-prod-rds-sg ← 5432
  • devths-V1-prod-ec2 ← 5432

8.7.2. V1 PostgreSQL

  • pg_dump 백업 완료
  • wal_level = logical 변경 완료
  • PostgreSQL 재시작 완료 (30초 이내)
  • repl_user 생성 완료 (REPLICATION 권한)
  • pg_hba.conf 설정 완료 ( 허용)
  • pg_reload_conf() 반영 완료

8.7.3. RDS

  • 스키마 적용 완료 (테이블 목록 V1과 일치)
  • RDS 재시작 완료 (rds.logical_replication in-sync)
  • RDS 연결 테스트 완료

8.7.4. 연결 테스트

  • V1 → RDS 연결 테스트
  • RDS → V1 연결 테스트 (복제 유저)
# V1 EC2에서 RDS 연결 테스트
psql \
    -h devths-v2-prod-rds.<rds-endpoint> \
    -U devths \
    -d devths \
    -c "SELECT version();"
# RDS에서 V1 연결 테스트는 아래처럼 확인 가능
# (Subscription 생성 시 자동으로 연결 시도하므로 사전 확인 용도)
# V1 EC2에서 repl_user 로컬 접속 테스트
sudo -u postgres psql \
    -U repl_user \
    -d devths \
    -c "SELECT COUNT(*) FROM pg_tables WHERE schemaname = 'public';"

9. 단계별 실행 계획

9.1. 전체 타임라인

일정 Phase 작업 소요시간 다운타임
D-3일 02:00 Phase 0 WAL 레벨 변경 + PostgreSQL 재시작 ~5분 30초 이내
D-2일 Phase 1 복제 전용 유저 생성 + pg_hba.conf + 스키마 적용 ~1시간 0초
D-1일 Phase 2 Publication 생성 + RDS 재시작 + Subscription 생성 ~30분 0초
초기 데이터 자동 동기화 시작 (copy_data = true) ~10분 0초

9.2. Phase 0: WAL 레벨 변경 (D-3일)

# [Step 1] 활성 트랜잭션 확인 (없어야 진행)
sudo -u postgres psql -d devths -c "
SELECT pid, usename, state,
       now() - query_start AS duration
FROM pg_stat_activity
WHERE state != 'idle'
AND query_start IS NOT NULL;"
# [Step 2] pg_dump 백업 (복구 포인트 확보)
sudo -u postgres pg_dump \
    -d devths \
    -F c \
    -f "/tmp/devths_pre_migration_$(date +%Y%m%d%H%M%S).dump"

ls -lh /tmp/devths_pre_migration_*.dump
# [Step 3] wal_level 변경
sudo sed -i "s/wal_level = replica/wal_level = logical/" \
    /etc/postgresql/14/main/postgresql.conf

# 변경 확인
grep "wal_level" /etc/postgresql/14/main/postgresql.conf
# 결과: wal_level = logical
# [Step 4] PostgreSQL 재시작 (30초 이내)
sudo systemctl restart postgresql

# [Step 5] 변경 확인
sudo -u postgres psql -c "SHOW wal_level;"
# 결과: logical ✅
# [Step 6] 서비스 정상 확인
curl -s -o /dev/null -w "%{http_code}" https://api.devths.com/actuator/health
# 결과: 200 ✅

9.3. Phase 1: 사전 준비 (D-2일)

# [Step 1] 복제 전용 유저 생성
sudo -u postgres psql -d devths -c "
CREATE USER repl_user WITH REPLICATION LOGIN PASSWORD '<repl-password>';
GRANT CONNECT ON DATABASE devths TO repl_user;
GRANT USAGE ON SCHEMA public TO repl_user;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO repl_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
    GRANT SELECT ON TABLES TO repl_user;"
# [Step 2] 유저 생성 확인
sudo -u postgres psql -c "
SELECT usename, userepl
FROM pg_user
WHERE usename = 'repl_user';"
# userepl: t ✅
# [Step 3] pg_hba.conf 설정
sudo tee -a /etc/postgresql/14/main/pg_hba.conf << 'EOF'

# Logical Replication - RDS Subscriber 허용 (devths-V2-prod-vpc)
host    replication     repl_user       <v2-vpc-cidr>           scram-sha-256
host    devths          repl_user       <v2-vpc-cidr>           scram-sha-256
EOF
# [Step 4] pg_hba.conf 반영
sudo -u postgres psql -c "SELECT pg_reload_conf();"

# 반영 확인
sudo -u postgres psql -c "
SELECT type, database, user_name, address, auth_method
FROM pg_hba_file_rules
WHERE user_name && ARRAY['repl_user'];"
# [Step 5] 스키마 추출 (V1)
sudo -u postgres pg_dump \
    -d devths \
    --schema-only \
    -F p \
    -f "/tmp/devths_schema_$(date +%Y%m%d%H%M%S).sql"

ls -lh /tmp/devths_schema_*.sql
# [Step 6] RDS에 스키마 적용
psql \
    -h devths-v2-prod-rds.<rds-endpoint> \
    -U devths \
    -d devths \
    -f /tmp/devths_schema_*.sql
-- [Step 7] 스키마 적용 확인 (RDS에서 실행)
SELECT tablename
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY tablename;
-- V1과 동일한 테이블 목록 확인 ✅
# [Step 8] V1 → RDS 연결 테스트
psql \
    -h devths-v2-prod-rds.<rds-endpoint> \
    -U devths \
    -d devths \
    -c "SELECT version();"
# PostgreSQL 14.17 확인 ✅

9.4. Phase 2: 복제 시작 (D-1일)

# [Step 1] RDS 재시작 (rds.logical_replication 적용)
aws rds reboot-db-instance \
    --db-instance-identifier devths-v2-prod-rds \
    --region ap-northeast-2
# [Step 2] RDS 재시작 완료 확인
aws rds describe-db-instances \
    --db-instance-identifier devths-v2-prod-rds \
    --query 'DBInstances[*].{Status:DBInstanceStatus,ParameterApplyStatus:DBParameterGroups[0].ParameterApplyStatus}' \
    --output table \
    --region ap-northeast-2
# Status: available, ParameterApplyStatus: in-sync ✅
-- [Step 3] RDS logical_replication 설정 확인
SHOW rds.logical_replication;
-- 결과: on ✅
-- [Step 4] Publication 생성 (V1에서 실행)
-- sudo -u postgres psql -d devths
CREATE PUBLICATION devths_pub FOR ALL TABLES;

-- 확인
SELECT pubname, puballtables, pubinsert, pubupdate, pubdelete
FROM pg_publication;
--    pubname   | puballtables | pubinsert | pubupdate | pubdelete
-- -------------+--------------+-----------+-----------+-----------
--  devths_pub  | t            | t         | t         | t    ✅
-- [Step 5] Subscription 생성 (RDS에서 실행)
GRANT rds_superuser TO devths;

CREATE SUBSCRIPTION devths_sub
CONNECTION 'host=<v1-ec2-private-ip> port=5432 dbname=devths user=repl_user password=<repl-password>'
PUBLICATION devths_pub
WITH (copy_data = true);
-- copy_data = true: 초기 데이터 자동 복사 시작
-- [Step 6] Subscription 생성 확인
SELECT subname, subenabled, subslotname
FROM pg_subscription;
--   subname    | subenabled | subslotname
-- -------------+------------+-------------
--  devths_sub  | t          | devths_sub  ✅
-- [Step 7] 초기 동기화 상태 확인 (RDS에서 실행)
SELECT subname,
       relid::regclass AS table_name,
       srsubstate
FROM pg_subscription_rel
JOIN pg_subscription ON pg_subscription.oid = pg_subscription_rel.srsubid
ORDER BY table_name;
-- srsubstate 상태값
-- 'i': 초기화 중
-- 'd': 데이터 복사 중
-- 'r': 동기화 완료 ✅
-- [Step 8] 복제 슬롯 생성 확인 (V1에서 실행)
SELECT slot_name, plugin, slot_type, active
FROM pg_replication_slots;
--   slot_name  |  plugin  | slot_type | active
-- -------------+----------+-----------+--------
--  devths_sub  | pgoutput | logical   | t     ✅

9.5. Phase 3: 애플리케이션 1차 컷오버 및 데이터 검증 (D-Day 오전)

9.5.1. [1차 컷오버: V2 환경으로 트래픽 전환]

  1. V2 환경(BE, AI, FE EC2) 인프라 배포를 완료한다. (이때, Spring Boot HikariCP 설정은 V1 DB IP를 유지)
  2. ALB 타겟 그룹 스위칭: 타겟 그룹을 V1에서 V2 EC2로 스위칭(무중단 트래픽 전환)한다. 헬스체크 정상 여부를 확인한다.
  3. 실트래픽 환경하에서 V2 신규 애플리케이션 환경의 안정성을 검증하며 운영한다.

⚠️ 주의 (DDL 동결): 이 시점(1차 컷오버 완료)부터 2차 컷오버(DB 전환)가 끝날 때까지, V1 DB에 스키마 변경(ADD COLUMN 등 구조 변경)을 발생시키는 릴리즈/배포는 절대 금지된다.

9.5.2. [k6 부하 주입 시작 및 발생한 이슈 해결]

이 Phase 시작 시점에 k6 부하를 ON합니다. 최초 migration_load.js 작성 시 POST 요청의 title 에 VU(Virtual User)와 Iteration 번호를 삽입하여 부하 트래픽임을 명확히 식별할 수 있도록 구성했습니다.

// migration_load.js 내 POST payload 예시
const payload = JSON.stringify({
  title: `마이그레이션 부하 테스트 VU${__VU}-#${__ITER}`,
  content: "마이그레이션 성능 테스트를 위한 게시글입니다.",
});

터미널을 2개로 분리해서 병렬 진행합니다.

터미널 1 (부하 주입) k6 run -e TOKEN="<BEER_TOKEN>" migration_load.js → stages: 0→5VU(2m) → 20VU(5m) → 20VU(999m, 컷오버까지 유지)

터미널 2 (Lag 모니터링) bash lag_monitor.sh → 5초 간격으로 Lag 실시간 출력

🚨 트러블슈팅 (Rate Limit 이슈)

  • 현상: 부하 주입 중 API 환경의 Redis 기반 Rate Limiting(ex. 1분당 100건)에 도달하여 HTTP 429 에러가 다량 발생했습니다.
  • 조치: V2 Redis 인스턴스의 Rate Limit Key(rate-limit:board:*)를 주기적으로 날려주는 쉘 스크립트(reset_rl.sh)를 작성하여 별도의 터미널(터미널 3)에서 1초 주기로 백그라운드 실행하도록 하여 해결했습니다.
  • 재시도: 이전 부하 데이터를 일괄 DELETE 쿼리로 정리한 뒤, Rate Limit 우회 스크립트와 함께 k6 부하를 재개하여 안정적인 0% 에러율을 유지한 채 컷오버를 진행했습니다.
# reset_rl.sh
#!/bin/bash
while true; do
  redis-cli -h <redis-endpoint> -p 6379 KEYS "rate-limit:board:*" | xargs -r redis-cli -h <redis-endpoint> -p 6379 DEL
  sleep 1
done

k6 확인 포인트:

  • 트래픽이 있는 상태에서 CDC Lag이 0으로 수렴하는지
  • k6 에러율 0% 유지 여부 (thresholds: http_req_failed < 1%)
  • V1 DB 커넥션 수 max 20 미만 유지 여부
-- [검증 1] 테이블별 Row Count 비교
-- V1과 RDS 양쪽에서 실행 후 결과 대조
SELECT schemaname, tablename, n_live_tup AS row_count
FROM pg_stat_user_tables
ORDER BY tablename;
-- [검증 2] Checksum 비교
-- V1과 RDS 양쪽에서 실행 후 결과 대조
SELECT
    tablename,
    md5(string_agg(t::text, ',' ORDER BY ctid)) AS checksum
FROM (
    SELECT tablename, tableoid::regclass t
    FROM pg_tables
    WHERE schemaname = 'public'
) sub
GROUP BY tablename;
-- [검증 3] 시퀀스 목록 비교
SELECT sequencename, last_value
FROM pg_sequences
ORDER BY sequencename;
# [검증 4] Lag 확인 (V1에서 실행)
# 컷오버 진입 전 반드시 0 확인
sudo -u postgres psql -d devths -c "
SELECT
    application_name,
    client_addr,
    state,
    pg_size_pretty(pg_wal_lsn_diff(
        pg_current_wal_lsn(), replay_lsn
    )) AS total_lag
FROM pg_stat_replication;"
# total_lag: 0 bytes ✅

9.6. Phase 4: DB 최종 컷오버 준비 - RDS 재초기화 (D-Day 심야)

2차 컷오버 직전, 확실한 정합성과 그간의 DDL 반영 여부를 깔끔하게 정리하기 위해 약 10MB 크기의 RDS 데이터를 완전히 비우고 최신 상태로 재구독한다.

-- [Step 1] 기존 Subscription 삭제 (RDS에서 실행)
DROP SUBSCRIPTION IF EXISTS devths_sub;

-- RDS 스키마 완전 초기화
DROP SCHEMA public CASCADE;
CREATE SCHEMA public;
GRANT ALL ON SCHEMA public TO devths;
# [Step 2] V1 EC2에서 최신 스키마 재추출 및 바로 반영
sudo -u postgres pg_dump -d devths --schema-only -F p -f "/tmp/devths_schema_final.sql"
psql -h devths-v2-prod-rds.<rds-endpoint> -U devths -d devths -f /tmp/devths_schema_final.sql
-- [Step 3] 새 Subscription 구성 및 데이터 완전 재동기화 (RDS에서 실행)
CREATE SUBSCRIPTION devths_sub
CONNECTION 'host=<v1-ec2-private-ip> port=5432 dbname=devths user=repl_user password=<repl-password>'
PUBLICATION devths_pub
WITH (copy_data = true);

9.7. Phase 5: 최종 DB 컷오버 (D-Day 02:00)

# [Step 1] 재동기화 후 Lag 0 수렴 확인 (Phase 5 컷오버 진입 조건)
while true; do
    LAG=$(sudo -u postgres psql -t -d devths -c "
        SELECT pg_wal_lsn_diff(
            pg_current_wal_lsn(), replay_lsn
        )
        FROM pg_stat_replication
        WHERE application_name = 'devths_sub';
    ")
    echo "$(date '+%H:%M:%S') Lag: ${LAG} bytes"
    if [ "${LAG// /}" -eq 0 ]; then
        echo "✅ Lag 0 확인 - 컷오버 진행"
        break
    fi
    sleep 5
done
-- [k6 확인] Nginx 스위칭 시 에러율 스파이크 없이 정상 전환 (예상된 동작)
-- [Step 2] Lock-Free 마이그레이션이므로 V1 DB 쓰기 차단(Read-Only)을 수행하지 않음
-- [Step 3] 시퀀스 동기화 (RDS에서 실행)
DO $$
DECLARE
    seq_record RECORD;
    max_val BIGINT;
    seq_query TEXT;
BEGIN
    FOR seq_record IN
        SELECT
            n.nspname AS schema_name,
            s.relname AS seq_name,
            t.relname AS table_name,
            a.attname AS col_name
        FROM pg_class s
        JOIN pg_namespace n ON n.oid = s.relnamespace
        JOIN pg_depend d ON d.objid = s.oid
        JOIN pg_class t ON t.oid = d.refobjid
        JOIN pg_attribute a ON a.attrelid = t.oid
            AND a.attnum = d.refobjsubid
        WHERE s.relkind = 'S'
    LOOP
        seq_query := format(
            'SELECT COALESCE(MAX(%I), 0) FROM %I.%I',
            seq_record.col_name,
            seq_record.schema_name,
            seq_record.table_name
        );
        EXECUTE seq_query INTO max_val;

        PERFORM setval(
            format('%I.%I',
                seq_record.schema_name,
                seq_record.seq_name),
            max_val + 1000
        );

        RAISE NOTICE '✅ 시퀀스 동기화: %.% → %',
            seq_record.table_name,
            seq_record.col_name,
            max_val + 1000;
    END LOOP;
END $$;

🚨 트러블슈팅 및 실제 진행 결과 (시퀀스 버퍼 확대)

  • 현상/원인: CodeDeploy 배포 소요 시간(약 5분) 및 잠재적인 부하 트래픽 중 인서트 충돌을 더 여유 있게 방지하기 위해 버퍼를 늘릴 필요가 있었습니다.
  • 조치: 계획된 +1000 대신 +20000 의 넉넉한 버퍼를 부여하는 최적화된 스크립트로 1차 시퀀스 동기화를 수행했습니다.
DO $$
DECLARE
    seq_record RECORD; max_val BIGINT; seq_query TEXT;
BEGIN
    FOR seq_record IN SELECT n.nspname AS schema_name, s.relname AS seq_name, t.relname AS table_name, a.attname AS col_name FROM pg_class s JOIN pg_namespace n ON n.oid = s.relnamespace JOIN pg_depend d ON d.objid = s.oid JOIN pg_class t ON t.oid = d.refobjid JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = d.refobjsubid WHERE s.relkind = 'S'
    LOOP
        seq_query := format('SELECT COALESCE(MAX(%I), 0) FROM %I.%I', seq_record.col_name, seq_record.schema_name, seq_record.table_name);
        EXECUTE seq_query INTO max_val;
        PERFORM setval(format('%I.%I', seq_record.schema_name, seq_record.seq_name), max_val + 20000);
        RAISE NOTICE '✅ 시퀀스 1차 버퍼링: %.% → %', seq_record.table_name, seq_record.col_name, max_val + 20000;
    END LOOP;
END $$;
-- [Step 4] 최종 Lag 0 재확인 (V1에서 실행)
SELECT pg_wal_lsn_diff(
    pg_current_wal_lsn(), replay_lsn
) AS final_lag_bytes
FROM pg_stat_replication
WHERE application_name = 'devths_sub';
-- final_lag_bytes: 0 ✅
# [핵심 검증] ALB 타겟 전환 순간 k6 에러율 스파이크 여부 주시
# [Step 5] ALB Listener Rule 변경을 통한 트래픽 스위칭 (콘솔/CLI)
# 컷오버 진행 시 타겟 그룹 V1 -> V2 로 즉시 전환
# [k6 에러율 동시 확인] 헬스체크 200 + k6 에러율 < 1% = 컷오버 성공
# [Step 6] 헬스체크 확인
curl -s https://api.devths.com/actuator/health
# {"status":"UP"} ✅

-- [Step 7] V1 DB 최종 Lag 0 확인 스크립트 대기 (롤백 대기 상태 유지)

🚨 트러블슈팅 및 실제 진행 결과 (배포 방식 변경 및 헬스체크 에러 처리)

  • 원본 계획은 애플리케이션 선 전환 후 ALB 라우팅 변경 및 V1 DB 락 설정 방식이었으나, 애플리케이션 내의 DB_URL_V2 파라미터 스토어 값을 V1에서 RDS로 스위칭하고 CodeDeploy로 애플리케이션을 무중단(Rolling) 재시작하는 방식으로 컷오버를 진행했습니다.
  • 롤백 및 재배포: 최초 CodeDeploy 배포 시, V2 BE의 scripts/health.sh 내 응답 체크 로직이 grep -c를 사용하면서 빈 응답에 대해 integer expression expected bash 에러가 발생하여 배포가 중단/롤백되었습니다.
    • 조치: health.sh의 비교문을 정규식 매칭 grep -qE로 수정하여 에러 저항성을 높인 후, fix/cicd 브랜치에 푸시 & main 병합하여 최종 배포에 성공했습니다.

🚨 트러블슈팅 및 실제 진행 결과 (시퀀스 2차 갭 최적화)

  • +20000 버퍼 부여 후 데이터에 갭이 생기는 것을 해결하기 위해 배포 직후 2차 동기화를 진행했습니다.
  • 초기 스크립트는 데이터가 없는 빈 테이블에서 MAX값이 0이 되어 value 0 is out of bounds 에러가 발생했습니다.
    • 조치: max_val = 0 일 경우 예외적으로 시퀀스를 setval(..., 1, false) 로 초기화하는 스크립트로 보완하여 모든 테이블의 시퀀스를 완벽히 일치시켰습니다.
DO $$
DECLARE
    seq_record RECORD; max_val BIGINT; seq_query TEXT;
BEGIN
    FOR seq_record IN SELECT n.nspname AS schema_name, s.relname AS seq_name, t.relname AS table_name, a.attname AS col_name FROM pg_class s JOIN pg_namespace n ON n.oid = s.relnamespace JOIN pg_depend d ON d.objid = s.oid JOIN pg_class t ON t.oid = d.refobjid JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = d.refobjsubid WHERE s.relkind = 'S'
    LOOP
        seq_query := format('SELECT COALESCE(MAX(%I), 0) FROM %I.%I', seq_record.col_name, seq_record.schema_name, seq_record.table_name);
        EXECUTE seq_query INTO max_val;
        IF max_val = 0 THEN
            PERFORM setval(format('%I.%I', seq_record.schema_name, seq_record.seq_name), 1, false);
        ELSE
            PERFORM setval(format('%I.%I', seq_record.schema_name, seq_record.seq_name), max_val);
        END IF;
    END LOOP;
END $$;

9.8. Phase 6: 사후 정리 (D+1 ~ D+3)

최종 컷오버 직후 k6를 10분 추가 유지하며 RDS 마이그레이션이 성공적인지 검증. 정상 처리 검증 완료 후 k6를 종료하고 이후 정리를 진행.

9.8.1. [k6 추가 검증 (10분) 및 종료]

컷오버 완료 후 k6를 10분 추가 유지합니다.

확인 포인트:

  • 컷오버 전/후 p95 응답시간 비교 (목표: V1 대비 110% 이내)
  • WRITE 요청에서 PK 충돌 발생 여부 (목표: 0건)
  • 컷오버 순간 에러율 스파이크 발생 여부

10분 후 k6 종료 (터미널 1에서 Ctrl+C 또는 ramp-down 자동 진행)

k6 결과 리포트 저장: k6 run --out json=/tmp/migration_k6_result.json migration_load.js → 저장 항목: 에러율, p50/p95 응답시간, PK 충돌 건수

-- [Step 1] Subscription 삭제 (RDS에서 실행)
DROP SUBSCRIPTION devths_sub;

-- 확인
SELECT subname FROM pg_subscription;
-- 0 rows ✅
-- [Step 2] Publication 삭제 (V1에서 실행)
DROP PUBLICATION devths_pub;

-- 확인
SELECT pubname FROM pg_publication;
-- 0 rows ✅
# [Step 3] V1 Security Group 5432 규칙 삭제
aws ec2 revoke-security-group-ingress \
    --group-id <v1-sg-id> \
    --protocol tcp \
    --port 5432 \
    --cidr <v2-vpc-cidr> \
    --region ap-northeast-2
# [Step 4] repl_user 삭제 (V1에서 실행)
sudo -u postgres psql -c "DROP USER repl_user;"

🚨 트러블슈팅 및 실제 진행 결과 (권한 종속성으로 인한 계정 삭제 불가)

  • 현상: 단순 DROP USER repl_user; 명령 실행 시, 해당 계정이 가지고 있는 테이블 권한 및 기본 소유권으로 인해 오류(role "repl_user" cannot be dropped because some objects depend on it)가 발생하여 삭제가 거부되었습니다.
  • 조치: 아래 스크립트를 통해 스키마 전체에서 repl_user의 모든 권한을 강제 회수하고 잔여 소유권을 postgres로 이관한 뒤 계정을 삭제했습니다.
sudo -u postgres psql -d devths << 'EOF'
REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM repl_user;
REVOKE ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public FROM repl_user;
REVOKE ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public FROM repl_user;
REVOKE ALL PRIVILEGES ON SCHEMA public FROM repl_user;
REVOKE ALL PRIVILEGES ON DATABASE devths FROM repl_user;

ALTER DEFAULT PRIVILEGES IN SCHEMA public REVOKE ALL ON TABLES FROM repl_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public REVOKE ALL ON SEQUENCES FROM repl_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM repl_user;

REASSIGN OWNED BY repl_user TO postgres;
DROP OWNED BY repl_user;

DROP USER repl_user;
EOF
# [Step 5] 백업 파일 정리
rm /tmp/devths_pre_migration_*.dump
rm /tmp/devths_schema_*.sql

9.9. 사후 정리 체크리스트

  • 72시간 모니터링 이상 없음 확인
  • Subscription 삭제 완료 (RDS)
  • Publication 삭제 완료 (V1)
  • V1 Security Group 5432 규칙 삭제 완료
  • repl_user 삭제 완료
  • 백업 파일 정리 완료
  • V1 PostgreSQL 종료 여부 결정

10. 데이터 검증 계획

10.1. Row Count 검증

-- V1과 RDS 양쪽에서 실행 후 결과 대조
SELECT
    schemaname,
    tablename,
    n_live_tup AS row_count
FROM pg_stat_user_tables
ORDER BY tablename;

pg_stat_user_tables는 통계 기반이라 실시간 정확도가 낮을 수 있음. 정확한 검증이 필요한 테이블은 아래 쿼리로 추가 확인 진행.

-- 테이블별 정확한 COUNT 확인
SELECT
    'users' AS tablename, COUNT(*) AS row_count FROM users
UNION ALL
SELECT 'posts', COUNT(*) FROM posts
UNION ALL
SELECT 'comments', COUNT(*) FROM comments;
-- 테이블명은 실제 devths 테이블 목록으로 교체
결과 판단 조치
V1 = RDS ✅ 정상 다음 검증 진행
V1 ≠ RDS ❌ 이상 복제 상태 확인 후 원인 파악 및 재동기화

10.2. Checksum 검증

-- V1과 RDS 양쪽에서 실행 후 checksum 대조
SELECT
    tablename,
    md5(string_agg(t::text, ',' ORDER BY ctid)) AS checksum
FROM (
    SELECT tablename, tableoid::regclass t
    FROM pg_tables
    WHERE schemaname = 'public'
) sub
GROUP BY tablename
ORDER BY tablename;

주의사항

  • Logical Replication 진행 중에는 V1에 변경이 계속 발생하므로 Checksum은 Lock-Free 상태에서도 Lag가 0에 수렴하는 한 동기화된 것으로 간주.
  • 컷오버 직전 Checksum 비교 → 일치 확인 → ALB(로드밸런서) 타겟 그룹 스위칭 순서로 진행.
결과 판단 조치
V1 = RDS (전 테이블) ✅ 정상 컷오버 진행
일부 테이블 불일치 ❌ 이상 해당 테이블 Row Count 재확인 및 원인 파악

10.3. 시퀀스 동기화 검증

Logical Replication은 시퀀스를 자동으로 복제하지 않음. 컷오버 직전 수동 동기화 이후 검증 진행

-- [검증 전] V1과 RDS 시퀀스 현재값 비교
-- V1과 RDS 양쪽에서 실행
SELECT
    sequencename,
    last_value,
    increment_by
FROM pg_sequences
ORDER BY sequencename;
-- [동기화] RDS에서 실행 (컷오버 직전)
DO $$
DECLARE
    seq_record RECORD;
    max_val BIGINT;
    seq_query TEXT;
BEGIN
    FOR seq_record IN
        SELECT
            n.nspname AS schema_name,
            s.relname AS seq_name,
            t.relname AS table_name,
            a.attname AS col_name
        FROM pg_class s
        JOIN pg_namespace n ON n.oid = s.relnamespace
        JOIN pg_depend d ON d.objid = s.oid
        JOIN pg_class t ON t.oid = d.refobjid
        JOIN pg_attribute a ON a.attrelid = t.oid
            AND a.attnum = d.refobjsubid
        WHERE s.relkind = 'S'
    LOOP
        seq_query := format(
            'SELECT COALESCE(MAX(%I), 0) FROM %I.%I',
            seq_record.col_name,
            seq_record.schema_name,
            seq_record.table_name
        );
        EXECUTE seq_query INTO max_val;

        PERFORM setval(
            format('%I.%I',
                seq_record.schema_name,
                seq_record.seq_name),
            max_val + 1000
        );

        RAISE NOTICE '✅ 시퀀스 동기화: %.% → %',
            seq_record.table_name,
            seq_record.col_name,
            max_val + 1000;
    END LOOP;
END $$;
-- [검증 후] 동기화 완료 확인
SELECT
    sequencename,
    last_value
FROM pg_sequences
ORDER BY sequencename;
-- RDS의 last_value >= V1의 last_value + 1000 ✅
항목 기준 판단
RDS 시퀀스 last_value V1 MAX(PK) + 1000 이상 ✅ 정상
컷오버 후 INSERT 시 PK 충돌 발생하지 않아야 함 ✅ 정상

10.4. 애플리케이션 레벨 검증

DB 레벨 검증 외에 실제 API 응답으로 데이터 정합성을 확인.

# [검증 1] 주요 API 응답 확인
# 컷오버 전 V1 기준 응답 저장
curl -s https://api.devths.com/api/v1/posts?page=0&size=10 \
    -H "Authorization: Bearer ${TOKEN}" \
    | jq '.' > /tmp/v1_posts_response.json

# 컷오버 후 RDS 기준 응답 저장
curl -s https://api.devths.com/api/v1/posts?page=0&size=10 \
    -H "Authorization: Bearer ${TOKEN}" \
    | jq '.' > /tmp/v2_posts_response.json

# 응답 비교
diff /tmp/v1_posts_response.json /tmp/v2_posts_response.json
# 차이 없으면 정상 ✅
# [검증 2] 헬스체크 확인
curl -s https://api.devths.com/actuator/health
# {"status":"UP"} ✅
# [검증 3] 컷오버 후 신규 데이터 쓰기 테스트
# INSERT 후 SELECT로 정상 확인
curl -s -X POST https://api.devths.com/api/v1/posts \
    -H "Authorization: Bearer ${TOKEN}" \
    -H "Content-Type: application/json" \
    -d '{"title":"마이그레이션 테스트","content":"정상 동작 확인"}' \
    | jq '.id'
# id 정상 반환 확인 (시퀀스 충돌 없음) ✅
검증 항목 기준 판단
주요 API 응답 V1 응답과 동일 ✅ 정상
헬스체크 {"status":"UP"} ✅ 정상
신규 데이터 INSERT PK 충돌 없이 정상 저장 ✅ 정상
에러율 5% 미만 ✅ 정상
응답시간 V1 대비 200% 이내 ✅ 정상

11. 컷오버 계획

11.1. 컷오버 조건

컷오버는 아래 조건이 모두 충족된 경우에만 진행.

조건 확인 방법 기준
Lag 0 확인 pg_stat_replication.replay_lsn 0 bytes
데이터 검증 완료 Row Count + Checksum 일치 100% 일치
RDS 연결 테스트 완료 psql -h RDS_ENDPOINT 연결 성공
시퀀스 동기화 완료 pg_sequences last_value 확인 MAX + 1000
서비스 헬스체크 정상 /actuator/health {"status":"UP"}
새벽 시간대 확인 트래픽 최저 구간 02:00 ~ 04:00

하나라도 충족되지 않으면 컷오버 중단 후 원인 파악 및 일정 재조정.

11.2. HikariCP 재연결 처리

컷오버 시 Spring Boot의 HikariCP가 기존 V1 연결을 끊고 RDS로 재연결하는 과정.

[컷오버 시점]
        │
        ▼
Spring Boot 재시작 (RDS Endpoint로 변경된 설정 적용)
        │
        ▼
HikariCP 커넥션 풀 초기화
  - minimum-idle: 5개 즉시 연결 시도
  - connection-timeout: 30,000ms 이내 연결 성공 필요
        │
        ├── 연결 성공 → 서비스 정상 복구 ✅
        └── connection-timeout 초과 → 재시도
              └── 30초 이내 미복구 시 롤백 트리거

현재 HikariCP 설정 기준 재연결 타임라인

항목 컷오버 영향
connection-timeout 30,000ms 30초 이내 RDS 연결 성공 필요
minimum-idle 5 재시작 시 5개 커넥션 즉시 생성 시도
maximum-pool-size 20 최대 20개까지 확장
keepalive-time 30,000ms 유휴 커넥션 유지로 재연결 지연 방지
initialization-fail-timeout 0 초기 연결 실패 시 즉시 종료하지 않음

RDS 연결 설정 (컷오버 시 적용)

spring:
  datasource:
    url: jdbc:postgresql://devths-v2-prod-rds.<rds-endpoint>:5432/devths
    username: devths
    password: ${DB_PASSWORD}
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      connection-timeout: 30000
      idle-timeout: 600000
      keepalive-time: 30000
      initialization-fail-timeout: 0

11.3. Spring Boot DB 연결 전환 (ALB Blue-Green 기반)

V1(단일 EC2)에서 V2(신규 EC2 + RDS)로 트래픽을 넘기기 위해 Nginx 설정 대신 AWS ALB(Application Load Balancer) 규칙 또는 타겟 그룹 가정을 조정하는 진정한 인프라 레벨의 Blue-Green 전환을 사용함.

11.3.1. 전환 흐름

현재 상태 (V1)
  Route53 → ALB → V1 EC2 타겟 그룹 (Nginx 80 → Spring Boot 8080 : V1 DB)

전환 후 (V2)
  Route53 → ALB → V2 EC2 타겟 그룹 (Nginx 80 → Spring Boot 8000 : RDS DB)

11.3.2. Step 1. V2 전용 신규 EC2 배포 및 RDS 연결 확인

# 1. 컷오버 전 V2 전용 EC2 인스턴스들(ASG 또는 직접 생성) 배포 완료
# 2. V2 인스턴스의 Spring Boot는 내부적으로 RDS를 바라보도록 환경변수 설정 적용 완료
# 3. ALB의 V2 타겟 그룹(Target Group)에 V2 EC2들이 Healthy 상태로 등록되어 있음

11.3.3. Step 2. 새 인스턴스 타겟 그룹 헬스체크 확인

# 외부에서 V2 타겟 그룹에 직접 바인딩된 임시 포트나 ALB 룰로 헬스체크 확인
curl -s http://V2_EC2_IP:8000/actuator/health
# {"status":"UP"} ✅

11.3.4. Step 3. ALB 트래픽 스위칭 (무중단)

# AWS Console 또는 CLI를 통해 ALB Listener Rule을 변경
# api.devths.com 접근 트래픽을 V1 타겟 그룹(100%)에서 V2 타겟 그룹(100%)으로 변경
aws elbv2 modify-listener \
    --listener-arn arn:aws:elasticloadbalancing:... \
    --default-actions Type=forward,TargetGroupArn=arn:aws:elasticloadbalancing:...V2-TG...

11.3.5. Step 4. 전환 확인

# 외부 헬스체크 및 실제 유저 API 데이터 정상 요청 응답 확인
curl -s https://api.devths.com/actuator/health
# {"status":"UP"} ✅

# V2 EC2 인스턴스 내부의 로그에서 정상 트래픽 인입 확인
docker logs -f ai-service

11.3.6. Step 5. 기존 V1 EC2 연결 Draining 여부 확인 보장 (수동)

# ALB 스위칭 직후 기존 V1 EC2에 묶여있던 진행 중인 트랜잭션이 종료될 때까지 대기
# ALB Target Group의 최소 Deregistration Delay 시간(기본 300초 또는 60초) 대기 권장
echo "⏳ 기존 V1 연결 상태 Draining 대기 (60s 이상 권장)..."

# 작업자는 즉시 V1을 종료하지 않고, 아래 2가지를 명확히 수동 검증한 후 명시적으로 종료(Kill)함.
# 1. V1 EC2의 애플리케이션 로그에 잔여 트랜잭션이 없는지 확인
# 2. V1 DB -> RDS 로의 논리적 복제 Lag가 완벽히 0 Bytes 인지 `lag_monitor.sh` 로 3회 이상 확인

11.3.7. Step 6. 보존용 V1 DB 쓰기 완전 차단 (Ghost Write 방지)

-- V1 EC2에서 psql 접속 후 실행
-- 컷오버가 완벽히 스위칭되고 잔류 트랜잭션이 전혀 없는 것을 확인한 직후 적용
-- 롤백 대기 2~3일간 외부 배치/스크립트 등 오염된 쓰기 작업(Ghost Write)이 진입하는 것을 원천 차단
ALTER DATABASE devths SET default_transaction_read_only = on;

-- 확인
SHOW default_transaction_read_only;
-- 결과: on ✅

11.3.8. Step 7. 데이터 복제(Subscription) 임시 중단

-- V2 RDS에서 psql 접속 후 실행
-- V1 DB에 락을 걸었다 하더라도, 롤백 발생 시 V1에 쓰인 데이터가 
-- 엉망이 된 채로 V2로 복제되는 것을 방지하기 위해 파이프라인 단절
ALTER SUBSCRIPTION devths_sub DISABLE;

-- 확인
SELECT subname, subenabled FROM pg_subscription;
-- subenabled: f ✅

11.4. 컷오버 완료 확인 항목

DB 연결 확인

  • Spring Boot → RDS 연결 정상 (HikariCP 커넥션 풀 active)
  • RDS 커넥션 수 정상 (최대 20개 이내)
  • V1 DB 커넥션 0 (더 이상 연결 없음)

서비스 확인

  • /actuator/health → {"status":"UP"}
  • 주요 API 정상 응답 (GET /api/v1/posts 등)
  • 신규 데이터 INSERT 정상 (PK 충돌 없음)
  • V2 EC2 컨테이너 로그 정상 트래픽 유입 확인

데이터 확인

  • 컷오버 후 Row Count V1 = RDS 일치
  • 시퀀스 충돌 없음 확인
  • 에러율 5% 미만

모니터링 확인

  • CloudWatch RDS CPU 정상 (80% 미만)
  • CloudWatch RDS 커넥션 수 정상
  • Grafana 응답시간 V1 대비 200% 이내
  • V1 DB 전원 휴면 대기 완료 및 쓰기 락(Read-Only) 활성화 완료

12. 롤백 계획

12.1. 롤백 트리거 조건

컷오버 후 아래 조건 중 하나라도 감지되면 즉시 롤백 실행.

즉시 롤백 (감지 즉시 실행)

조건 기준 확인 방법
헬스체크 실패 /actuator/health 비정상 curl 응답 확인
DB 연결 실패 HikariCP 커넥션 풀 고갈 애플리케이션 로그
에러율 급증 5% 초과 Grafana 대시보드
데이터 불일치 감지 Row Count / Checksum 불일치 검증 스크립트
PK 충돌 발생 INSERT 시 시퀀스 충돌 애플리케이션 로그

경고 후 롤백 (10분 내 미해소 시 실행)

조건 기준 확인 방법
응답시간 저하 V1 대비 200% 초과 Grafana 대시보드
RDS CPU 과부하 80% 초과 5분 지속 CloudWatch
RDS 커넥션 수 이상 20개 초과 지속 CloudWatch

롤백 가능 시간은 컷오버 후 V1 DB가 유지되는 D+3일까지.

12.2. 단계별 롤백 절차

롤백 플로우

flowchart TD
    A[이상 감지] --> B{즉시 롤백 조건?}

    B --> |Yes| C[즉시 롤백 실행]
    B --> |No - 경고 조건| D[10분 모니터링]

    D --> |해소됨| E[정상 운영 유지]
    D --> |미해소| C

    C --> F[ALB Listener V1 타겟 그룹으로 원복]
    F --> G[ALB 라우팅 즉시 전환]
    G --> H[기존 V1 EC2 트래픽 유입 재개 확인]
    H --> I[외부 헬스체크 확인]

    I --> |성공| J[롤백 완료 ✅\n소요시간 30초 이내]
    I --> |실패| K[긴급 대응\n온콜 알림]

    J --> L[원인 분석 후\n재마이그레이션 계획]

    style A fill:#FF5722,color:#fff
    style C fill:#FF5722,color:#fff
    style J fill:#4CAF50,color:#fff
    style E fill:#4CAF50,color:#fff
    style K fill:#B71C1C,color:#fff
Loading

12.2.1. Step 1. ALB V1 타겟 그룹 복구

# ALB Listener Rule을 다시 원래 V1 EC2가 묶여있는 대상 그룹으로 복구 (즉각적 트래픽 복원)
# V1 EC2는 컷오버 중에도 인스턴스와 Spring Boot가 계속 런닝 상태였으므로 재부팅 불필요
aws elbv2 modify-listener \
    --listener-arn arn:aws:elasticloadbalancing:... \
    --default-actions Type=forward,TargetGroupArn=arn:aws:elasticloadbalancing:...V1-TG...

12.2.2. Step 2. 롤백 헬스체크 및 트래픽 유입 확인

# 외부 헬스체크 정상화 대기
curl -s https://api.devths.com/actuator/health
# {"status":"UP"} ✅

# 구 V1 인스턴스(기존 V1 DB 연결)의 로그 모니터링
tail -f /home/ubuntu/ai/logs/api.log
# 트래픽이 다시 V1으로 유입되는 것 확인

12.2.3. Step 3. V2 환경(오염된 RDS 연결 인스턴스) 종료

# 롤백 후 트래픽이 완전히 V1으로 우회되었음을 확인하면, 더 이상 쓰이지 않는 V2 인스턴스 종료
# (Auto Scaling Group에 물려있다면 Desired Capacity를 0으로 조절)

12.2.4. Step 6. Subscription 일시 중단 (롤백 후 데이터 혼입 방지)

-- RDS에서 실행
-- V1 DB로 롤백 후 RDS로 계속 복제되면 데이터 혼입 위험
ALTER SUBSCRIPTION devths_sub DISABLE;

-- 확인
SELECT subname, subenabled FROM pg_subscription;
-- subenabled: f ✅

12.3. 롤백 완료 확인 항목

서비스 복구 확인

  • /actuator/health → {"status":"UP"}
  • V1 EC2 로그 트래픽 정상 확인
  • 주요 API 정상 응답 확인
  • 에러율 5% 미만 복구

DB 연결 확인

  • Spring Boot → V1 DB 연결 정상
  • V1 DB default_transaction_read_only = off 확인
  • RDS Subscription 비활성화 완료 (devths_sub disabled)
  • V1 DB 커넥션 정상 (HikariCP active)

롤백 후 조치

  • 롤백 시점 및 원인 기록
  • 롤백 중 V1 DB 변경분 확인 (Subscription 비활성화 구간)
  • 재마이그레이션 일정 재조정
  • 원인 분석 완료 후 재시도 여부 결정

재마이그레이션 준비

롤백 후 재마이그레이션 시 아래 항목 재확인 후 진행.

항목 확인 내용
롤백 원인 해소 동일 문제 재발 방지 조치 완료
RDS Subscription 재활성화 ALTER SUBSCRIPTION devths_sub ENABLE
데이터 재동기화 Subscription 비활성화 구간 변경분 반영 확인
검증 스크립트 재실행 Row Count + Checksum 재확인
컷오버 조건 재충족 10.1 체크리스트 전체 재확인

13. 모니터링 계획

13.1. 마이그레이션 중 모니터링 지표

마이그레이션 전 과정에서 아래 지표를 지속 모니터링.

13.1.1. 애플리케이션 지표

지표 정상 기준 경고 기준 즉시 중단 기준
에러율 1% 미만 5% 초과 20% 초과
p95 응답시간 3초 이내 V1 대비 200% 초과 V1 대비 300% 초과
헬스체크 {"status":"UP"} - 비정상 응답

13.1.2. V1 DB 지표

지표 정상 기준 경고 기준 즉시 중단 기준
CPU 사용률 60% 미만 80% 초과 95% 초과 5분 지속
활성 커넥션 수 15개 이하 18개 초과 20개 (풀 고갈)
Replication Lag 0 bytes 1MB 초과 컷오버 진입 불가
WAL 생성 속도 정상 범위 급증 감지 -

13.1.3. RDS 지표 (CloudWatch)

지표 정상 기준 경고 기준 즉시 중단 기준
CPU 사용률 60% 미만 80% 초과 95% 초과 5분 지속
DatabaseConnections 15개 이하 18개 초과 20개 초과
FreeStorageSpace 10GB 이상 5GB 미만 1GB 미만
ReadLatency / WriteLatency 10ms 이내 50ms 초과 100ms 초과
ReplicaLag 0 1초 초과 컷오버 진입 불가

13.2. Logical Replication Lag 모니터링

13.2.1. V1에서 실시간 Lag 확인

-- 복제 전송 상태 전체 확인
SELECT
    application_name,
    client_addr,
    state,
    pg_size_pretty(pg_wal_lsn_diff(
        pg_current_wal_lsn(), sent_lsn
    )) AS send_lag,
    pg_size_pretty(pg_wal_lsn_diff(
        sent_lsn, write_lsn
    )) AS write_lag,
    pg_size_pretty(pg_wal_lsn_diff(
        write_lsn, replay_lsn
    )) AS replay_lag,
    pg_wal_lsn_diff(
        pg_current_wal_lsn(), replay_lsn
    ) AS total_lag_bytes
FROM pg_stat_replication;

13.2.2. RDS에서 수신 상태 확인

-- Subscription 수신 위치 확인
SELECT
    subname,
    received_lsn,
    latest_end_lsn,
    latest_end_time,
    now() - latest_end_time AS lag_time
FROM pg_stat_subscription;

13.2.3. 자동화 Lag 모니터링 스크립트 (5초 간격)

#!/bin/bash
# lag_monitor.sh
# 실행: bash lag_monitor.sh

LOG_FILE="/tmp/replication_lag_$(date +%Y%m%d).log"
ALERT_THRESHOLD=1048576  # 1MB

echo "Replication Lag 모니터링 시작: $(date)" | tee -a $LOG_FILE

while true; do
    LAG=$(sudo -u postgres psql -t -d devths -c "
        SELECT pg_wal_lsn_diff(
            pg_current_wal_lsn(), replay_lsn
        )
        FROM pg_stat_replication
        WHERE application_name = 'devths_sub';
    " | tr -d ' ')

    TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')

    if [ -z "$LAG" ]; then
        echo "$TIMESTAMP ⚠️  복제 연결 없음 - Subscriber 확인 필요" | tee -a $LOG_FILE
    elif [ "$LAG" -eq 0 ]; then
        echo "$TIMESTAMP ✅ Lag: 0 bytes" | tee -a $LOG_FILE
    elif [ "$LAG" -gt "$ALERT_THRESHOLD" ]; then
        echo "$TIMESTAMP 🚨 Lag 경고: ${LAG} bytes (1MB 초과)" | tee -a $LOG_FILE
    else
        echo "$TIMESTAMP ℹ️  Lag: ${LAG} bytes" | tee -a $LOG_FILE
    fi

    sleep 5
done

13.2.4. 복제 슬롯 상태 확인

-- 복제 슬롯 WAL 누적 확인
-- inactive 상태 지속 시 WAL 파일 무한 누적 위험
SELECT
    slot_name,
    plugin,
    slot_type,
    active,
    pg_size_pretty(pg_wal_lsn_diff(
        pg_current_wal_lsn(),
        confirmed_flush_lsn
    )) AS retained_wal
FROM pg_replication_slots;

복제 슬롯이 inactive 상태로 장시간 유지되면 V1 디스크 고갈 위험. 즉시 원인 파악 후 조치.

# 디스크 사용량 확인
df -h /var/lib/postgresql/14/main/pg_wal/

13.3. 컷오버 후 이상 감지 기준

컷오버 완료 후 72시간 동안 아래 기준으로 이상 감지.

13.3.1. 이상 감지 자동화 스크립트

#!/bin/bash
# post_cutover_monitor.sh
# 실행: bash post_cutover_monitor.sh

HEALTH_URL="https://api.devths.com/actuator/health"
LOG_FILE="/tmp/post_cutover_$(date +%Y%m%d).log"
CHECK_INTERVAL=10

echo "컷오버 후 모니터링 시작: $(date)" | tee -a $LOG_FILE

while true; do
    TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')

    # 헬스체크
    STATUS=$(curl -s -o /dev/null -w "%{http_code}" $HEALTH_URL)

    if [ "$STATUS" != "200" ]; then
        echo "$TIMESTAMP 🚨 헬스체크 실패: HTTP $STATUS → 롤백 트리거" | tee -a $LOG_FILE
        break
    fi

    echo "$TIMESTAMP ✅ 헬스체크 정상: HTTP $STATUS" | tee -a $LOG_FILE
    sleep $CHECK_INTERVAL
done

13.3.2. 단계별 모니터링 기준

시간 모니터링 강도 주요 확인 항목
컷오버 후 1시간 집중 (10초 간격) 헬스체크, 에러율, DB 커넥션
컷오버 후 24시간 일반 (1분 간격) 응답시간, RDS CPU, 에러율
컷오버 후 72시간 유지 (5분 간격) RDS 전반 지표, 이상 징후
72시간 이후 정상 운영 CloudWatch 알람 기반

13.3.3. CloudWatch 알람 설정 (컷오버 후 활성화)

# RDS CPU 80% 초과 알람
aws cloudwatch put-metric-alarm \
    --alarm-name devths-prod-rds-cpu-high \
    --metric-name CPUUtilization \
    --namespace AWS/RDS \
    --dimensions Name=DBInstanceIdentifier,Value=devths-v2-prod-rds \
    --period 300 \
    --evaluation-periods 2 \
    --threshold 80 \
    --comparison-operator GreaterThanThreshold \
    --statistic Average \
    --alarm-actions arn:aws:sns:ap-northeast-2:ACCOUNT_ID:devths-alert \
    --region ap-northeast-2
# RDS 커넥션 수 18개 초과 알람
aws cloudwatch put-metric-alarm \
    --alarm-name devths-prod-rds-connections-high \
    --metric-name DatabaseConnections \
    --namespace AWS/RDS \
    --dimensions Name=DBInstanceIdentifier,Value=devths-v2-prod-rds \
    --period 60 \
    --evaluation-periods 3 \
    --threshold 18 \
    --comparison-operator GreaterThanThreshold \
    --statistic Average \
    --alarm-actions arn:aws:sns:ap-northeast-2:ACCOUNT_ID:devths-alert \
    --region ap-northeast-2
# RDS FreeStorageSpace 5GB 미만 알람
aws cloudwatch put-metric-alarm \
    --alarm-name devths-prod-rds-storage-low \
    --metric-name FreeStorageSpace \
    --namespace AWS/RDS \
    --dimensions Name=DBInstanceIdentifier,Value=devths-v2-prod-rds \
    --period 300 \
    --evaluation-periods 1 \
    --threshold 5368709120 \
    --comparison-operator LessThanThreshold \
    --statistic Average \
    --alarm-actions arn:aws:sns:ap-northeast-2:ACCOUNT_ID:devths-alert \
    --region ap-northeast-2

13.3.4. Grafana PLG 스택 연동 대시보드 확인 항목

패널 지표 경고 임계값
API 에러율 HTTP 5xx 비율 5% 초과
p95 응답시간 Spring Boot 응답시간 3초 초과
DB 커넥션 수 HikariCP active connections 18개 초과
RDS CPU CloudWatch CPUUtilization 80% 초과
JVM Heap Spring Boot Heap 사용률 85% 초과
GC 시간 GC pause time 500ms 초과

14. 비용 계획

14.1. 마이그레이션 비용

Logical Replication은 PostgreSQL 네이티브 기능을 사용하므로 마이그레이션 과정에서 추가 AWS 비용 없음.

14.1.1. DMS 방식 대비 비용 절감

항목 DMS + Shadow EC2 방식 Logical Replication 방식
DMS Replication Instance dms.t3.medium 약 $70/월 없음 ✅
Shadow EC2 t3.medium 약 $30/월 없음 ✅
마이그레이션 기간 비용 약 $100 (1~2일 기준) $0
추가 네트워크 비용 VPC 간 데이터 전송 VPC Peering 데이터 전송만 발생

14.1.2. VPC Peering 데이터 전송 비용

VPC Peering을 통한 데이터 전송은 동일 리전 내 AZ 간 요금 적용.

구간 요금 예상 전송량 예상 비용
V1 VPC → V2 VPC (초기 동기화) $0.01/GB 10MB $0.0001 미만
V1 VPC → V2 VPC (CDC 기간) $0.01/GB 수십 MB $0.001 미만
합계 무시 가능 수준

devths DB 용량이 10MB 수준이므로 데이터 전송 비용은 사실상 무시 가능.

14.2. RDS 운영 비용 산정

14.2.1. devths-v2-prod-rds 월 운영 비용 (ap-northeast-2 기준)

항목 스펙 단가 월 비용
RDS 인스턴스 db.t3.medium, Multi-AZ $0.156/시간 약 $113
스토리지 20GB gp3 $0.138/GB/월 약 $2.8
백업 스토리지 자동 백업 20GB (보존 7일) $0.095/GB/월 약 $1.9
합계 약 $117/월

14.2.2. V1 PostgreSQL 대비 비용 비교

항목 V1 (EC2 내 PostgreSQL) V2 (RDS)
DB 비용 EC2 비용에 포함 (별도 없음) 약 $117/월
운영 부담 높음 (직접 관리) 낮음 (AWS 관리형)
HA 구성 없음 (SPOF) Multi-AZ (자동 Failover)
백업 수동 자동 + PITR 7일
비용 효율 단기적으로 저렴 장기적으로 운영 안정성 확보

비용 최적화 옵션 (향후 검토)

옵션 절감 효과 조건
Reserved Instance 1년 약 40% 절감 → 약 $70/월 1년 약정
Reserved Instance 3년 약 60% 절감 → 약 $47/월 3년 약정
Single-AZ 전환 약 50% 절감 → 약 $58/월 HA 포기

V2 MAU 30만명 목표 달성 시 인스턴스 스펙 업 필요. 트래픽 증가에 따라 db.t3.large 또는 db.r6g.large로 전환 검토.

14.3. 리소스 정리 계획

마이그레이션 완료 후 불필요한 리소스 정리로 비용 최소화.

14.3.1. 정리 대상 리소스

리소스 정리 시점 정리 방법 예상 절감
V1 PostgreSQL (프로세스) D+3일 systemctl stop postgresql EC2 리소스 확보
V1 Security Group 5432 규칙 D+3일 aws ec2 revoke-security-group-ingress 보안 강화
VPC Peering 라우팅 D+3일 aws ec2 delete-route 불필요 라우팅 제거
pg_dump 백업 파일 D+3일 rm /tmp/devths_*.dump 디스크 확보
repl_user D+3일 DROP USER repl_user 보안 강화
devths-prod-postgres-params (구 16버전) 즉시 aws rds delete-db-parameter-group 리소스 정리

14.3.2. 정리 실행 스크립트

# [D+3일] V1 PostgreSQL 중지
sudo systemctl stop postgresql
sudo systemctl disable postgresql

# 중지 확인
sudo systemctl status postgresql
# [D+3일] Security Group 5432 규칙 삭제
aws ec2 revoke-security-group-ingress \
    --group-id <v1-sg-id> \
    --protocol tcp \
    --port 5432 \
    --cidr <v2-vpc-cidr> \
    --region ap-northeast-2
# [D+3일] V1 VPC 라우팅 테이블에서 V2 경로 삭제
aws ec2 delete-route \
    --route-table-id <rtb-id> \
    --destination-cidr-block <v2-vpc-cidr> \
    --region ap-northeast-2
# [D+3일] 백업 파일 정리
rm /tmp/devths_pre_migration_*.dump
rm /tmp/devths_schema_*.sql
-- [D+3일] repl_user 삭제 (V1에서 실행)
DROP USER repl_user;
# [즉시] 구 파라미터 그룹 삭제 (16버전)
aws rds delete-db-parameter-group \
    --db-parameter-group-name devths-prod-postgres-params \
    --region ap-northeast-2

정리 완료 체크리스트

  • V1 PostgreSQL 프로세스 중지 완료
  • V1 PostgreSQL 자동 시작 비활성화 완료
  • Security Group 5432 규칙 삭제 완료
  • VPC Peering 라우팅 삭제 완료
  • pg_dump 백업 파일 삭제 완료
  • repl_user 삭제 완료
  • 구 파라미터 그룹 삭제 완료
  • CloudWatch 알람 정상 동작 확인
  • Grafana 대시보드 RDS 기준으로 업데이트 완료

15. 부록

15.1. 실행 스크립트 모음

15.2. 체크리스트

사전 체크리스트 (D-3일 이전)

  • devths-v2-prod-rds 생성 완료 (PostgreSQL 14.17)
  • rds.logical_replication = 1 파라미터 적용 완료
  • VPC Peering 라우팅 설정 완료 ()
  • Security Group 설정 완료
    • devths-prod-rds-sg ← 5432
    • devths-V1-prod-ec2 ← 5432
  • 팀 전체 배포 동결 공지 완료 (D-1일 ~ D+1일)
  • 롤백 스크립트 사전 검증 완료
  • 컷오버 담당자 온콜 일정 확인
  • migration_load.js k6 스크립트 작성 완료
  • 테스트 유저 20명분 AccessToken 사전 발급 완료
  • k6 실행 환경 확인 (k6 설치, 실행 터미널 2개 준비)

실행 체크리스트 (D-3일 ~ D-Day)

  • Phase 0: WAL 레벨 변경 완료 (wal_level = logical)
  • Phase 0: PostgreSQL 재시작 30초 이내 완료
  • Phase 0: 서비스 헬스체크 정상 확인
  • Phase 1: repl_user 생성 완료 (userepl = t)
  • Phase 1: pg_hba.conf 설정 완료 ( 허용)
  • Phase 1: RDS 스키마 적용 완료 (테이블 목록 V1과 일치)
  • Phase 1: RDS 연결 테스트 완료
  • Phase 2: RDS 재시작 완료 (ParameterApplyStatus: in-sync)
  • Phase 2: Publication devths_pub 생성 완료
  • Phase 2: Subscription devths_sub 생성 완료
  • Phase 2: 복제 슬롯 active 확인
  • Phase 2: 초기 동기화 완료 (srsubstate = r)
  • Phase 3: V2 환경(BE, AI, FE) 배포 완료
  • Phase 3: ALB Listener 타겟 그룹 전환 완료 (1차 컷오버: V2 EC2)
  • Phase 3: 외부 헬스체크 및 트래픽 정상 확인
  • Phase 3: 중간 운영 중 DDL 동결 배포 정책 준수 확인
  • Phase 3: k6 부하 주입 시작 (Lag 0 수렴 및 에러율 0% 모니터링)
  • Phase 3: Row Count 및 Checksum 등 데이터 정합성 사전 검증 완료
  • Phase 4: D-Day 심야 컷오버 직전, 기존 Subscription 및 Schema 완전 초기화
  • Phase 4: V1 최신 스키마 추출 및 RDS 즉시 재반영 완료
  • Phase 4: 복제 Subscription 재구성 및 데이터 재동기화(copy_data=true) 완료
  • Phase 5: 최종 DB 컷오버 선행조건(재동기화 후 Lag 0 수렴) 확인
  • Phase 5: V1→RDS 시퀀스 강제 동기화 스크립트 실행 완료
  • Phase 5: 서비스 DB 커넥션(HikariCP 설정) V1 → RDS 전환 배포
  • Phase 5: 전환 순간 애플리케이션 에러율 스파이크 및 헬스체크 확인
  • Phase 5: 컷오버 후 새로운 데이터 INSERT 시 PK 충돌 0건 확인
  • Phase 6: DB 컷오버 완료 후 k6 10분 추가 유지 검증
  • Phase 6: p95 응답시간 목표치 이내(V1 대비 110% 이하) 확인
  • Phase 6: k6 결과 리포트 저장 및 부하 종료 완료 (/tmp/migration_k6_result.json)

사후 체크리스트 (D+1 ~ D+3)

  • 72시간 모니터링 이상 없음 확인
  • Subscription 삭제 완료 (RDS)
  • Publication 삭제 완료 (V1)
  • V1 PostgreSQL 중지 완료
  • V1 Security Group 5432 규칙 삭제 완료
  • VPC Peering 라우팅 삭제 완료
  • repl_user 삭제 완료
  • 백업 파일 정리 완료
  • 구 파라미터 그룹 삭제 완료
  • CloudWatch 알람 정상 동작 확인
  • Grafana 대시보드 RDS 기준 업데이트 완료

15.3. 참고 자료

PostgreSQL 공식 문서

항목 URL
Logical Replication https://www.postgresql.org/docs/14/logical-replication.html
Publication https://www.postgresql.org/docs/14/sql-createpublication.html
Subscription https://www.postgresql.org/docs/14/sql-createsubscription.html
pg_stat_replication https://www.postgresql.org/docs/14/monitoring-stats.html
pg_hba.conf https://www.postgresql.org/docs/14/auth-pg-hba-conf.html

AWS 공식 문서

항목 URL
RDS PostgreSQL Logical Replication https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/PostgreSQL.Concepts.General.FeatureSupport.LogicalReplication.html
RDS Parameter Groups https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_WorkingWithParamGroups.html
VPC Peering https://docs.aws.amazon.com/vpc/latest/peering/what-is-vpc-peering.html
CloudWatch Alarms https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/AlarmThatSendsEmail.html
RDS Multi-AZ https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.MultiAZ.html
⚠️ **GitHub.com Fallback** ⚠️