Rhymix R2 Attachment Storage
서울파인트리클럽 홈페이지 운영 개편에서는 회원과 게시글을 그대로 유지하면서 웹호스팅의 저장 공간 문제를 해결해야 했다. Cafe24에 남은 공간은 635MB였고, 오랫동안 쌓인 첨부파일이 대부분을 차지했다. 실행 환경과 데이터베이스는 Cafe24에 남기고 파일만 Cloudflare R2로 옮기면 더 작은 호스팅 상품을 사용할 수 있었다.
문제는 파일의 위치만 바꾸는 것으로 끝나지 않았다. Rhymix는 데이터베이스에 저장된 경로를 보고 내부 첨부파일인지 판단하고, 그 결과에 따라 다운로드 권한과 본문 이미지 처리를 결정한다. 내가 확인하려 한 것은 기존 데이터 형식과 권한 검사를 유지하면서 파일의 물리 저장소만 R2로 분리할 수 있는지였다.
구현 범위
Rhymix와 AWS SDK for PHP를 기반으로 rx_object_storage 모듈을 구현하고 운영하면서 수정했다. 모듈 골격에는 포에시스의 Rhymix 모듈 생성기를 사용했다. 나는 파일 경로와 객체 키의 대응 방식, 업로드 이후 R2 이전, 권한을 거치는 다운로드, 기존 자료 마이그레이션과 정합성 검사, 삭제 안전장치와 회귀 검사를 맡았다.
구현에는 AI 코딩 에이전트도 사용했다. 작업을 경로 변환, 파일 생명주기, 동기화와 삭제처럼 검증 가능한 단위로 나누고 생성된 변경을 직접 검토했다. 운영 중 발견한 오류는 커밋 이력과 재현 조건을 바탕으로 수정하고 같은 문제가 다시 생기지 않도록 검사에 추가했다.
파일은 옮기고 경로는 유지
초기 구현에서는 R2의 URL을 files.uploaded_filename에 저장했다. 업로드한 파일은 R2에 있었지만 Rhymix의 procFileDownload 경로가 정상적으로 작동하지 않았다. Rhymix가 URL 형식의 값을 외부 파일로 취급하면서 기존 첨부파일 처리 흐름에서 벗어났기 때문이다.
이후 데이터베이스에는 기존과 같은 로컬 형식의 경로를 남기고, 앞의 ./만 제거한 값을 R2 객체 키로 사용했다.
데이터베이스 경로: ./files/attach/images/example.jpg
R2 객체 키: files/attach/images/example.jpg
일반 업로드는 먼저 Rhymix의 파일 등록과 검증을 거친다. 등록이 끝나면 모듈이 파일을 R2에 올리고 해당 행을 간접 다운로드 대상으로 바꾼다. R2 전송이나 데이터베이스 갱신에 실패하면 로컬 파일을 지우지 않아 기존 경로로 복구할 여지를 남겼다. 사용자는 저장 위치가 바뀐 것을 알 필요 없이 이전과 같은 편집기에서 글과 사진을 올릴 수 있다.
권한 검사 뒤에만 열리는 다운로드
R2의 객체 URL을 본문에 그대로 넣으면 Rhymix의 문서·그룹별 다운로드 권한을 우회할 수 있다. 그래서 다운로드 요청은 기존 Rhymix 경로로 받고, 파일과 문서의 권한 검사를 통과한 뒤 10분 동안 유효한 presigned GET URL을 발급해 R2로 이동시켰다.
브라우저 → Rhymix 다운로드 권한 검사 → 임시 URL 발급 → R2
서명 URL 발급에 실패했을 때는 영구적인 공개 객체 URL로 우회하지 않는다. 로컬 백업이 남아 있는 모드에서는 Rhymix가 그 파일을 제공하고, 그렇지 않으면 기존 오류 처리로 끝난다. 저장소 장애가 접근 권한을 약화시키는 방향으로 이어지지 않도록 한 선택이다.
본문 이미지와 썸네일은 별도의 이미지 최적화 모듈이 서명된 URL로 바꾼다. 이 글에서는 파일 저장과 다운로드 권한까지만 다룬다. 회원 그룹 판정과 이미지 블러, 서명과 캐시 정책은 Rhymix 회원 그룹별 이미지 블러와 서명 URL에 정리했다.
기존 자료 이전과 정합성 검사
기존 첨부파일은 로컬 경로와 같은 객체 키로 R2에 복사했다. 업로드 뒤에는 객체의 크기와 checksum을 확인하고, 작업을 시작한 뒤 데이터베이스 행이 바뀌지 않았을 때만 경로 상태를 갱신했다. 같은 로컬 파일을 여러 행이 공유할 수 있어 마이그레이션 과정에서는 로컬 백업을 자동으로 삭제하지 않았다.
운영을 시작한 뒤에는 R2에만 남은 객체와 데이터베이스에만 남은 참조가 생길 수 있었다. 이를 확인하기 위해 R2에서 데이터베이스로, 데이터베이스에서 R2로 이어지는 양방향 검사를 만들었다. 검사는 먼저 변경 사항이 없는 dry run으로 실행하고, 누락된 객체와 복구·삭제 후보를 별도의 계획으로 남긴다.
처음에는 동기화 기능이 데이터베이스를 실제로 고치지 않거나, 과거 CDN URL과 현재 객체 키의 경로가 달라 같은 파일을 찾지 못하는 문제가 있었다. 운영 중 이 사례들을 재현하면서 경로 정규화와 데이터베이스 갱신 조건을 수정했다.
파일 삭제보다 먼저 확인할 것
첨부파일 행이 삭제될 때 R2 객체도 바로 지우는 방식은 단순하지만 안전하지 않았다. Rhymix의 삭제 트리거만으로는 데이터베이스 작업이 최종 반영됐는지 확실히 알 수 없고, 여러 행이 같은 파일을 가리킬 수도 있었다. 그래서 삭제 트리거에서는 원격 객체를 지우지 않고, 정합성 검사에서 확인된 고아 객체만 별도의 정리 과정으로 처리했다.
실제 삭제 전에는 다음 조건을 다시 확인한다.
- dry run에서 만든 계획의 식별자·버전·해시와 관리자 확인값
- 현재 데이터베이스에 같은 객체를 가리키는 행이 없는지
- 스캔 이후 객체의 ETag나 수정 시각이 바뀌지 않았는지
- 새로 올라온 파일을 지우지 않도록 최소 보존 시간이 지났는지
스캔 뒤 데이터베이스 참조가 생긴 객체, 내용이 바뀐 객체, 보존 시간이 지나지 않은 객체와 확인 중 오류가 난 객체는 삭제하지 않는다. 한 번에 처리하는 수에도 상한을 두고, 일부 항목이 남으면 완료로 표시하지 않고 다음 검토 대상으로 보존한다.
검증과 관찰
경로 변환, 다운로드 URL 처리, 마이그레이션과 정리 과정은 운영 R2 자격 증명 없이 실행할 수 있는 characterization 검사로 남겼다. 특히 스캔 뒤 참조가 생기는 경우, 삭제 직전 객체가 바뀌는 경우, R2 삭제가 실패하는 경우와 데이터베이스 행이 동시에 수정되는 경우를 각각 확인했다. AWS SDK가 Rhymix의 Composer 환경에서 presigned request를 만들 수 있는지도 네트워크 요청 없이 별도로 검사했다.
실제 운영에서는 기존 게시글의 첨부파일과 새 이미지 업로드, 권한별 다운로드를 확인했다. 다만 같은 조건에서 업로드·다운로드 지연 시간이나 처리량을 반복 측정하지는 않았다. 이 작업의 결과는 성능 향상보다 기존 사용 흐름과 권한을 유지하면서 저장 위치를 분리했다는 데 있다.
현재 한계
현재 Direct Upload는 Rhymix의 MIME 판별과 이미지 변환 등 일부 검증 경로를 그대로 거치지 않으므로 관리자 진단 화면에서만 사용한다. 일반 회원 업로드는 Rhymix에 파일을 먼저 저장한 뒤 R2로 옮기는 방식을 유지한다.
R2는 조건부 DeleteObject를 제공하지 않아 고아 객체 삭제를 완전히 원자적으로 만들 수 없다. 삭제 직전 데이터베이스 참조와 객체 정보를 다시 확인해 경쟁 조건을 줄였지만, 두 확인 사이에 상태가 바뀌는 가능성까지 없앴다고 말할 수는 없다. 더 강한 보장이 필요해지면 삭제 표시와 유예 기간을 둔 별도의 수거 과정을 검토해야 한다.