6.3 KiB
사용자 projection 가시성 감사 보고서
작성 시각: 2026-06-08 16:55 KST
관련 이슈:
- #1035: adminfront 사용자 레지스트리 total이 Kratos 250건 제한으로 잘못 표시됨
- #1036: 사용자 projection 가시성 영향 범위 검증 및 WORKS 비교 표 row count 표시
결론
총 250명 표시는 단순 UI 문제가 아니라, Kratos partial list를 full snapshot처럼 처리한 projection 동기화 버그였습니다.
현재 로컬 DB와 API는 다음 상태입니다.
| 항목 | 건수 | 설명 |
|---|---|---|
| users 전체 | 2,114 | deleted_at 포함 전체 row |
| visible users | 1,917 | deleted_at IS NULL, adminfront/orgfront 사용자 목록 기준 |
| soft-deleted users | 197 | 사용자 삭제 또는 과거 projection 문제로 숨겨진 row |
| CSV 원본 줄 수 | 1,887 | 헤더 포함 |
| CSV 실제 데이터 행 | 1,886 | 헤더 제외 |
| 이번 import 사용자 | 1,886 | 모두 DB 매칭, 모두 visible |
| import ID에 없는 기존 사용자 | 228 | visible 31, soft-deleted 197 |
따라서 “기존 221명 + 신규 1887명” 계산은 그대로 적용하면 안 됩니다.
1887은 CSV 헤더 포함 줄 수이고 실제 신규 데이터 행은1886입니다.- 현재 DB에서 import ID에 없는 기존 사용자는
228명입니다. - 그중
197명은 soft-delete 상태라 adminfront/orgfront visible total에는 포함되지 않습니다. - 현재 화면 기준 총 사용자는
신규 visible 1886 + 기존 visible 31 = 1917입니다.
생성한 대조 파일
보고 파일 위치:
reports/user-projection-visibility-audit-20260608-1645/existing_users_not_in_saman_import.csvreports/user-projection-visibility-audit-20260608-1645/imported_users_missing_or_soft_deleted.csv
파일 내용:
existing_users_not_in_saman_import.csv: 이번 CSV import ID에 없는 기존 사용자 228명 전체 목록입니다.imported_users_missing_or_soft_deleted.csv: 이번 import 사용자 중 DB 누락 또는 soft-delete 상태인 사용자 목록입니다. 현재는 헤더만 있고 데이터 row는 0건입니다.
삭제 정책 확인
adminfront 사용자 삭제는 다음 순서로 동작합니다.
- Kratos identity 삭제를 시도합니다.
- WORKS 연동 범위 사용자이면 WORKS delete outbox를 enqueue합니다.
- 로컬
usersrow는 GORMDelete로 soft-delete 합니다.
따라서 adminfront에서 사용자 삭제를 했다고 로컬 DB row가 hard-delete 되는 구조는 아닙니다. users.deleted_at에 값이 들어가고, 일반 조회에서는 제외됩니다.
예외적으로 사용자 생성/재생성 시 email unique 충돌을 풀기 위한 Unscoped hard-delete 경로가 일부 존재합니다. 이 경로는 일반 사용자 삭제 정책과 다릅니다.
영향 범위 검증
adminfront 사용자 레지스트리
GET /api/v1/admin/users?limit=50&offset=0- 응답 total:
1917 - adminfront
/users화면 문구:총 1917명의 사용자가 등록되어 있습니다.
orgfront 사용자 소비 경로
현재 orgfront 조직도와 picker는 fetchUsers(5000, 0) 형태로 사용자 목록을 한 번 가져옵니다.
검증 결과:
GET /api/v1/admin/users?limit=5000&offset=0- items:
1917 - total:
1917 - soft-deleted 기존 사용자 197명 중 응답 포함:
0
현재 visible 사용자가 5,000명 미만이라 orgfront의 현 방식은 전체 visible 사용자를 모두 받습니다. 다만 사용자가 5,000명을 넘으면 partial 조회가 될 수 있으므로 cursor 기반 전환이 필요합니다.
WORKS comparison API와 화면
WORKS comparison은 Baron visible 사용자뿐 아니라 WORKS에만 존재하는 remote row도 함께 보여주는 비교 화면입니다. 따라서 사용자 목록 visible total과 1:1로 일치하지 않습니다.
includeMatched=true 기준 최신 API 결과:
| 구분 | 전체 | matched | missing_in_worksmobile | missing_external_key | missing_in_baron |
|---|---|---|---|---|---|
| users | 2,110 | 1,874 | 41 | 6 | 189 |
| groups | 187 | 185 | 1 | - | 1 |
WORKS 화면 row count 표시:
- 구성원:
표시 232 / 전체 2110 - 조직/그룹:
표시 2 / 전체 187
구성원 표시 232는 기본 필터와 보호 계정 제거가 적용된 화면 노출 row 수입니다. 전체 2110은 API comparison row 전체입니다.
soft-deleted 기존 사용자 197명과 WORKS comparison baronId를 대조한 결과:
- soft-deleted Baron row 포함:
0
왜 업데이트가 많았는가
이번 문제는 단일 UI 카운트 문제가 아니라 다음 경계가 한꺼번에 얽힌 문제였습니다.
- Kratos identity list는 partial list인데 backend projection sync가 full snapshot으로 처리했습니다.
- projection sync가 local DB soft-delete까지 수행해 사용자 가시성 자체를 손상시켰습니다.
- adminfront 사용자 목록, orgfront 조직도, WORKS comparison이 모두 사용자 projection을 다른 방식으로 소비하고 있었습니다.
- WORKS comparison은 사용자 목록이 아니라 Baron/WORKS 양쪽 차이를 보여주는 비교 화면이라 total 의미가 달랐습니다.
- 운영자가 partial data인지 바로 볼 수 있도록 WORKS 표 row count가 필요했습니다.
pagination 정리
현재 구조는 다음과 같습니다.
- Ory/Kratos -> backend projection sync: 현재
ListIdentities()partial 조회입니다. offset/cursor 전체 순회가 아닙니다. - backend projection -> adminfront 사용자 목록:
cursor가 있으면 cursor pagination, 없으면 offset pagination을 받습니다. adminfront는 infinite query로nextCursor를 사용합니다. - backend projection -> orgfront 조직도/picker: 현재
limit=5000&offset=0단일 offset 조회입니다. - WORKS comparison: backend가 비교 결과 배열을 만들어 내려주고, adminfront가 검색/필터 후 화면 row를 표시합니다.
재발 방지 조치
- 사용자 목록 API는 Kratos가 아니라 local projection DB를 primary source로 사용합니다.
- Kratos partial list에 없는 사용자를 projection sync에서 삭제하지 않도록 수정했습니다.
- WORKS comparison에서 soft-deleted local user가 들어와도 comparison row로 노출되지 않도록 방어 테스트와 로직을 추가했습니다.
- WORKS comparison 표에
표시 N / 전체 Mrow count를 표시했습니다.