forked from baron/baron-sso
111 lines
7.8 KiB
Markdown
111 lines
7.8 KiB
Markdown
# Admin User List SSOT, Cache, and Local User DB 제거 정책
|
|
|
|
작성일: 2026-06-17
|
|
|
|
## 결론
|
|
|
|
AdminFront 사용자 목록은 테넌트 목록과 같은 cursor/search 경험을 제공해야 하지만, 사용자 identity의 SSOT는 Ory Kratos입니다. 성능 목표를 위해 cache를 사용할 수 있고 사용해야 하지만, cache나 PostgreSQL `users` table을 사용자 identity/profile/소속 조회의 원장 또는 read model로 사용하면 안 됩니다.
|
|
|
|
테넌트 목록은 현재 primary DB cursor/search/sort query와 batch aggregation으로 개선되어 있으며, Redis cache는 필수 구현으로 들어가지 않았습니다. 다만 #1191의 정책처럼 Redis cache는 tenant tree edge, scope 계산, page response, member count aggregate 같은 보조 성능 계층으로 허용됩니다.
|
|
|
|
사용자 목록은 데이터 원장이 Kratos이고 목록 조회가 Redis identity mirror를 거치므로, 목표 수치에 도달하려면 단순 KV mirror 전체 scan이 아니라 cursor/search/scope에 맞는 Redis index cache가 필요합니다.
|
|
|
|
## 사용자 목록 SSOT 원칙
|
|
|
|
- Kratos `identities`가 subject, credentials, recovery/verifiable address, identity state의 원장입니다.
|
|
- Redis identity mirror/index는 Kratos identity를 빠르게 조회하기 위한 cache입니다.
|
|
- Backend DB `users`는 Kratos를 대체하는 사용자 원장도, 사용자 조회 read model도 아닙니다.
|
|
- 사용자 목록 API는 Kratos에서 warm-up된 Redis mirror를 기준으로 응답해야 하며, identity 존재 여부와 identity total은 Kratos mirror 기준을 우선해야 합니다.
|
|
- Redis mirror가 stale/failed/empty이면 이를 숨기지 말고 API 응답 또는 운영 상태에 드러내야 합니다.
|
|
- cache miss 또는 drift가 발견되면 Kratos 기준으로 Redis mirror를 보정해야 하며, local `users` DB를 정상 fallback처럼 사용하면 안 됩니다.
|
|
|
|
## Cache 사용 가능 범위
|
|
|
|
허용되는 cache:
|
|
|
|
- `identity:mirror:{identityID}`: Kratos identity summary JSON
|
|
- `identity:mirror:state`: mirror status, observed count, refreshed time, error
|
|
- sorted/index set: `createdAt,id` 기반 cursor page 조회
|
|
- normalized search token index: email, name, login ID, phone, selected metadata search
|
|
- tenant access key index: primary tenant, joined tenant, additional appointment tenant id/slug
|
|
- short TTL page response cache: role/scope/search/tenantSlug/sort/direction/cursor/limit 포함 key
|
|
|
|
제약:
|
|
|
|
- cache key는 query scope를 모두 포함해야 합니다.
|
|
- 권한 범위가 들어간 cache는 user id 또는 permission scope hash를 포함해야 합니다.
|
|
- Kratos write-through 실패 시 mirror state를 `stale` 또는 `failed`로 전환해야 합니다.
|
|
- Redis 장애 시 local DB를 identity SSOT fallback처럼 사용하지 않습니다. 가능한 경우 Kratos API fallback을 쓰고, 불가능하면 identity mirror unavailable로 실패시킵니다.
|
|
- cache는 감사 근거 또는 권한 판정 단독 근거가 아닙니다. 권한 관계의 원장은 Keto입니다.
|
|
|
|
## 현재 사용자 목록 병목
|
|
|
|
현재 `GET /v1/admin/users`는 `listIdentitiesFromMirrorOrKratos()`를 통해 identity mirror 전체를 배열로 읽은 다음 handler 메모리에서 검색, tenant filter, 권한 scope, sort, pagination을 수행합니다.
|
|
|
|
Redis mirror 구현도 `SCAN identity:mirror:*` 후 key별 `GET`으로 전체 identity를 materialize합니다. 따라서 API 응답은 cursor를 반환하지만, 서버 내부 비용은 page 단위가 아니라 전체 mirror 크기에 비례합니다.
|
|
|
|
테넌트 목록과 같은 성능 특성을 만들려면 `/v1/admin/users`의 cursor/search/scope 조건이 Redis identity index 또는 Kratos-backed query boundary까지 내려가야 합니다.
|
|
|
|
## Local `users` DB 잔존 의존 제거 대상
|
|
|
|
PostgreSQL `users` table은 admin user list, org-context, orgfront snapshot의 사용자 identity/profile/소속 read model로 사용하면 안 됩니다. 현재 코드 기준으로 남아 있는 의존은 유지 근거가 아니라 제거 또는 별도 원장 재정의가 필요한 대상입니다.
|
|
|
|
| 기능 | 사용 위치 | 정리 방향 |
|
|
| --- | --- | --- |
|
|
| 사용자 생성/수정 후 local sync | `user_handler.go`, `auth_handler.go` | Kratos write-through 후 Redis mirror 갱신으로 대체하고 local sync 제거 여부를 추적합니다. |
|
|
| custom login ID index | `user_login_ids`, `IsLoginIDTaken`, `FindTenantIDByLoginID` | Kratos traits/credentials identifier 또는 별도 명시 원장으로 재정의합니다. |
|
|
| tenant membership count | `TenantHandler.countTenantMembers`, `UserRepo.CountByTenantIDs` | Redis identity mirror/Keto relation 기준 aggregate로 대체합니다. |
|
|
| org context member export | `TenantHandler.loadOrgContextMembers` | Kratos identity mirror 기준으로 전환합니다. |
|
|
| admin CSV export/import | `ExportUsersCSV`, bulk create/update | Kratos/mirror 기반 export와 command import로 분리합니다. |
|
|
| WORKS Mobile sync/comparison | `worksmobile_sync_service.go` | Kratos identity mirror와 WORKS API 비교로 전환합니다. |
|
|
| Hanmac email/local-part policy | `hanmac_email_policy.go` | Kratos identifier/mirror index 기준 uniqueness로 전환합니다. |
|
|
| user status 운영 정책 | `users.status`, `deleted_at` | Kratos traits/state 또는 명시된 별도 원장으로 재정의합니다. |
|
|
| profile/session 보조 | `auth_handler.go` | Kratos session/identity traits와 Redis mirror 기준으로 전환합니다. |
|
|
|
|
따라서 local `users` DB를 사용자 조회의 primary source 또는 read model로 유지하는 방향은 최신 정책과 맞지 않습니다.
|
|
|
|
## 사용자 목록 개선 방향
|
|
|
|
1. Redis identity mirror index를 확장합니다.
|
|
- 기본 정렬: `createdAt desc, id desc`
|
|
- cursor: timestamp + identity id
|
|
- search: normalized email/name/login ID/phone/token index
|
|
- tenant filter: primary tenant id/slug와 additional appointments 기반 index
|
|
|
|
2. `/v1/admin/users` query boundary를 분리합니다.
|
|
- 입력: `limit`, `cursor`, `search`, `tenantSlug`, `sort`, `direction`, requester scope
|
|
- 출력: page identities, `identityTotal`, `nextCursor`, `mirrorStatus`
|
|
- handler는 전체 identity slice를 직접 만들지 않습니다.
|
|
|
|
3. row enrichment는 page 단위 batch로 수행합니다.
|
|
- primary tenant summary lookup batch
|
|
- joined tenant/relation lookup batch 또는 bounded cache
|
|
- per-row `GetTenant`/`ListJoinedTenants` 반복 호출 금지
|
|
|
|
4. local `users` DB join을 제거합니다.
|
|
- 사용자 identity/profile/소속/상태는 Kratos mirror 기준으로 응답
|
|
- Kratos mirror에 없는 local DB row를 목록 identity로 승격하지 않음
|
|
- 남은 local DB 의존은 drift/deprecation report에만 노출
|
|
|
|
5. AdminFront는 tenant 목록과 같은 query lifecycle로 맞춥니다.
|
|
- `useInfiniteQuery` queryKey에 normalized/deferred search, tenantSlug, sort, direction 포함
|
|
- 검색/정렬 변경 시 cursor를 재시작
|
|
- tenant filter option 로딩은 사용자 첫 page 렌더를 막지 않음
|
|
|
|
## 테스트 기준
|
|
|
|
RED 테스트는 다음 실패를 먼저 보여야 합니다.
|
|
|
|
- search/cursor 요청에서 backend가 전체 mirror를 매번 materialize하는 경로가 호출됨
|
|
- 첫 page 밖 사용자가 검색되어야 하지만 cursor/search/index 계약이 없어서 전체 scan에 의존함
|
|
- page item 50개 매핑 시 tenant lookup 또는 joined tenant lookup이 row 수만큼 반복됨
|
|
- Redis mirror stale 상태에서 local DB만으로 정상 목록처럼 응답함
|
|
|
|
최종 통과 기준:
|
|
|
|
- Kratos mirror 기준 identity result와 API `items`가 동일합니다.
|
|
- Redis index cache hit/miss가 같은 결과를 냅니다.
|
|
- Redis stale/failed 상태가 숨겨지지 않습니다.
|
|
- local DB row만 있고 Kratos mirror에 없는 사용자는 일반 admin user list에 identity item으로 나오지 않습니다.
|
|
- 3,500건 이상 사용자 데이터에서 첫 화면 1500ms 이하, 검색 500ms 이하를 유지합니다.
|