Skip to content

과거 삭제분·파기 실패분의 스토리지 객체 회수 - #223

Merged
i3months merged 1 commit into
devfrom
fix/purge-legacy-deleted-content
Aug 24, 2026
Merged

과거 삭제분·파기 실패분의 스토리지 객체 회수#223
i3months merged 1 commit into
devfrom
fix/purge-legacy-deleted-content

Conversation

@i3months

Copy link
Copy Markdown
Member

문제 — #219 의 빠진 절반

#222 에서 "제약을 완화할 때 기존 데이터 백필이 필요한지 확인할 것"이라는 교훈을 얻었는데, #219 에도 같은 구멍이 있었다.

#219"앞으로의 삭제"만 다룬다. 두 부류가 남는다.

1. 그 이전에 지운 자료 — 행만 soft delete 됐고 S3 의 원본 PDF·분석 마크다운, document_embeddings 청크가 그대로다. 앞으로 삭제 이벤트가 다시 발생할 일이 없으니 백필하지 않으면 영원히 남는다. V31(탈퇴자 GitHub 토큰)과 정확히 같은 종류의 누락이다.

2. #219 의 파기 실패분 — AFTER_COMMIT 파기는 실패해도 사용자 요청을 실패시키지 않는다(그 시점엔 이미 커밋됐고 되돌릴 수도 없다 — #219 에서 의도한 설계다). 그래서 스토리지가 잠깐 죽어 있으면 객체가 로그 한 줄만 남기고 샌다. 그때 남긴 ERROR 로그는 "수동 회수 필요"라고 말하는데, 수동 회수 수단이 없었다.

수정

OrphanedObjectSweeper 가 둘을 같은 경로로 회수한다 (기본 15분 주기).

  • 삭제됐는데 경로가 남아 있는 행을 찾아 S3 객체를 지우고 경로를 비운다
  • 회수에 성공해야만 경로를 비운다 — 실패하면 다음 주기에 재시도
  • 한 건이 실패해도 나머지는 계속 회수한다(예외를 전파하면 같은 배치가 통째로 롤백된다)

임베딩 청크는 DB 안에서 끝나므로 V32 가 한 번에 정리한다.

파괴적 작업이라는 점

이 스위퍼는 S3 객체를 실제로 지운다. 대상 선정이 하나라도 어긋나면 살아있는 사용자의 이력서 원본이 사라진다. 그래서:

  • 파생 쿼리 이름에 DeletedTrue 를 박아 조건이 메서드 시그니처에 드러나게 했다 (findTop100ByDeletedTrueAndFilePathIsNotNull)
  • 실제 Postgres 로 "살아있는 행은 절대 안 잡힌다" 를 고정했다
  • 배치 상한 100

V32 가 CHECK 을 완화하는 이유

회수 완료 표시로 file_path 를 비워야 하는데 chk_resumes_locator_by_type 이 막는다(PDF 는 file_path NOT NULL). #198ck_users_provider_identity 가 막았던 것과 같은 상황이다.

제약의 의도는 "살아있는 행은 갖춰야 한다" 이지 "지운 행도 영원히 들고 있어라" 가 아니므로, 삭제된 행을 예외로 둔다. users 의 provider 식별자 CHECK(V28)·유니크 인덱스(V3·V22)가 이미 쓰는 규약이다.

경로를 비우는 것 자체도 옳다 — 객체가 없는데 키만 남으면 "아직 회수 안 됨"과 구분되지 않아 매 주기 같은 키를 다시 지우려 든다.

테스트

단위 4건 (로컬 통과): 회수 후 경로 비움 / 실패 시 경로 유지(재시도) / 한 건 실패해도 나머지 진행 / 대상 없으면 무동작

DB 3건: 이력서 조회가 살아있는 행·이미 회수된 행을 안 잡는지, 문서 조회 동일, 그리고 삭제된 행의 file_path 비우기가 CHECK 에 막히지 않는지(V32 검증)

검증 한계

로컬 Docker 데몬이 여전히 응답하지 않아 DB 테스트 3건은 CI 로 검증한다. 컴파일과 단위 테스트는 로컬에서 통과했다.

운영

STORAGE_ORPHAN_SWEEP_INTERVAL_MS(기본 900000) / STORAGE_ORPHAN_SWEEP_INITIAL_DELAY_MS(기본 60000). docs/environment.md·docs/security.md §5.1.1 갱신.

배포 직후 첫 주기에 과거 삭제분이 한꺼번에 회수되므로, 그때 orphaned storage object purged 로그 건수로 잔존 규모를 확인할 수 있다.

#219 는 "앞으로의 삭제"만 다뤘다. 두 부류가 남는다.

1. 그 이전에 지운 자료 — 행만 soft delete 됐고 S3 원본 PDF·분석 마크다운이
   그대로다. 앞으로 삭제 이벤트가 다시 날 일이 없어 백필하지 않으면 영원히 남는다.
   V31(탈퇴자 GitHub 토큰)과 같은 종류의 누락이다.
2. #219 의 파기 실패분 — AFTER_COMMIT 파기는 실패해도 사용자 요청을 실패시키지
   않는다(이미 커밋됐고 되돌릴 수도 없다). 스토리지가 잠깐 죽으면 로그 한 줄만
   남기고 객체가 샌다.

OrphanedObjectSweeper 가 둘을 같은 경로로 회수한다. 임베딩 청크는 DB 안에서
끝나므로 V32 가 한 번에 정리한다.

파괴적 작업이라 대상 선정이 핵심이다. 파생 쿼리 이름에 DeletedTrue 를 박아
조건이 시그니처에 드러나게 하고, 실제 DB 로 "살아있는 행은 절대 안 잡힌다"를
고정했다. 회수 성공 시에만 경로를 비우고(실패는 다음 주기 재시도), 한 건이
실패해도 나머지는 계속 회수한다.

V32 는 삭제된 행을 resumes 의 타입별 locator CHECK 에서 제외한다 — 회수 완료
표시로 file_path 를 비워야 하는데 제약이 막는다. users 의 provider 식별자
CHECK(V28)·유니크 인덱스(V3·V22)가 이미 쓰는 규약이다.
@i3months
i3months merged commit 15082d2 into dev Aug 24, 2026
5 checks passed
@i3months
i3months deleted the fix/purge-legacy-deleted-content branch August 24, 2026 03:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant