Skip to content

휘발성 레코드 보존 정리 (멱등 레코드·만료 refresh token) - #224

Merged
i3months merged 1 commit into
devfrom
feat/volatile-record-retention
Aug 24, 2026
Merged

휘발성 레코드 보존 정리 (멱등 레코드·만료 refresh token)#224
i3months merged 1 commit into
devfrom
feat/volatile-record-retention

Conversation

@i3months

Copy link
Copy Markdown
Member

문제

루트 CLAUDE.md 는 "Redis 미사용 — 휘발성 데이터는 PostgreSQL 의 short-lived 레코드" 로 처리한다고 명시한다. 그런데 두 테이블은 지우는 쪽이 없어 사실상 영구 보관이었다. short-lived 라는 전제가 코드로는 지켜지지 않았다.

processed_messages

AI 콜백마다 한 행씩 쌓인다 — 질문 풀·꼬리질문·피드백·분석 4종·음성·TTS. 면접 한 번에 수십 행이다.

의도는 분명히 있었다. 테이블 생성(V4) 때 idx_processed_messages_processed_at 인덱스를 함께 만들었는데, 그 컬럼으로 조회하는 코드가 하나도 없다. 리포지토리에는 existsById(PK 조회) 뿐이다. 보존 정리를 전제로 만든 인덱스인데 정작 정리하는 쪽이 안 만들어진 것이다.

refresh_tokens

로그인마다 한 행. 만료된 토큰은 검증에서 항상 걸러지므로 남겨둬도 무효인데 쌓이기만 한다.

수정

도메인별 스위퍼 2개 (기본 24시간 주기).

보존 기간이 이 변경의 핵심 판단이다. 멱등 레코드를 너무 일찍 지우면 DLQ 에서 늦게 재주입된 메시지가 "처음 보는 메시지" 가 되어 다시 처리된다 — 질문이 두 번 붙거나 피드백이 재생성될 수 있다. 즉 이 정리는 "공간 회수"보다 "멱등성을 언제까지 보장하느냐" 의 문제다. 기본 30일은 현실적인 재전달·수동 재주입 창보다 충분히 길다.

revoked 이지만 아직 만료 전인 refresh token 은 남긴다 — 탈퇴·로그아웃 시 revoke 한 기록이 만료 시각까지는 "이 토큰은 무효"라는 판단 근거로 필요하다.

왜 스위퍼를 둘로 나눴나

하나로 묶으면 common.messaging.applicationauth.domain.RefreshTokenRepository 를 참조하게 된다. common 은 최하위 공용 레이어라 방향이 거꾸로다. ArchUnit 은 통과했지만(순환으로 잡히지 않았다) 통과한다고 옳은 건 아니라서 도메인별로 나눴다.

대상이 아닌 것

oauth_states발급할 때마다 만료분을 지워 스스로 자정한다(OAuthStateService.issueStateWithPkce 가 insert 전에 deleteByExpiresAtBefore). 이미 해결돼 있어 건드리지 않았다.

테스트

실제 Postgres 로 "기한이 지난 것만" 지우는지 양쪽 확인:

  • 30일 넘은 멱등 레코드는 지우고 최근 것은 남긴다 (지우면 재전달 메시지가 중복 처리된다)
  • 만료된 refresh token 은 지우고 유효한 것은 남긴다 (지우면 로그인 세션이 끊긴다)

두 테스트 모두 "지운다"보다 "안 지운다" 쪽이 본체다.

검증 한계

로컬 Docker 데몬이 여전히 응답하지 않아 DB 테스트 2건은 CI 로 검증한다. 컴파일·ArchUnit 은 로컬 통과.

운영

MESSAGING_PROCESSED_MESSAGE_RETENTION_DAYS(30) · MESSAGING_VOLATILE_SWEEP_INTERVAL_MS(24h) · AUTH_REFRESH_TOKEN_SWEEP_INTERVAL_MS(24h). docs/environment.md 갱신 — 보존 기간을 줄일 때의 위험도 함께 적었다.

이 프로젝트는 Redis 를 쓰지 않고 "휘발성 데이터는 PostgreSQL 의 short-lived
레코드"로 처리하기로 했다(루트 CLAUDE.md). 그런데 아래 둘은 지우는 쪽이 없어
사실상 영구 보관이었다 — short-lived 라는 전제가 코드로 지켜지지 않았다.

- processed_messages: AI 콜백마다 한 행(질문·꼬리질문·피드백·분석·음성·TTS).
  idx_processed_messages_processed_at 인덱스가 처음부터 있는데 그 컬럼으로
  조회하는 코드가 하나도 없었다 — 보존 정리를 전제로 만든 인덱스인데 정작
  정리가 없었던 셈이다.
- refresh_tokens: 로그인마다 한 행. 만료된 것은 검증에서 항상 걸러지므로
  남겨둬도 무효인데 쌓이기만 한다.

보존 기간은 재전달 창보다 길어야 한다 — 너무 일찍 지우면 DLQ 에서 늦게 재주입된
메시지가 "처음 보는 메시지"가 되어 다시 처리된다(질문 중복·피드백 재생성).
기본 30일.

스위퍼를 도메인별로 나눴다. 하나로 묶으면 common 이 auth.domain 을 참조하게 되는데
common 은 최하위 공용 레이어라 방향이 거꾸로다(ArchUnit 은 통과했지만 설계상 아님).

oauth_states 는 발급 때마다 만료분을 지워 스스로 자정하므로 대상이 아니다.
@i3months
i3months merged commit 455c0ef into dev Aug 24, 2026
5 checks passed
@i3months
i3months deleted the feat/volatile-record-retention branch August 24, 2026 04:16
i3months added a commit to i3months/stackup that referenced this pull request Aug 28, 2026
Team-StackUp#224 에서 쓴 렌즈("인덱스는 있는데 그 컬럼을 쓰는 코드가 없다")로 전체 인덱스를
훑은 결과다.

ai_request_logs 는 LLM 호출마다 한 행이라 이 코드베이스에서 가장 빨리 자라는
테이블이다 — 피드백 한 번에 10여 건(패널·첫인상·직무적합·인성·답변별 코칭),
TTS 는 문장마다. created_at 계열 인덱스가 3개나 있는데 조회하는 코드도,
정리하는 코드도 없었다.

보존 90일로 넉넉히 잡았다. 이 테이블의 가치는 비용 추이라 멱등 레코드와 성격이
다르다 — 지운다고 동작이 깨지지는 않지만 짧게 잡으면 학기 단위 비교가 불가능해진다.

함께: idx_user_consents_user_id 는 idx_user_consents_user_type
(user_id, consent_type) 의 leftmost prefix 로 완전히 커버된다. 실제 쿼리 두 개
모두 복합 인덱스로 처리되므로 조회에 보탬 없이 쓰기 비용만 더한다.

훑는 김에 확인한 미구현: activity_logs(US-31)는 테이블·엔티티·리포지토리만 있고
읽기도 쓰기도 없다. 인덱스 3개도 함께 놀고 있다. 삭제 대상이 아니라 계획된
기능이므로 docs/observability.md 에 미구현으로 명시만 했다.
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