Files
portfolio-hub/docs/batches/bat-021_20260628_req047-step2-integration-and-runbook/design.md
T
genichin 3e1b571906 feat(bat-021): REQ-047 Step2 동반 코드 수정 + 실 DB 통합검증 + 런북
- ensure_schema() broker_credentials: daemon_id NOT NULL, PK=(daemon_id,broker)로 갱신
- _row_to_broker_credential: Step2 컬럼 순서(daemon_id 선두)로 갱신
- upsert_broker_credential: ON CONFLICT (daemon_id,broker)로 수정, fallback try/except 제거
- list_broker_credentials: fallback 제거, SELECT 컬럼 순서 갱신
- test_bat020_broker_credential_domain.py: AC-3/AC-4 행 순서를 Step2 포맷으로 갱신
- tests/test_bat021_step2_ensure_schema_and_upsert.py: 12개 신규 테스트
- docs/runbooks/bat-021-req047-step2-migration-runbook.md: 운영 적용 런북
- docs/batches/bat-021: batch 문서(index/planning/design/implementation/verification)

실 PostgreSQL 통합검증(AC-1~7 전건 PASS):
  Step1 upgrade → backfill → Step2 upgrade → downgrade 시나리오 포함
  NULL 사전검증(upgrade 거부) 및 downgrade 안전장치(중복 시 RuntimeError) 확인

521 passed, 0 failed (기존 509 + 신규 12)
2026-06-28 10:19:27 +09:00

4.8 KiB

BAT-021 Design

변경 설계

T-1/T-2/T-3: app/repositories/postgres.py 수정

ensure_schema() broker_credentials (T-1)

현재 ensure_schema()의 broker_credentials CREATE TABLE은:

CREATE TABLE IF NOT EXISTS broker_credentials (
    user_id TEXT NOT NULL,
    broker TEXT NOT NULL,
    account_ref TEXT NOT NULL,
    encrypted_payload TEXT NOT NULL,
    credential_version INTEGER NOT NULL DEFAULT 1,
    last_rotated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    PRIMARY KEY (user_id, broker)
)

Step2 이후 최종 스키마:

  • daemon_id TEXT NOT NULL 추가
  • PK = (daemon_id, broker)
  • user_id 컬럼 유지 (NOT NULL — 신규 INSERT는 항상 user_id 포함)

갱신 후:

CREATE TABLE IF NOT EXISTS broker_credentials (
    daemon_id TEXT NOT NULL,
    broker TEXT NOT NULL,
    user_id TEXT NOT NULL,
    account_ref TEXT NOT NULL,
    encrypted_payload TEXT NOT NULL,
    credential_version INTEGER NOT NULL DEFAULT 1,
    last_rotated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    PRIMARY KEY (daemon_id, broker)
)

ensure_schema()는 IF NOT EXISTS를 사용하므로 테이블이 이미 존재하면 CREATE를 스킵한다. Step2 마이그레이션이 적용된 DB에서는 이 정의와 실제 스키마가 정합된다. Step1만 적용된 DB(Step2 미적용)에서 ensure_schema()를 호출하면 테이블은 이미 존재하므로 스킵되며 영향 없다.

Step2 이후 추가될 인덱스:

CREATE INDEX IF NOT EXISTS idx_broker_credentials_daemon_broker
ON broker_credentials (daemon_id, broker)

이 인덱스는 PK 자체이므로 별도 인덱스는 불필요. Step1에서 만든 idx_broker_credentials_daemon_id 인덱스는 그대로 유지.

upsert_broker_credential (T-2)

현재:

  • Primary INSERT: ON CONFLICT (user_id, broker) — Step2 이후 PK가 바뀌어 동작 안 함
  • Fallback: ON CONFLICT (user_id, broker) without daemon_id — Step1 미적용 환경용

Step2 이후 세계에서:

  • daemon_id는 항상 NOT NULL (Step2 PK 제약)
  • ON CONFLICT는 (daemon_id, broker) 로 변경
  • Fallback 불필요 → 제거

단, BAT-020의 fallback try/except는 psycopg 트랜잭션 abort 이슈가 있었다: 트랜잭션이 abort 상태에서 fallback 쿼리를 실행하면 InFailedSqlTransaction 오류가 발생. 이 batch에서 fallback을 제거하면서 이 이슈도 동시에 해소된다.

갱신 후 upsert_broker_credential:

cur.execute(
    """
    INSERT INTO broker_credentials (daemon_id, user_id, broker, account_ref, encrypted_payload)
    VALUES (%s, %s, %s, %s, %s)
    ON CONFLICT (daemon_id, broker) DO UPDATE
    SET account_ref = EXCLUDED.account_ref,
        encrypted_payload = EXCLUDED.encrypted_payload,
        user_id = EXCLUDED.user_id,
        credential_version = broker_credentials.credential_version + 1,
        last_rotated_at = NOW()
    RETURNING daemon_id, user_id, broker, account_ref, encrypted_payload, credential_version, last_rotated_at
    """,
    (daemon_id, user_id, broker, account_ref, encrypted_payload),
)

RETURNING 절에서 daemon_id 컬럼 순서 변경: _row_to_broker_credential의 현재 row 매핑 확인 필요.

현재 _row_to_broker_credential:

return BrokerCredential(
    user_id=row[0],   # user_id
    broker=row[1],    # broker
    account_ref=row[2],
    encrypted_payload=row[3],
    credential_version=int(row[4]),
    last_rotated_at=row[5],
    daemon_id=row[6],  # optional
)

RETURNING 컬럼 순서를 유지해야 한다. 새 RETURNING: daemon_id(0), user_id(1), broker(2), account_ref(3), encrypted_payload(4), credential_version(5), last_rotated_at(6) → _row_to_broker_credential 매핑도 함께 수정 필요.

list_broker_credentials도 같은 방향으로 정렬: SELECT daemon_id, user_id, broker, account_ref, encrypted_payload, credential_version, last_rotated_at

_row_to_broker_credential 갱신:

return BrokerCredential(
    daemon_id=row[0],
    user_id=row[1],
    broker=row[2],
    account_ref=row[3],
    encrypted_payload=row[4],
    credential_version=int(row[5]),
    last_rotated_at=row[6],
)

list_broker_credentials fallback 제거 (T-3)

Step2 이후 daemon_id 컬럼이 항상 존재하므로 try/except fallback 제거.

영향 범위

  • app/repositories/postgres.py — ensure_schema, upsert_broker_credential, list_broker_credentials, _row_to_broker_credential
  • tests/ — bat021 신규 테스트, 기존 FakeRepo 의존 테스트 영향 없음 (FakeRepo는 실제 DB 쿼리 미사용)

하위 호환성

Step2 PK 전환 이후에만 이 코드가 올바르게 동작한다. Step2 미적용 DB에서는 ON CONFLICT (daemon_id, broker) 제약이 없어 오류 발생 가능. 런북에서 Step2 적용 이전에 새 코드를 배포하지 말 것을 명시한다.

배포 순서: Step1 → backfill → Step2 → 새 코드 배포.