1
0
forked from baron/baron-sso
Files
baron-sso/docs/admin-user-list-ssot-cache-and-local-user-db.md

7.8 KiB

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/userslistIdentitiesFromMirrorOrKratos()를 통해 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 이하를 유지합니다.