Files
auto-stock/docs/multi_tenant_phase1_review.md
shuim b4a1fc79ab feat: 멀티테넌트 1단계 — 사용자별 KIS 계좌·포트폴리오·수동매매 (MULTI_TENANT_ENABLED 기본 OFF)
설계: docs/multi_tenant_design.md / 검수: docs/multi_tenant_phase1_review.md (High 0, MT=False 배포 가능)

데이터·인프라
- user_kis_credentials(Fernet 암호화, PBKDF2 키 파생, key_version), stock_bans_v2 신설,
  portfolio/trade_history/active_trades/trade_journals/trading_sessions 등 user_id nullable 추가
- alembic 0009~0011(idempotent, legacy owner 백필). main.py 기동 시 portfolio.user_id 스키마 검사(H1)
- Jenkinsfile: DB Migration 을 Deploy 앞으로, alembic 이력 없으면 stamp 0008 후 upgrade(N1)
- bootstrap: users 비어 있으면 BOOTSTRAP_ADMIN_* 로 관리자 시드 + .env KIS 자격증명 이관 + 백필(SAVEPOINT)

백엔드
- core/crypto.py, core/tenant_auth.py(get_current_user — 비활성 사용자 401, API 키→legacy owner,
  deny_non_owner_session_when_mt), core/dt_utils.py
- services/tenant: context, kis_pool, registry(UserContextRegistry, 락 최소화·복호화 to_thread), risk_factory
- KoreaInvestmentAPI.for_user(사용자별 인스턴스, 토큰캐시 users/{id}/, 시세는 공용 위임)
- trade_executor.safe_place_order_v2(사용자별 락/세마포어, legacy ctx 는 기존 키)
- mdd_manager/loss_tracker _v2 + ban_until tz-aware/naive 경계 정규화(레거시 경로 버그 포함 수정)
- db_worker: payload user_id 옵션 필터, upsert_stock_ban_v2, legacy ActiveTrade 조회 LIMIT 1
- API: /account/credentials(등록·마스킹조회·삭제·연결테스트·자동매매), /auth/admin/tenant/*,
  /portfolio/v2(조회·요약·sync·buy·sell), /trades/v2(+pending). PortfolioResponse Optional 수정

프론트
- /account 내 계좌 설정, useTenantMode(세션=v2 / API키=legacy, 미확정 시 쿼리·버튼 비활성),
  계좌 미등록 인라인 안내, v2 매수/매도/거래이력 분기

테스트: tests/integration(격리 Postgres, KIS 전면 mock) 20 passed / 1 xfailed(0012 이전 제약)
2단계(스케줄러 사용자화·legacy 라우터 격리) 전까지 MULTI_TENANT_ENABLED=True 전환 금지

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 10:26:51 +09:00

22 KiB
Raw Blame History

멀티테넌트 1단계 검증 보고서 (2026-09-21, 브랜치 dev 미커밋 변경분)

검수 범위: git status 의 백엔드/프론트 변경 23파일 + 신규 17파일, docs/multi_tenant_design.md, CLAUDE.md. 읽기 전용(코드 수정·컨테이너 조작 없음). ast.parse 29파일 전부 통과, npx tsc --noEmit exit 0. 로컬에 fastapi 미설치로 import 실행 검사 불가 → 정적 순환 임포트 분석으로 대체(순환 없음: auth.py↔bootstrap.py 는 양쪽 모두 함수 내 지연 import).


High (배포 차단)

H1. 배포 순서 결함 — alembic upgrade head 없이 새 이미지를 올리면 레거시 경로 전체 500

  • backend/app/models/models.py:47,130,165,213,228,278,332 에 user_id 컬럼 추가. main.py:159 주석대로 스키마는 create_all 만 실행되고 alembic 은 CLI 전용인데, create_all 은 기존 테이블에 컬럼을 추가하지 않음. Dockerfile CMD 는 uvicorn 직행, compose 에도 alembic 단계 없음.
  • 영향: SQLAlchemy 는 매핑된 모든 컬럼을 명시 SELECT/INSERT 하므로 portfolio/trade_history/active_trades/trade_journals/trading_sessions 관련 모든 기존 엔드포인트와 db_worker 태스크(insert_trade_history, upsert_portfolio 등)가 UndefinedColumnError 로 실패. MT 기능과 무관하게 자동매매 기록이 전부 유실됨.
  • 권장: 배포 절차에 alembic current 확인 → alembic upgrade head → docker compose up -d --build 명문화. main.py lifespan 에 information_schema 로 portfolio.user_id 존재 확인 후 없으면 명확한 에러 로그+기동 중단(또는 경고). 담당: dev(mt-a-dev) + 배포 문서.

H2. get_current_user 가 비활성/삭제 사용자를 legacy owner 로 폴백 → 회수된 계정이 owner 계좌로 매매 가능

  • backend/app/core/tenant_auth.py:87-99: _resolve_session_user 결과가 None 또는 is_active=False 면 예외 대신 CurrentUser(id=legacy_owner_id, ...) 반환. security.require_api_key(security.py:72-91)는 JWT 서명/exp/aud 만 검증하고 is_active 를 보지 않으므로 이 경로는 "비정상 경합"이 아니라 revoke 직후 토큰 만료(24h)까지 항상 도달함.
  • 영향: MT=True 에서 registry.resolve()(registry.py:160-166)가 current_user.id == legacy_ctx.user_id 로 legacy 컨텍스트를 반환 → 비활성 사용자가 /portfolio/v2/buy|sell 로 legacy owner 의 KIS 계좌에 주문, /portfolio/v2·/trades/v2 로 owner 데이터 열람. MT=False 에서도 owner 데이터 열람.
  • 권장: 두 경우 모두 HTTPException(401) 로 거부. 담당: dev(mt-b-dev).

H3. 자격증명 변경/삭제/자동매매 OFF 가 캐시된 UserContext 에 반영되지 않음

  • backend/app/api/endpoints/kis_credentials.py:183,200 은 kis_client_pool.invalidate() 만 호출. user_context_registry.invalidate() 호출처는 코드베이스에 0건(grep 확인). registry.get()(registry.py:59-61)은 _contexts 캐시 히트 시 DB 를 보지 않음.
  • 영향: PUT(키 교체, is_enabled=False 로 리셋)·DELETE 후에도 캐시된 ctx 가 구 app_key 로 만든 KIS 인스턴스를 계속 보유(httpx 는 is_closed 체크로 자동 재생성) → 삭제·교체된 자격증명으로 재시작 전까지 주문 계속 가능. PATCH /auto-trade enabled=false 도 ctx.auto_trade_enabled 에 반영 안 됨 → buy_stock_v2:270 가드 무력화.
  • 권장: 세 엔드포인트에서 await user_context_registry.invalidate(user_id) 호출(내부에서 kis_pool 도 정리, 순환 임포트 없음). 담당: dev(mt-b-dev).

H4. tests/integration/test_kis_credentials.py 가 운영 DB 를 직접 대상으로 함

  • 파일 docstring(:4-13) 자체가 "실제 AsyncSessionLocal(운영 DB)에 대해 검증", 실행 예시 docker exec autostock-backend python -m pytest .... 라이브 DB 가드 없음, 테스트 사용자 생성/삭제를 운영 DB 에서 수행. 중간 실패 시 잔여 행.
  • test_multi_tenant.py:38-43 가드는 os.environ["DATABASE_URL"] 만 검사 → env 미설정 시 settings.DATABASE_URL 기본값(...:5432/autostock_db, 컨테이너 내부면 라이브 postgres)으로 떨어지는데 가드를 통과함. test_h_bootstrap_admin_seed_and_idempotent 는 _backfill_legacy_owner_ids 로 실제 UPDATE 를 수행.
  • 권장: 두 파일 모두 settings.DATABASE_URL 로 가드하고, 임시 DB 마커(예: DB 명 _test 접미사) 필수화. 담당: mt-c-dev(테스트).

Medium

M1. CurrentUser.effective_user_id(None) vs UserContextRegistry.legacy().user_id(0) 불일치

  • 위치: tenant_auth.py:47,85(None) vs registry.py:128(owner_id = 0).
  • 통일 방향: None. users.id=0 은 존재하지 않으므로 0 은 FK 위반 값이고, Optional[int] 타입(context.py:36)과도 맞음.
  • 실제 오동작 경로(관리자 미존재 + API 키 호출, 즉 초기 배포/MCP 서버 호출):
    • portfolio_v2.py:268,406 ctx.user_id is None 가드를 0 이 통과 → 주문 실행 후 enqueue_trade_history({"user_id": 0})(:355,443) → users FK 위반 → db_worker 3회 재시도 후 폐기 → 주문은 나갔는데 거래 기록 유실. enqueue_portfolio_upsert:373, enqueue_close_active_trade_v2:465 동일.
    • MT=True 시 _user_scope_filter(Portfolio.user_id, 0) → 빈 결과 → 매도 항상 404(fail-closed), 매수는 보유수 0 으로 계산돼 MAX_HOLDINGS_COUNT 가드 무의미.
    • _resolve_target_user_id 는 None 이면 400 → GET 계열은 fail-closed(정상).
  • 권장: registry.legacy() 에서 owner_id = owner.id if owner else None, owner 미확정 상태면 캐시하지 않기(현재 60초 TTL 동안 0 고정). 담당: mt-a-trading.

M2. legacy 컨텍스트가 safe_place_order_v2 를 타면 락 키가 기존 경로와 달라 이중 매도 방지가 깨짐

  • trade_executor.py safe_place_order_v2 는 항상 order_lock:{user_id}:{code}:{type} + 세마포어 키 {user_id}:{code} 사용. 자체 docstring 과 설계문서 §2-4/§10-4("legacy 경로는 기존 키 그대로") 위반. MT=False(기본)에서 세션 사용자가 v2 매도 → order_lock:{owner}:005930:SELL, 동시에 swing/daytrading exit_service 는 order_lock:005930:SELL → 같은 계좌 같은 종목 동시 매도 가능.
  • 기존 /portfolio/sell 도 락이 없었으므로 회귀는 아니지만 설계 의도 미달. 권장: ctx.is_legacy 면 _acquire_order_lock/_get_order_semaphore(stock_code) 로 분기. 담당: mt-a-trading.

M3. MT=True 에서 legacy 스케줄러가 쓰는 행(user_id=NULL)이 owner 의 v2 화면에서 사라지고 ActiveTrade 종료가 누락

  • swing/daytrading/trade_executor/portfolio.py/manual_buy.py 어느 곳도 payload 에 user_id 를 넣지 않음(grep 0건; 설계 §4 "DB 쓰기 payload에 user_id를 넣되" 미구현). 부트스트랩 백필은 기동 시 1회.
  • 영향: MT=True 시 _user_scope_filter 가 == owner 로 엄격해져 기동 이후 자동매매로 생긴 보유/이력이 /portfolio/v2, /trades/v2 에서 비표시, sell_stock_v2 404. MT=False 에서도 sell_stock_v2:465 의 enqueue_close_active_trade_v2(owner, ...) 가 ActiveTrade.user_id == owner 필터(db_worker.py:587)로 NULL 행을 못 찾아 전량 수동매도 후 ActiveTrade 가 active 로 잔존 → exit_service 가 유령 포지션 추적.
  • 권장: 1단계에서는 enqueue_close_active_trade_v2 를 legacy ctx 에서 user_id=None 으로 호출(기존 동작), 또는 _user_scope_filter 를 close 에도 적용. MT=True 전환 전제조건으로 legacy 쓰기 경로 user_id 주입 필요. 담당: mt-a-trading.

M4. registry.get() 이 asyncio.Lock 안에서 DB I/O + Redis + PBKDF2 CPU 작업 수행

  • registry.py:63-111: 락 내부에서 db.execute, decrypt×3(PBKDF2 200k 회 × 3 ≈ 0.3~0.6초 동기 CPU, crypto.py:28-32 파생키 캐시 없음), load_user_risk_state(Redis+DB). CLAUDE.md §3-4 위반이며 모든 사용자 컨텍스트 조립을 직렬화 + 이벤트 루프 정지(실시간 스케줄러 지연).
  • kis_credentials._to_out:103 도 GET 마다 decrypt 실행 → 요청당 ~0.2초 루프 블로킹.
  • 권장: _derive_key 에 functools.lru_cache, 복호화는 asyncio.to_thread, 락은 dict 갱신 구간만. 담당: mt-b-dev / mt-a-trading.

M5. 프론트 useTenantMode 로딩 중 legacy 반환 → 세션 사용자에게 owner 데이터 노출/오주문 창

  • frontend/src/lib/tenant.ts:37. usePortfolioData 는 ['portfolio-combined','legacy'] 키로 /portfolio/combined 를 즉시 호출해 결과를 캐시. MT=True 에서 이는 타 사용자(owner) 보유 정보 노출이며, 그 창에 매수/매도 클릭 시 useTradeActions 가 legacy /portfolio/sell 로 owner 계좌에 주문.
  • 근본 원인은 백엔드: legacy /portfolio*, /trades* 가 MT=True 에서도 모든 세션 사용자에게 열려 있음(설계 §5.7 "기존 /portfolio* 는 legacy 컨텍스트로 유지"). 프론트 가드는 완화책일 뿐.
  • 권장: (프론트) mode: 'v2' | 'legacy' | undefined, 로딩 중 쿼리 enabled: false. (백엔드, MT=True 전환 전제) legacy 라우터에 "MT=True 이고 세션 사용자가 owner 아니면 403" 의존성. 담당: mt-d-dev / mt-b-dev.

M6. resetPendingTrades → DELETE /trades/pending 가 사용자 무관 전체 삭제

  • useTradeActions.ts:97-99 v2 전환 없이 legacy 유지, trades.py:192-209 는 user_id 조건 없음 → MT=True 에서 일반 사용자가 타 사용자의 PENDING/FAILED 이력 삭제 가능. 권장: /trades/v2/pending 추가 또는 v2 모드에서 버튼 숨김. 담당: mt-d-dev.

M7. db_worker scalar_one_or_none() 이 사용자 다중화로 MultipleResultsFound 가능

  • db_worker.py:543,589(upsert_active_trade/close_active_trade) legacy 호출(user_id=None) 시 필터 미적용인데 MT 로 두 사용자가 같은 종목 active 면 예외 → 태스크 3회 실패 후 폐기. user_id=None 경로 자체는 기존과 동일(is not None 체크 정확 — == None 실수 없음). 권장: .limit(1) 또는 scalars().first(). 담당: mt-e-risk-fix.

M8. 부트스트랩/이관 API 의 백필이 첫 실패 후 조용히 전부 무효화

  • bootstrap.py:122-146, auth.py:337-349: PostgreSQL 은 트랜잭션 내 한 문장 실패 시 이후 문장이 모두 InFailedSqlTransaction → 개별 try/except 가 삼켜 backfilled 카운트가 0 으로 오보되고 실제 UPDATE 미실행. 권장: 문장별 SAVEPOINT(session.begin_nested()) 또는 사전 information_schema 체크. 담당: mt-b-dev.

Low

  • L1. mdd_manager.py:159 json.dumps 신규 사용(§4 _dumps() 규칙). 기존 :44 도 동일 패턴이라 일관성 차원.
  • L2. crypto.decrypt ValueError → registry.get:80 은 "계좌 미등록" 400 으로 안내(원인 은폐), kis_credentials._to_out:103 은 미처리 → 500. 키 로테이션 실패 시 진단 어려움.
  • L3. Alembic 0009 downgrade drop_index("ix_user_kis_credentials_app_key_hash") — 테이블이 create_all 로 생성된 경우 unique 는 constraint(..._app_key_hash_key)라 인덱스가 없어 downgrade 실패. 0010 idempotent OK, FK ondelete 모델과 일치 OK(SET NULL / CASCADE). 사용자 삭제 엔드포인트가 없어 CASCADE↔SET NULL 정책 충돌은 현재 비발생.
  • L4. registry.legacy() 의 auto_trade_enabled=settings.AUTO_TRADE_ENABLED 스냅샷은 60초 TTL 동안 고정 — 런타임 토글 지연.
  • L5. kis_credentials.verify_credentials:235 detail=f"...{e}" 로 KIS 예외 원문 노출 — 현재 KIS 오류 본문에 키는 없으나 방어적으로 예외 타입만 노출 권장. 평문 키/시크릿/비밀번호는 응답·로그 어디에도 없음 확인(hint 6자만, BOOTSTRAP_ADMIN_PASSWORD 미로깅).

확인 통과 항목

  • 시그니처 변경: git diff - 라인에 def/class 변경 0건. 신규는 전부 _v2/for_user/신규 파일.
  • 실시간 계층 AsyncSessionLocal() 신규 호출 없음(bootstrap 은 lifespan 1회, 허용).
  • 직접 commit 신규 경로: kis_credentials(설계 §5.3 허용), sync_portfolio_v2(sync 예외), sell_stock_v2:469(기존 /portfolio/sell:1365 와 동일 패턴). 정책 내.
  • require_api_key: portfolio_v2/trades_v2/kis_credentials 라우터 등록 시 의존성 부여(api/__init__.py:30-52). /auth/admin/tenant/* 는 require_admin(내부적으로 require_api_key 의존) 강제.
  • 관리자 ?user_id= 오버라이드: _resolve_target_user_id:69/_resolve_ctx:80 비관리자 403, GET 2개 + trades_v2 모두 경유.
  • app_key_hash 중복 체크 user_id != user_id 로 자기 갱신 오탐 없음(:151).
  • JWT: aud/exp 검증(security.py:58).
  • .env 미추적(.env.example 만), git diff --cached 비어 있음.
  • sync_portfolio_v2 SELECT/INSERT/DELETE 모두 Portfolio.user_id == ctx.user_id(:193,219,230-232).
  • execute_reduction_v2 큐 payload 3곳 모두 user_id 포함.
  • registry.get() is_enabled=False 거부(:72), invalidate 시 _active_cache=None(:214).
  • for_user 속성 집합 = for_real_market_data ⊇ + mode,user_id; _last_token_request_time 은 hasattr 가드.
  • SL 관련 max() 신규 도입 없음.

CLAUDE.md §8.1 체크리스트

  1. DB 쓰기 db_worker 경유 — ✅ (허용 예외만 직접 commit)
  2. §5 리스크 규칙 — ✅ (buy_v2 에 가격필터/ban/MDD/최대보유 적용) — 단 M1 로 legacy·무관리자 시 보유수 가드 무의미
  3. SIDEWAYS 진입 제안 금지 — 해당 없음 ✅
  4. 백테스트 없이 실전 투입 금지 — 전략 변경 없음 ✅
  5. SL min() 만 — ✅
  6. record_trade_result 후 save_to_db — v2 경로에 record 호출 없음(해당 없음) ✅
  7. 쓰기 API require_api_key — ✅
  8. _dumps()/새 파일/시그니처 불변 — ❌ (L1 json.dumps 1건; 나머지 ✅)

배포 가능 여부

불가. High 4건(H1 배포 순서, H2 비활성 사용자 폴백, H3 컨텍스트 캐시 미무효화, H4 운영 DB 테스트). H1H3 는 각 15줄 수정으로 해소 가능. MULTI_TENANT_ENABLED=True 전환은 추가로 M1·M3·M5(백엔드 legacy 라우터 격리) 해소가 전제.


재검증 (2026-09-21, 4팀 수정 반영 후 dev 워킹트리)

읽기 전용 재검수. ast.parse 30파일 OK, npx tsc --noEmit exit 0. git diff - 라인 재점검: 함수/클래스 시그니처 변경 없음(portfolio.py PortfolioResponse 필드 Optional[...] 화는 기존 알려진 pydantic v2 버그 수정, 별도 항목). 순환 임포트 신규 없음(api/__init__.py → core/tenant_auth, kis_credentials → services/tenant/registry 모두 endpoints 로 되돌아오지 않음).

항목별 판정

항목 판정 근거
H1 alembic 배포 순서 ✅ 해소 main.py:90-109 information_schema 검사 — create_all(:87) 직후, 부트스트랩(:184) 이전, try 블록 내 raise RuntimeError → 기동 중단. Jenkinsfile:81-88 DB Migration 스테이지가 Deploy(:90) 앞으로 이동, || echo WARNING 제거로 실패 시 파이프라인 중단. docker-compose run --rm --no-deps backend alembic upgrade head: compose backend 서비스 env_file: .env(DEPLOY_DIR 기준) + autostock-network 부착 + 컨테이너 내 /.dockerenv 존재로 config._apply_docker_db_override 가 localhost→postgres 치환(config.py:369-372), WORKDIR /app 에 alembic.ini 존재 → NAS 환경 동작 가능. 단 전제 2건은 아래 N1 참조
H2 비활성 사용자 폴백 ✅ 해소 tenant_auth.py:97-105 user is None or not is_active → 401. API 키 경로(:76-85) 변경 없음 — legacy owner 매핑 유지, owner 미존재 시 id=None, is_api_key=True
H3 컨텍스트 캐시 미무효화 ✅ 해소 kis_credentials.py:201,218,312 PUT/DELETE/PATCH 모두 user_context_registry.invalidate(user_id) (내부에서 kis_client_pool.invalidate 호출, registry.py:226-231)
H4 운영 DB 테스트 ✅ 해소 tests/integration/conftest.py:44-52 settings.DATABASE_URL DB명 _test 접미사 강제, 미충족 시 pytest.exit. 두 테스트 파일이 공용 conftest 경유. 부작용 N3 참조
M1 effective_user_id vs legacy 0 ✅ 해소 registry.py:147-172 owner_id: int | None = None, _legacy_owner_confirmed 로 owner 미확정 시 TTL 캐시 미적용. portfolio_v2.py:59-73 _user_scope_filter MT=True & None → sql_false()(fail-closed), column == None 의 IS NULL 컴파일 함정 회피. buy/sell 가드 ctx.user_id is None and not ctx.is_legacy(:282)
M2 legacy 락 키 ✅ 해소 trade_executor.py safe_place_order_v2 ctx.is_legacy 분기 → _acquire_order_lock/_release_order_lock/_get_order_semaphore(stock_code) (v1 과 동일 키)
M3 legacy 행 user_id 정합 🟡 부분 (1단계) portfolio_v2.py:386-407,485-496 legacy ctx 는 trade_history/portfolio payload 에 user_id 미포함 + enqueue_close_active_trade(v1) 사용 → 기존 v1 과 동일 ✅. (MT=True 전제) 스윙/데이트레이딩 스케줄러 쓰기 경로에 user_id 주입은 여전히 없음 → MT=True 에서 _user_scope_filter == owner 가 기동 이후 자동매매 행(NULL)을 owner 의 v2 화면에서 제외. 설계 §4 미구현 그대로 — MT=True 전환 전 필수
M4 락 내 I/O·PBKDF2 ✅ 해소 registry.py:57-116 락은 dict 갱신(더블체크)만, decrypt 는 asyncio.to_thread, crypto.py:36 _derive_key lru_cache. 동일 user_id 동시 요청: kis_client_pool.get_or_create 가 자체 락+캐시로 KIS 인스턴스 단일 보장 → 이중 생성/누수 없음(중복 조립되는 MDD/loss_tracker 객체는 패자 측이 버려짐, 외부 자원 없음). kis_credentials.py:100-119 _to_out 도 to_thread
M5 프론트 legacy 플래시 / 백엔드 legacy 라우터 격리 🟡 부분 프론트: tenant.ts:30-55 mode: 'v2'|'legacy'|undefined, usePortfolioData.ts:100 enabled: mode !== undefined, useTradeActions.ts:48 isReady, Dashboard.tsx:797,838,844 버튼 disabled, TradeHistory.tsx:352 동일 ✅. /auth/me(auth.py:250-258) API 키 → auth_type: "api_key" → legacy 정상 ✅. 백엔드: api/__init__.py:44,48,100 portfolio/manual_buy/trades 라우터에 deny_non_owner_session_when_mt(tenant_auth.py:108-128: MT=False 통과, is_api_key 통과 → MCP 서버 영향 없음, 스케줄러는 HTTP 를 타지 않아 무관) ✅. 잔여: swing_portfolio/day_trading/daytrading_v2/us_trading/journals/risk_management/recommendations 등 나머지 legacy 라우터는 MT=True 에서 여전히 모든 세션에 노출(설계 §7-3 2단계 이관 대상) — MT=True 전환 전제
M6 DELETE /trades/pending 전체 삭제 ✅ 해소 trades_v2.py:96-128 /trades/v2/pending _user_scope_filter 적용, useTradeActions.ts:114-119 v2 분기
M7 scalar_one_or_none MultipleResultsFound ✅ 해소 db_worker.py:540-550,592-602 legacy(user_id None) 경로 order_by(id.desc()).limit(1) + scalars().first()
M8 백필 InFailedSqlTransaction ✅ 해소 bootstrap.py:128-146 문장별 begin_nested(), auth.py:336-345 동일
L1 json.dumps ✅ 해소 mdd_manager.py:13,46,161 _dumps
L2 decrypt 오류 진단 ✅ 해소 crypto.py:29 CredentialDecryptError, registry.py:83-87 400 "복호화 실패" 명시, _to_out decrypt_error 플래그
L3 0009 downgrade ✅ 해소 0009:50-66 pg_indexes 존재 확인 후 drop
L4 AUTO_TRADE_ENABLED 스냅샷 ✅ 해소 context.py:48- effective_auto_trade_enabled 프로퍼티, portfolio_v2.py:286 사용
L5 KIS 예외 원문 노출 ✅ 해소 kis_credentials.py:260 type(e).__name__ 만 노출

신규 발견 (수정 과정 부작용 / 전제조건)

  • N1 (Medium, 배포 전제조건) alembic upgrade head 의 alembic_version 선행 상태. 이전 파이프라인의 docker-compose exec ... \|\| echo WARNING 이 실제 성공했는지 불명. 운영 DB 가 create_all 로만 만들어져 alembic_version 이 비어 있으면 upgrade head 가 0001 부터 실행되고, 0003_add_strategy_type.py:23-53 add_column/create_index 는 존재 가드가 없어 DuplicateColumn 으로 실패 → 이제는 파이프라인이 중단(안전하지만 배포 불가). 첫 배포 전 alembic current 확인 후 비어 있으면 alembic stamp 0008 필요 — Jenkinsfile 또는 배포 문서에 명시 권장. 또한 --no-deps 는 postgres 컨테이너가 이미 기동 중임을 전제(최초 배포 환경에선 Deploy 이전이라 실패).
  • N2 (Low) get_current_user 가 deny_non_owner_session_when_mt 경유로 모든 /portfolio*·/trades* 요청에 붙어 요청당 DB 쿼리 2회(get_legacy_owner, 사용자 조회) 추가 — MT=False 에서도 실행됨(플래그 검사가 의존성 해석 뒤). 30초 폴링 수준에선 무시 가능하나 MULTI_TENANT_ENABLED=False 면 get_current_user 를 건너뛰는 조기 반환으로 줄일 수 있음.
  • N3 (Low) tests/integration/conftest.py:47 pytest.exit 는 backend/ 에서 인자 없이 pytest 실행(단위 테스트 전체 수집) 시에도 발동 → 라이브 DB 를 안 건드리는 기존 단위 테스트 실행까지 중단. pytest.ini 에 testpaths = tests + norecursedirs = tests/integration(또는 addopts = --ignore=tests/integration) 로 기본 수집에서 제외하고 통합 테스트는 명시 경로로만 실행하도록 권장. Jenkins 는 pytest 를 실행하지 않아 CI 영향 없음.
  • N4 (Low, UX) useTenantMode 가 /auth/me 실패(네트워크 오류 등) 시 undefined 를 유지하고 staleTime 5분·refetchOnWindowFocus:false 라 재시도 소진 후 대시보드가 빈 상태·버튼 비활성으로 고정될 수 있음. retry/수동 재시도 버튼 고려.

최종 판정

배포 가능(MULTI_TENANT_ENABLED=False 기본값 기준). High 4건 모두 해소, Medium 8건 중 6건 해소·2건(M3, M5) 은 1단계 범위 내 부분 해소이며 잔여분은 MT=True 전환 전제조건. 배포 시 N1(alembic current 확인/필요 시 stamp 0008) 을 먼저 수행할 것. MULTI_TENANT_ENABLED=True 전환은 아직 불가 — M3 잔여(legacy 스케줄러 쓰기 경로 user_id 주입 또는 owner 에 대해 NULL 행 포함 필터), M5 잔여(나머지 legacy 라우터 격리) 해소 후.