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
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