얼쑤에서는 연주를 분석한 뒤 마음에 드는 기록을 저장하고 나중에 녹음을 다시 들을 수 있습니다. 처음에는 R2의 수명 주기 규칙이 녹음 객체를 생성한 지 30일이 지나면 삭제 대상으로 잡았습니다. 연습 기록을 저장했다는 사실은 DB에 있었기 때문에 같은 접두사 아래의 객체에 적용되는 규칙은 그 차이를 알 수 없었습니다. 저장한 녹음을 계속 들을 수 있게 하려면, 어떤 녹음을 정리할지 정하는 곳부터 바꿔야 했습니다. Cloudflare R2의 객체 수명 주기
업로드 시각과 저장 여부
저는 30일 정리를 R2 규칙에서 백엔드의 예약 작업으로 옮겼습니다. 저장한 녹음은 회원이 기록을 지우거나 탈퇴 삭제가 완료될 때까지 보관하고, 예약 작업은 미저장 녹음만 고릅니다. DB에 녹음 키가 있고, 연습 기록의 saved_at이 비어 있으며, 업로드 성공 시각인 ended_at이 이번 실행의 UTC 기준 시각보다 30일 넘게 앞선 세션이 대상입니다. SQL로는 ended_at < cutoff이고, cutoff는 현재 UTC 시각에서 30일을 뺀 값입니다. 정확히 30일이 된 녹음은 이번 실행에서 건너뜁니다.
연습 세션을 만든 뒤 업로드할 수 있는 40분은 세션 시작 시각부터 셉니다. 읽기용 서명 URL의 15분은 URL을 발급한 시각부터 셉니다. 녹음 정리는 업로드에 성공한 시각을 따로 기록해 그때부터 계산합니다.
삭제 직전 DB에서 대상 확보
작업이 미저장 녹음 목록을 읽은 직후에 사용자가 기록을 저장할 수 있습니다. 그 상태에서 목록에 있던 파일을 바로 지우면, 방금 저장한 기록이 사라진 녹음을 가리킵니다. 그래서 파일을 지우기 직전에 DB에서 세션 ID, 처음 읽었던 녹음 키, 저장 여부와 30일 경계를 다시 확인하고, 조건을 모두 만족할 때만 키를 비우도록 했습니다.
UPDATE practice_sessions
SET recording_url = NULL
WHERE id = :session_id
AND recording_url = :selected_key
AND saved_at IS NULL
AND ended_at < :cutoff
먼저 저장한 요청이 saved_at을 채웠다면 이 갱신은 0행을 바꾸고, 정리 작업은 파일을 그대로 둡니다. 갱신이 1행에 반영됐을 때만 읽어 둔 키의 객체를 지웁니다. 반대로 정리 작업이 먼저 키를 비웠다면, 뒤이은 기록 저장은 성공하더라도 해당 녹음은 다시 듣기에 표시되지 않습니다. 목록을 읽은 때와 실제로 삭제할 자격을 확보하는 때를 나눈 이유입니다.
삭제 실패 시 녹음 키 복구
키를 비운 뒤 저장소 삭제에서 오류가 나면, 세션이 남아 있고 기록이 삭제되지 않았으며 녹음 키가 여전히 비어 있을 때만 기존 키를 돌려놓습니다. 미저장 상태가 이어지면 다음 실행에서 다시 시도할 수 있습니다. 그사이 기록을 저장했다면 복구된 녹음은 저장한 기록에 남습니다.
DB의 키 비우기와 복구는 각각 짧은 갱신으로 끝내고, 그 사이에 저장소를 호출합니다. 저장소 응답을 기다리는 동안 DB 행을 잠그지 않아도 됩니다. 키를 비운 직후 프로세스가 중단되거나 삭제 실패 뒤 돌려놓을 행이 사라지면, DB 참조가 없는 객체가 남을 수 있습니다.
앱 권한과 버킷 관리 권한 분리
백엔드의 S3 호환 클라이언트에는 Access Key ID와 Secret Access Key를 사용했습니다. 관리 API의 토큰 값과 구분되는 S3 자격 증명입니다. 권한은 비공개 녹음 버킷 하나의 객체 읽기·쓰기 범위로 제한했습니다. 이 자격 증명으로 앱은 객체를 쓰거나 지우고, 분석 서버에 건넬 읽기용 URL에 서명할 수 있습니다. 분석 서버는 서명된 주소만 받아 녹음을 읽습니다. 버킷 수명 주기 같은 설정은 별도의 관리 권한으로 다룹니다. Cloudflare R2의 인증과 토큰 권한
테스트와 운영 전환에서 확인한 범위
고정 시계를 적용한 폐기용 MySQL 8.4에서 실제 조회와 갱신 SQL을 실행했습니다. 저장소는 삭제 요청을 받아 키를 기록하는 대역으로 바꿨습니다. 한 묶음에 한 행씩 읽게 하자, 30일을 1마이크로초 넘긴 녹음을 포함해 미저장 녹음 2건의 키가 비워졌고 같은 2건의 삭제가 요청됐습니다. 정확히 30일인 녹음과 그보다 오래된 저장 기록의 키는 남았습니다.
별도 검사에서는 목록을 읽은 뒤 저장 시각을 채웠습니다. 이어서 정리 갱신을 시도하자 0행이 바뀌고 녹음 키가 남았습니다. 저장소 대역이 삭제 오류를 던지게 한 경우에는 삭제 0건, 키 복구 1건이 기록됐습니다.
운영에서는 백엔드 작업을 배포한 뒤 기존 버킷의 일괄 삭제 규칙과 이관 파일을 시간 기준으로 지우던 작업을 제거했습니다. 백엔드 자격 증명으로는 버킷의 수명 주기 규칙을 조회할 때 AccessDenied가 돌아와, 규칙 제거는 관리 화면에서 확인했습니다. 다음 날 첫 예약 실행은 6ms에 끝났고 삭제·건너뜀·복구·실패가 모두 0건이었습니다. 그때 운영에는 30일을 넘긴 녹음이 없었습니다. 운영 기록으로는 예약 작업이 실행됐음을 확인했고, 실제로 지울 대상이 생겼을 때의 동작은 위 격리 검사에서 확인한 범위입니다.

댓글
댓글과 반응은 GitHub 계정으로 로그인해 남길 수 있어요.