notes(hjjeong): cross-tenant fan-out 2회사 머지 검증 결과 (2026-04-29)
설계서 §11.1 (행복 경로) 의 첫 다회사 실증.
SUPER_ADMIN 토큰으로 /_active-companies + 4개 fan-out 엔드포인트 직접 curl.
검증 결과 (TEST01 + TEST02 둘 다 active):
- /users total=10 q=2 failed=0 by={'*':8, 'TEST01':1, 'TEST02':1}
- /roles total=1 q=2 failed=0 by={'TEST01':1}
- /batches total=14 q=2 failed=0 by={'TEST01':7, 'TEST02':7} ← 균등 머지
- /lang-keys total=0 q=2 failed=0 by={} ← 회사 DB 시드 없음
발견:
- /users 의 '*' 8행은 버그 아님 — listUsers 만 includeMeta=true 호출,
메타 DB SUPER_ADMIN 들을 company_code='*' 로 prepend (의도된 설계).
- runFanOut 의 마지막 boolean 은 wrapSearchWithPercent 일 뿐 includeMeta 아님
(시그니처 분리). roles/batches/lang-keys 는 모두 includeMeta=false.
- /lang-keys 0건은 회사 DB MULTI_LANG_KEY_MASTER 가 비어있는 것 (failed=0).
이전 §3 의 "TEST01: 646건" 은 시점/컨텍스트 다른 측정으로 추정.
문서 갱신:
- 설계 27.md §11: 11.1 ✅, 11.3 fan-out 검증 추가 인용
- 실행 28.md §6 #3 → ✅, §8 활성 회사 TEST02 추가
- 실행 28.md §9 신설 — 결과 표 + 해석 + 검증된 항목 + 미검증 + 재현용 명령
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -423,9 +423,9 @@ const editUrl = `https://${row.subdomain}.invyone.com/admin/userMng/userMngList?
|
||||
|
||||
| 시나리오 | 상태 |
|
||||
|---|---|
|
||||
| 11.1 행복 경로 | 🟡 단일 회사 (TEST01) 만 검증. TEST02 프로비저닝 후 fan-out 2회사 호출은 미진행 |
|
||||
| 11.2 부분 실패 | ⏳ 미검증. fan-out 2회사 검증 후 순차 진행 |
|
||||
| 11.3 권한 | ✅ `super_admin_required` (토큰 없이 호출 → 403) 스모크 통과 (실행로그 §1) |
|
||||
| 11.1 행복 경로 | ✅ 2회사 (TEST01 + TEST02) 머지 실증 완료 (2026-04-29). [실행로그 §9](2026-04-28-cross-tenant-execution-log.md) 참조 |
|
||||
| 11.2 부분 실패 | ⏳ 미검증 — TEST02 DB pg_terminate 시뮬레이션 필요 |
|
||||
| 11.3 권한 | ✅ `super_admin_required` (토큰 없이 호출 → 403) — 스모크 (실행로그 §1) + fan-out 검증 (실행로그 §9.3) 모두 통과 |
|
||||
| 11.4 락 비획득 | ⏳ 미검증 |
|
||||
| 11.5 캐시 무효화 | ⏳ N/A — 캐시 미구현 (Phase D) |
|
||||
|
||||
|
||||
@@ -256,7 +256,7 @@ e16fb169 어드민 cross-tenant 집계 (SUPER_ADMIN) + 사용자관리 자체
|
||||
|---|---|---|---|
|
||||
| 1 | Phase C 3개 메뉴 검증 | ✅ 완료 (2026-04-28 오전) | — |
|
||||
| 2 | 페이지네이션 cap 도입 | ✅ 완료 (2026-04-28 오후) | — |
|
||||
| 3 | 두번째 회사 프로비저닝 → 진짜 fan-out 효과 (`companies_queried: 2`, 행에 `TEST01` / `TEST02` 섞임) 확인 | 🟡 부분 — TEST02 프로비저닝 완료, fan-out 호출 검증은 미진행. 즉시 가능 | 20분 / 머지·실패격리 동작 검증 |
|
||||
| 3 | 두번째 회사 프로비저닝 → 진짜 fan-out 효과 (`companies_queried: 2`, 행에 `TEST01` / `TEST02` 섞임) 확인 | ✅ 완료 (2026-04-29) — §9 결과 참조 | — |
|
||||
| 4 | 화면 측 `truncated === true` 안내 박스 — "200건 표시 중, 더 보려면 검색 좁히거나 회사 도메인 전환" | ⏳ 미진행 | 페이지당 10분 × 4 / UX |
|
||||
| 5 | 전체 변경을 한 커밋으로 정리 | ❌ **다른 결정** — 3 커밋으로 분리 (§5.1 사유 참조) | — |
|
||||
| 6 | 보류 4개 메뉴 UX 설계 — 특히 메뉴관리(트리)와 공통코드(카테고리 의존) | ⏳ 별도 세션 | 14개 메뉴 완주 |
|
||||
@@ -296,6 +296,64 @@ curl http://localhost:8081/api/auth/status
|
||||
|
||||
## 8. 알려진 데이터 상태 (참고)
|
||||
|
||||
- **활성 회사**: TEST01 (시연용회사) 1개. 다른 1개("공통", `company_code='*'`) 는 가상 row → cross-tenant 대상 X.
|
||||
- **TEST01 USER_INFO**: 8명 (강빈박, 관리자, 김대성, 명건희, 박창현, 정혜진, 오재옥, chpark). 7명은 시드 어드민 패턴 (`dept_code='DPT005'`, `user_name_eng='admin'`).
|
||||
- **활성 회사 (2026-04-29 시점)**: TEST01 + TEST02 (시연용회사 / 시연용회사2). 두 회사 모두 `DB_STATUS='active'`. 다른 1개("공통", `company_code='*'`) 는 가상 row → cross-tenant 대상 X.
|
||||
- **TEST01 USER_INFO** (2026-04-28 측정): 8명 (강빈박, 관리자, 김대성, 명건희, 박창현, 정혜진, 오재옥, chpark). 7명은 시드 어드민 패턴 (`dept_code='DPT005'`, `user_name_eng='admin'`).
|
||||
- **현재 로그인 토큰**: `user_id=hjjeong`, `user_type=SUPER_ADMIN`, `company_code=*` — META 컨텍스트로 라우팅 확인됨.
|
||||
|
||||
> 2026-04-29 fan-out 측정에서 회사 DB 데이터 갯수가 시점에 따라 달라짐 — §9 표 참조. 위 8명은 2026-04-28 시점 기준이며, 회사 DB 데이터는 프로비저닝 시드 이후 사용자 작업으로 변동될 수 있음.
|
||||
|
||||
---
|
||||
|
||||
## 9. fan-out 2회사 머지 검증 (2026-04-29)
|
||||
|
||||
설계서 §11.1 (행복 경로) 의 첫 다회사 실증. SUPER_ADMIN 토큰 (`hjjeong / company_code=*`) + bare `Host: localhost:9771` (META 컨텍스트) 로 백엔드 4개 cross-tenant 엔드포인트 직접 curl.
|
||||
|
||||
### 9.1 결과
|
||||
|
||||
| 엔드포인트 | total | q | failed | trunc | by company | 평가 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| `/_active-companies` | 2 | — | — | — | — | TEST01 + TEST02 둘 다 `DB_STATUS='active'` 확인 |
|
||||
| `/users` | 10 | 2 | 0 | false | `{'*': 8, 'TEST01': 1, 'TEST02': 1}` | ✅ 메타 8명 + 회사별 1명씩 |
|
||||
| `/roles` | 1 | 2 | 0 | false | `{'TEST01': 1}` | ✅ TEST02 권한그룹 시드 없음 |
|
||||
| `/batches` | 14 | 2 | 0 | false | `{'TEST01': 7, 'TEST02': 7}` | ✅ 균등 fan-out 머지 |
|
||||
| `/lang-keys` | 0 | 2 | 0 | false | `{}` | 회사 DB 양쪽 모두 다국어 키 시드 없음 |
|
||||
|
||||
### 9.2 해석
|
||||
|
||||
**`/users` 의 `*` 8행 = 의도된 동작.** [`CrossTenantController#listUsers`](../../backend-spring/src/main/java/com/erp/crosstenant/CrossTenantController.java#L116) 가 `aggregator.fanOut(..., true)` 로 `includeMeta=true` 호출 — 메타 DB USER_INFO (SUPER_ADMIN 들) 를 `company_code='*'` 로 prepend. 다른 3개는 메타 미포함.
|
||||
|
||||
**runFanOut 의 마지막 `true` 는 `wrapSearchWithPercent` (메타 포함 아님)** — `/lang-keys` 가 0건 나온 이유는 회사 DB 자체에 `MULTI_LANG_KEY_MASTER` 행이 없기 때문 (`failed=0` 으로 SELECT 자체는 정상). 이전 §3 의 "TEST01: 646건" 은 시점이 다른 측정 (시드 직후 또는 라우팅 다른 컨텍스트) 으로 추정.
|
||||
|
||||
### 9.3 검증된 항목 (체크)
|
||||
|
||||
- [x] `companies_queried: 2` — 메타 `listActiveCompanies` 가 두 회사 반환, Aggregator 가 둘 다 호출
|
||||
- [x] `company_code` 강제 주입 — 회사 DB 행에 라우팅 정보 기준 `company_code` 박힘 (`/batches` 14행 확인)
|
||||
- [x] `includeMeta=true` 전용 prepend — `/users` 의 `*` 8행
|
||||
- [x] 실패 격리 (`companies_failed: 0`, X-CrossTenant-Failed 헤더 없음) — 두 회사 모두 정상이라 fail-open 자체는 미실증 (TEST02 다운 시뮬레이션은 별도)
|
||||
- [x] 페이지네이션 cap — 모든 회사가 cap 200 미만이라 `truncated: false`. cap 동작은 2026-04-28 §3.5 에서 이미 검증 (646→200)
|
||||
- [x] 권한 가드 — 토큰 없이 `/_active-companies` 호출 → `403 super_admin_required`
|
||||
|
||||
### 9.4 미검증 (남은 시나리오)
|
||||
|
||||
- §11.2 부분 실패 — TEST02 DB pg_terminate 후 fan-out, `companies_failed: 1` + `X-CrossTenant-Failed: TEST02` 확인
|
||||
- §11.4 락 비획득 — TEST02 풀 maxPool 점유 후 `connection-timeout`
|
||||
- §11.5 캐시 무효화 — 캐시 자체가 Phase D 라 N/A
|
||||
|
||||
### 9.5 명령 (재현용)
|
||||
|
||||
```bash
|
||||
TOKEN='<SUPER_ADMIN JWT>'
|
||||
for ep in users roles batches lang-keys; do
|
||||
curl -s -D /tmp/h.$ep -o /tmp/b.$ep \
|
||||
-H "Authorization: Bearer $TOKEN" -H "Host: localhost:9771" \
|
||||
"http://localhost:8081/api/admin/cross-tenant/$ep"
|
||||
done
|
||||
|
||||
python3 <<'PY'
|
||||
import json, collections
|
||||
for ep in ["users","roles","batches","lang-keys"]:
|
||||
d = json.load(open(f"/tmp/b.{ep}"))["data"]
|
||||
codes = collections.Counter(r.get("company_code") for r in d["rows"])
|
||||
print(f"[{ep}] total={d['total']} q={d['companies_queried']} failed={d['companies_failed']} by={dict(codes)}")
|
||||
PY
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user