본문으로 건너뛰기
JSL107's Tech Note
돌아가기

객체 스토리지의 수명주기 기준을 업로드부터 파기까지 연결하기

6분 분량이 글 고치기
목차 · 5

객체 스토리지의 수명주기는 버킷에 규칙 하나를 추가한다고 완성되지 않아요. 객체를 업로드할 때 정한 기준일이 변환·축소 과정부터 파기 후 데이터베이스 정리까지 이어져야 하나의 정책으로 작동해요.

이번 작업에서는 객체 스토리지 객체의 customTime을 중심으로 전체 흐름을 살펴봤어요. 그 과정에서 학년도 경계 계산 오류와 서버 타임존 의존성, 파생본의 기준일 누락, 파기 후 고아 기록이 생길 가능성을 차례로 발견했어요.

같은 파일의 기준일이 실행 환경에 따라 달라졌다

스토리지 객체에는 보관 등급 전환의 기준이 되는 customTime을 기록하고 있었어요. 버킷의 Lifecycle 규칙은 이 값을 기준으로 객체를 더 저렴한 보관 등급으로 전환해요.

문제는 기준일을 계산할 때 한국의 학년도 규칙과 서버의 로컬 시간이 뒤섞인다는 점이었어요. 학년도는 3월 1일에 시작하지만 기존 TypeScript 구현은 getMonth()와 setHours()처럼 로컬 시간대에 의존하는 API를 사용했어요. 이 때문에 같은 시각을 입력해도 프로세스의 TZ에 따라 서로 다른 학년으로 판단할 수 있었어요.

예를 들어 한국 시각으로 2027년 3월 1일 00시 30분이면 UTC에서는 아직 2월 28일이에요. 서버가 Asia/Seoul일 때는 새 학년으로 처리하지만 UTC 환경에서는 이전 학년으로 판단해 기준일이 한 해 달라질 수 있었어요.

운영 환경에서는 문제가 생기지 않았어요. 확인한 Kubernetes 매니페스트 다섯 곳에 모두 TZ: Asia/Seoul이 설정되어 있었거든요. 하지만 애플리케이션의 데이터 규칙 자체가 배포 환경 변수에 의존한다는 점은 위험했어요. 특히 customTime은 한 번 기록하면 앞당기거나 제거할 수 없어서 잘못된 값을 복구하려면 객체를 다시 써야 했어요.

레거시 PHP API에는 이와 별개로 학년도 경계를 잘못 계산하는 문제도 있었어요. 연도를 무조건 1년 더하는 바람에 1~2월에 올라온 파일의 기준일이 실제 학년도보다 한 해 뒤로 밀렸어요. 이 경로는 사용자가 직접 올리는 첨부에도 쓰이기 때문에 실제 보존 등급 전환 시점에 영향을 줄 수 있었어요.

원본의 기준이 파생본에서 끊겼다

기준일 계산만 바로잡아서는 충분하지 않았어요. 객체를 실제로 생성하는 모든 경로에서 customTime을 전달하는지도 확인해야 했어요.

점검해 보니 사용자가 직접 올린 원본과 달리 외부 웹사이트에서 수집한 크롤링 첨부와 문서·이미지·영상·음성 변환 산출물 일부에는 기준일을 전달하지 않았어요. customTime이 없는 객체는 Lifecycle 조건을 충족하지 못해 계속 기본 보관 등급에 남을 수 있었어요.

파생본을 만들 때마다 현재 시각을 기준으로 기준일을 다시 계산하는 방식도 적절하지 않았어요. 같은 원본에서 나온 파일인데도 생성 시점에 따라 보존 일정이 달라지기 때문이에요. 원본과 파생본을 하나의 데이터 묶음으로 관리하려면 기준일도 그 계보를 따라야 했어요.

저장소 경계를 확인하면서 또 다른 사실도 알게 됐어요. 처음에는 레거시 PHP API 저장소의 크롤링 경로만 고치려고 했지만 해당 저장소에서는 크롤링 파일의 출처를 식별하는 컬럼을 사용하지 않았어요. 그래서 실제 실행 주체일 가능성이 높은 신규 Node 기반 크롤링 실행 저장소를 따로 확인했고, 운영 이미지를 만드는 개발 브랜치의 크롤링 업로드 두 곳에도 customTime을 추가했어요.

시간대와 메타데이터 계보를 코드에 명시했다

먼저 한국 학년도를 판정하는 로직을 서버의 로컬 시간대와 분리했어요. 실행 환경의 TZ와 관계없이 한국 시각을 기준으로 3월 경계를 판단하도록 계산 방식을 바꿨어요. PHP 구현에서는 12월과 312월을 구분해 학년도 종료 시점을 선택하도록 고쳤어요.

업로드 경로는 원본과 파생본으로 나눠 점검했어요. 새 원본을 올리는 경로에서는 수정한 학년도 계산을 바탕으로 기본 customTime을 만들도록 했어요. 변환·축소 파생본은 생성 시점마다 기준일을 다시 계산하지 않고 원본 객체에 실제로 기록된 customTime을 전달하도록 바꿨어요. 크롤링 첨부도 실제 실행 저장소의 업로드 지점까지 추적해 같은 메타데이터를 기록했어요.

이제 하나의 원본에서 문서 미리보기와 이미지 축소본, 영상·음성 변환본을 만들어도 모두 같은 Lifecycle 기준을 적용해요. 객체를 만든 시점이 아니라 데이터가 속한 학년과 원본의 보존 정책을 기준으로 삼아요.

파기에서도 파일과 기록의 수명을 맞췄다

수명주기의 마지막 단계도 함께 살펴봤어요. 기존 파기 작업은 파생본 파일을 제거하고도 데이터베이스 기록을 남길 수 있었어요. 스토리지에는 객체가 없는데 DB에는 파생본이 존재하는 것처럼 보이는 새 고아 기록이 생길 수 있는 구조였어요.

기존 첨부 연결 정리 방식에 맞춰 파생본 파일 처리와 관련 DB 기록 정리를 같은 트랜잭션 범위에서 수행하도록 바꿨어요. 핵심은 삭제 API를 한 번 더 호출하는 데 있지 않았어요. 파기 작업의 완료 단위를 파일에서 메타데이터를 포함한 전체로 넓히는 일이었어요.

다만 데이터베이스 트랜잭션이 외부 객체 스토리지까지 원자적으로 묶어 주지는 않아요. 이번 변경은 애플리케이션 내부의 정리 순서와 DB 상태 전이를 일관되게 구성해 정상 처리 과정에서 새로운 고아 기록이 남지 않도록 했다는 데 의미가 있어요.

Lifecycle 전환과 파기는 별개의 흐름이다

이번 변경은 살아 있는 객체를 삭제하도록 Lifecycle 규칙을 바꾼 게 아니에요. 여기서 다룬 버킷 동작은 customTime을 기준으로 보관 등급을 전환하는 것이고, 파기는 별도의 애플리케이션 작업이에요. 두 흐름을 구분해야 운영 위험을 정확히 설명할 수 있어요.

검증은 실행 환경에 따른 차이를 중심으로 진행했어요. 신규 Node API의 기본 브랜치에서는 타입 검사를 통과했고 Asia/Seoul, UTC 두 환경에서 10개 테스트도 통과했어요. 이전 구현이 UTC 조건의 5개 경계 사례에서 실패하는 현상도 재현했어요. 레거시 PHP API의 변경 파일은 PHP 문법 검사를 통과했고, 크롤링 실행 저장소의 기본 브랜치에서는 새 테스트 5개가 통과했어요.

개발 브랜치에서는 타입 검사를 통과했지만 Jest 설정이 해당 디렉터리를 제외하고 있어 새 테스트가 실행되지 않는 한계는 남았어요.


이 글 고치기
이 글 공유하기:

이전 글
파일 파기에서 저장소와 DB 정합성을 지킨 순서