얼쑤에서 연습 녹음을 받으면 백엔드는 원본을 객체 저장소에 보관하고, 분석 서버는 필요한 파일을 가져와 처리합니다. 처음에는 서울 리전과 신규 계정 크레딧을 고려해 Google Cloud Storage(GCS)를 쓰는 설계를 잡았습니다. 다음 날에는 크레딧이 끝난 뒤의 비용과 다운로드 조건, 저장소를 바꿀 때의 구현 부담을 다시 검토해 S3 호환 API를 쓰는 Cloudflare R2로 계획을 바꿨습니다.

한 파일에 15분 읽기 권한

분석 서버가 해야 할 일은 전달받은 녹음 한 건을 읽는 일이었습니다. 그래서 백엔드가 특정 객체에 대해 GET 요청만 15분(900초) 동안 허용하는 URL을 서명하고, 분석 서버는 그 주소로 원본을 내려받도록 했습니다. 저장소의 자격 증명은 서명하는 백엔드에만 둡니다. 유효한 URL을 가진 사람도 파일을 읽을 수 있으므로 주소 자체를 노출하지 않는 것도 이 설계의 일부입니다. Google Cloud Storage 서명 URL · Cloudflare R2 서명 URL

GCS와 R2에서 주소를 만드는 방법은 다르지만 분석 서버에 넘기는 것은 어느 쪽이든 만료 시간이 있는 읽기용 URL입니다. 저장소 선택을 바꾸면서 백엔드의 서명 구현을 바꿀 수 있었고, 분석 서버가 녹음을 받는 인터페이스는 유지했습니다. GCS는 초기 설계안이었고, 구현과 아래 검증은 R2에서 진행했습니다.

새 주소로 만료와 삭제 구분

처음에는 운영 설정으로 배포 JAR의 저장소 구현을 실행하자 PUT과 DELETE가 모두 HTTP 403 AccessDenied로 끝났습니다. 서버 설정에 실제 버킷명 대신 토큰 이름이 들어 있었습니다. 검증 프로세스에서 버킷명만 바꿔 PUT과 서명 GET이 성공하는 것을 확인했습니다.

그때 만든 첫 합성 객체에 900초짜리 서명을 발급했습니다. 만료 뒤의 실패가 파일 삭제 때문일 수도 있어, 같은 객체에 새 서명을 발급해 대조했습니다.

확인한 조건실제 응답
발급 직후 900초짜리 서명으로 읽기HTTP 200, 원본 바이트와 응답 본문 일치
발급한 지 900초가 지난 주소로 읽기HTTP 403 ExpiredRequest
같은 객체에 새 서명을 발급해 읽기HTTP 200, 원본 바이트와 응답 본문 일치
객체를 삭제한 뒤 방금 성공한 주소로 읽고 별도로 HEAD 요청두 요청 모두 HTTP 404

만료 뒤 새 주소가 같은 바이트를 돌려줘 이전 주소의 실패를 객체 삭제와 구분할 수 있었습니다. 이어서 객체를 지운 뒤에는 방금 성공했던 주소도 404가 됐습니다.

운영 설정의 버킷명을 고쳐 백엔드를 다시 띄운 뒤에는 별도의 46바이트 합성 객체로 재검증했습니다. 서명 GET은 HTTP 200을 반환했고 본문이 원본과 일치했습니다. 같은 서명을 PUT에 사용하거나 주소에서 객체 키를 바꾼 경우에는 각각 HTTP 403 SignatureDoesNotMatch를 받았습니다. 900초가 지난 뒤의 응답은 앞서 만든 객체에서 확인했고, 수정 후 재검증에서는 서명이 지정한 파일과 읽기 요청에만 유효한 것을 확인했습니다.