설계: 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>
22 KiB
멀티테넌트 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.pylifespan 에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) vsregistry.py:128(owner_id = 0). - 통일 방향:
None.users.id=0은 존재하지 않으므로 0 은 FK 위반 값이고,Optional[int]타입(context.py:36)과도 맞음. - 실제 오동작 경로(관리자 미존재 + API 키 호출, 즉 초기 배포/MCP 서버 호출):
portfolio_v2.py:268,406ctx.user_id is None가드를0이 통과 → 주문 실행 후enqueue_trade_history({"user_id": 0})(:355,443) →usersFK 위반 → 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.pysafe_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_v2404. 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-99v2 전환 없이 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:159json.dumps신규 사용(§4_dumps()규칙). 기존:44도 동일 패턴이라 일관성 차원. - L2.
crypto.decryptValueError→registry.get:80은 "계좌 미등록" 400 으로 안내(원인 은폐),kis_credentials._to_out:103은 미처리 → 500. 키 로테이션 실패 시 진단 어려움. - L3. Alembic
0009downgradedrop_index("ix_user_kis_credentials_app_key_hash")— 테이블이create_all로 생성된 경우 unique 는 constraint(..._app_key_hash_key)라 인덱스가 없어 downgrade 실패.0010idempotent OK, FKondelete모델과 일치 OK(SET NULL / CASCADE). 사용자 삭제 엔드포인트가 없어 CASCADE↔SET NULL 정책 충돌은 현재 비발생. - L4.
registry.legacy()의auto_trade_enabled=settings.AUTO_TRADE_ENABLED스냅샷은 60초 TTL 동안 고정 — 런타임 토글 지연. - L5.
kis_credentials.verify_credentials:235detail=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_v2SELECT/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 체크리스트
- DB 쓰기 db_worker 경유 — ✅ (허용 예외만 직접 commit)
- §5 리스크 규칙 — ✅ (buy_v2 에 가격필터/ban/MDD/최대보유 적용) — 단 M1 로 legacy·무관리자 시 보유수 가드 무의미
- SIDEWAYS 진입 제안 금지 — 해당 없음 ✅
- 백테스트 없이 실전 투입 금지 — 전략 변경 없음 ✅
- SL
min()만 — ✅ record_trade_result후save_to_db— v2 경로에 record 호출 없음(해당 없음) ✅- 쓰기 API
require_api_key— ✅ _dumps()/새 파일/시그니처 불변 — ❌ (L1json.dumps1건; 나머지 ✅)
배포 가능 여부
불가. 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-53add_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:47pytest.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를 유지하고staleTime5분·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 라우터 격리) 해소 후.