DB

Tasteam V2 이관기 2: DB는 무중단이었는데 왜 S3 이관은 다르게 했을까? — aws s3 sync 기반 이관기

clay.kim 2026. 2. 28. 14:23

Tasteam V2 이관기 2: DB는 무중단이었는데 왜 S3 이관은 다르게 했을까? — aws s3 sync 기반 이관기

안녕하세요, Tasteam에서 Cloud 팀을 맡고 있는 clay입니다.

"DB는 논리 복제로 무중단 마이그레이션 했잖아요. 그럼 파일도 비슷하게 옮기면 되는 거 아닌가요?"

 

V1에서 V2로 넘어오면서 DB는 Logical Replication 기반으로 무중단 마이그레이션을 준비했습니다. 그래서 처음에는 이미지와 파일이 들어 있는 S3도 비슷하게 생각했어요. 기존 버킷의 데이터를 백그라운드로 복사하고, 신규 업로드는 async로 두 군데에 쓰면 되지 않을까 싶었습니다.

그런데 막상 구조를 뜯어보니 S3는 DB와 결이 달랐습니다. 특히 저희 서비스는 S3 Presigned POST 기반 업로드를 사용하고 있었고, DB에는 버킷이나 전체 URL이 아니라 이미지 이름만 저장하고 있었어요. 서버 코드에 비동기 처리를 붙인다고 끝나는 문제가 아니었습니다.

이 글은 이전 편인 Tasteam V2 이관기 1: 다른 AWS 계정으로 DB를 무중단으로 옮긴 이야기에서 이어집니다.

오늘은 왜 S3 이관을 DB와 같은 방식으로 밀어붙이기 어려웠는지, 그리고 왜 저희 구조에서는 기존 object key를 보존하는 단순한 복사 절차가 가장 현실적이었는지 공유해 보겠습니다.


환경

당시 저희의 파일 업로드 구조는 다음과 같았습니다.

항목 내용

기존 업로드 버킷 tasteam-uploads-dev-v1
신규 업로드 버킷 tasteam-uploads-dev, tasteam-uploads-prod
업로드 방식 Presigned POST 기반 직접 업로드
런타임 설정 STORAGE_BUCKET, STORAGE_BASE_URL, STORAGE_REGION
DB 저장값 버킷명이나 전체 URL이 아닌 이미지 이름
객체 경로 구성 런타임 버킷 + 업무 도메인 prefix(user, food, comment 등) + 이미지 이름
최종 목표 V2 서비스가 최종적으로 V2 버킷만 바라보도록 정리

 

DB는 변경 로그를 따라가며 복제하고 컷오버하는 전략이 가능했습니다. 하지만 S3는 객체 스토리지입니다. "변경 스트림을 따라간다"기보다, 결국 객체를 복사하고, 어느 버킷을 기준으로 서비스할지 전환하는 문제에 더 가까웠어요.


처음엔 DB처럼 생각했습니다

DB 무중단 마이그레이션을 경험하고 나면, 자연스럽게 이런 생각을 하게 됩니다.

  • 기존 데이터는 백그라운드로 복제한다
  • 신규 데이터는 일정 기간 동안 양쪽에 같이 쓴다
  • 충분히 따라잡히면 읽기/쓰기 대상을 바꾼다

문제는 이 패턴이 DB에서는 잘 맞지만, Presigned POST 기반 S3 업로드에서는 그대로 적용되지 않는다는 점이었습니다.

DB와 S3가 달랐던 지점

관점 DB S3

데이터 단위 row / transaction object 전체
동기화 방식 logical replication, CDC 가능 기본적으로 copy/sync 관점
쓰기 주체 애플리케이션 서버 클라이언트가 Presigned POST로 직접 업로드
정합성 기준 transaction 단위로 비교적 명확 객체 복사 성공, object key 구조 유지, 런타임 설정 전환이 따로 움직임
무중단 전략 복제 lag 확인 후 컷오버 대량 복사 + 최종 전환 + 검증이 중심

특히 저희 구조에서는 파일 업로드가 대략 이렇게 진행됩니다.

FE -> user / food / comment 등 도메인별 업로드 API 호출
BE -> STORAGE_BUCKET + 업무 도메인 prefix + 이미지 이름으로 Presigned POST 발급
FE -> S3에 직접 POST 업로드
BE -> 도메인 데이터에는 이미지 이름만 저장
조회 시 -> STORAGE_BASE_URL + 업무 도메인 prefix + 이미지 이름으로 URL 구성

즉, 서버가 파일 바이트를 직접 받아서 저장하는 구조가 아니기 때문에, 단순히 백엔드에서 @Async를 붙인다고 "두 버킷에 동시에 업로드"가 되지 않습니다.

그리고 더 중요한 제약이 하나 있었습니다. DB에는 어느 버킷에 있는지까지 저장하지 않았습니다. 런타임 버킷 설정을 바꾸는 순간, 기존 이미지 이름들은 모두 새 버킷의 같은 경로를 바라보게 됩니다.

기존: tasteam-uploads-dev-v1/user/a.png
전환: tasteam-uploads-dev/user/a.png

기존: tasteam-uploads-dev-v1/food/b.png
전환: tasteam-uploads-dev/food/b.png

그래서 이번 이관의 핵심은 "이미지 레코드별 저장 위치를 바꾸는 것"이 아니라, 새 버킷에도 기존과 동일한 object key 구조를 재현하는 것이었습니다.


선택지를 비교했습니다

그래서 실제로는 네 가지 방법을 놓고 비교했습니다.

후보 1. 애플리케이션 레벨 이중쓰기(dual-write) + async

신규 업로드가 발생하면 두 버킷에 동시에 쓰거나, 한 버킷에 먼저 쓴 뒤 다른 버킷으로 비동기 복사를 거는 방식입니다.

가장 먼저 떠오르는 방식이지만, 저희 구조에서는 생각보다 복잡했습니다.

  • 클라이언트가 버킷별 Presigned POST를 두 개 받아서 두 번 업로드해야 할 수도 있음
  • 또는 한 번 업로드한 뒤 서버가 백그라운드 복사 작업을 따로 책임져야 함
  • DB에는 이미지 이름만 있으므로, 버킷별 저장 위치를 개별 객체 단위로 구분하기 어려움
  • 한 버킷에는 성공하고 다른 버킷에는 실패하는 경우를 처리해야 함
  • 재시도, 멱등성, 모니터링, 보상 로직까지 필요해짐

기술적으로 불가능한 방식은 아니지만, 단발성 이관을 위해 상시 복잡도를 애플리케이션 안에 들이는 선택에 가까웠습니다.

후보 2. 기존 버킷을 그대로 두고 정책만 교차 허용

이 방법은 가장 빠릅니다. 복사 작업도 필요 없고, 당장 서비스는 붙일 수 있어요.

하지만 장기적으로 보면 문제가 남습니다.

  • V1/V2의 경계가 흐려짐
  • 버킷 소유권과 운영 책임이 섞임
  • "언제까지 이걸 유지할 것인가?"라는 질문이 계속 남음

즉, 빠른 임시 대응으로는 괜찮지만 이관의 완료 상태로 보기는 어려웠습니다.

후보 3. S3 Batch Operations 또는 DataSync

관리형 서비스답게 기능은 강력합니다.

  • 대규모 객체 이관에 적합
  • 리포트, 재시도, 스케줄링이 편함

물론 이 방식들도 설정에 따라 object key를 유지한 복사가 가능합니다. key 보존 자체가 aws s3 sync만의 특별한 장점은 아니었습니다.

다만 이번 건은 단발성 이관이었고, 객체 키 구조를 유지하면서 빠르게 검증하는 것이 더 중요했습니다. 저희가 필요한 건 대규모 작업 관리 시스템보다는, user/{imageName}, food/{imageName}, comment/{imageName} 같은 key가 새 버킷에도 그대로 있는지 확인하는 일이었어요. 현재 규모에서는 운영 복잡도가 오히려 더 크게 느껴졌습니다.

후보 4. aws s3 sync + 임시 Bucket Policy 허용 + 정책 원복

결국 가장 현실적인 선택지는 이쪽이었습니다.

  • 대량 객체 이관이 단순함
  • --dryrun으로 사전 검증이 쉬움
  • 필요한 object key 보존 조건을 만족함
  • 전환 직전과 직후에 같은 명령을 반복해 차이분을 줄이기 쉬움
  • 애플리케이션 코드 변경을 최소화할 수 있음
  • 이관 후 임시 권한을 바로 닫을 수 있음

그래서 aws s3 sync를 선택했습니다

저희는 aws s3 sync + 임시 Bucket Policy 허용 + 이관 후 정책 원복 방식을 선택했습니다.

이 선택의 핵심은 aws s3 sync만 object key를 보존할 수 있다는 뜻이 아니었습니다. 저희 서비스는 DB에 이미지 이름만 저장하고, 실제 S3 경로는 런타임 설정과 업무 도메인 prefix로 조합합니다. 그래서 마이그레이션의 성공 조건은 명확했습니다.

tasteam-uploads-dev-v1/user/a.png     -> tasteam-uploads-dev/user/a.png
tasteam-uploads-dev-v1/food/b.png     -> tasteam-uploads-dev/food/b.png
tasteam-uploads-dev-v1/comment/c.png  -> tasteam-uploads-dev/comment/c.png

즉, 기존 object key를 그대로 보존하는 복사가 반드시 필요했습니다. 그 조건을 만족하는 여러 방법 중에서, 이번 규모와 일정에서는 aws s3 sync가 가장 단순하게 검증하고 반복 실행할 수 있는 방법이었습니다.

핵심은 이렇습니다.

  1. 신규 버킷과 IAM 권한을 먼저 준비한다
  2. 이관 시점에만 필요한 교차 접근 권한을 잠깐 연다
  3. sync --dryrun으로 대상과 변경량을 먼저 검증한다
  4. 1차 대량 복사를 수행한다
  5. 전환 직전에 한 번 더 sync해서 차이분을 줄인다
  6. 런타임 설정을 신규 버킷 기준으로 전환한다
  7. 전환 직후 마지막 sync를 한 번 더 수행한다
  8. 검증이 끝나면 임시 정책을 제거한다

즉, DB처럼 "계속 복제하면서 lag를 본다"기보다, 기존 key 구조를 새 버킷에 그대로 복사하고, 전환 전후의 짧은 차이분을 반복 sync로 줄이는 방식이었습니다.


실제 이관 절차

1단계 — 신규 버킷과 권한 준비

먼저 V2에서 사용할 업로드 버킷을 준비했습니다.

  • tasteam-uploads-dev
  • tasteam-uploads-prod

그리고 V2 백엔드 런타임 역할에 아래 권한을 부여했습니다.

  • s3:ListBucket
  • s3:GetObject
  • s3:PutObject
  • s3:DeleteObject

또한 런타임 설정은 아래 키 기준으로 정렬했습니다.

  • STORAGE_BUCKET
  • STORAGE_BASE_URL
  • STORAGE_REGION

이 단계가 먼저 정리되어 있어야, 실제 이관 이후 애플리케이션이 어느 버킷을 바라봐야 하는지 흔들리지 않습니다. 저희처럼 DB에 이미지 이름만 저장하는 구조에서는 특히 중요합니다. 버킷 설정 하나가 바뀌면 기존 이미지 이름들이 모두 새 버킷의 동일한 업무 도메인 prefix 아래에서 조회되기 때문입니다.

2단계 — 이관용 임시 권한 열기

소스 버킷과 타깃 버킷이 다른 계정/역할 경계를 갖고 있었기 때문에, 이관 시점에만 v1 Principal이 v2 버킷에 접근할 수 있도록 임시 Bucket Policy를 열었습니다.

중요한 점은 상시 허용이 아니라 이관 시점에만 잠깐 연다는 것입니다.

  • 버킷: s3:ListBucket
  • 오브젝트: s3:PutObject, s3:GetObject, s3:DeleteObject

이렇게 해야 작업이 끝난 뒤 다시 깔끔하게 닫을 수 있습니다.

3단계 — dryrun으로 먼저 검증

실제 복사 전에 항상 --dryrun으로 어떤 객체가 이동 대상인지 확인했습니다.

AWS_PROFILE=tasteam-v1 aws s3 sync s3://<source-bucket> s3://<target-bucket> \
  --source-region ap-northeast-2 \
  --region ap-northeast-2 \
  --dryrun

이 단계의 장점은 단순합니다.

  • 실수로 잘못된 버킷을 지정하지 않았는지 확인 가능
  • 예상보다 변경량이 많거나 적은 경우 바로 눈에 띔
  • 실제 명령과 거의 동일한 형태로 검증 가능

4단계 — 1차 대량 복사 수행

Dry-run 결과를 확인한 뒤, 같은 명령에서 --dryrun만 제거해 실제 복사를 진행했습니다.

AWS_PROFILE=tasteam-v1 aws s3 sync s3://<source-bucket> s3://<target-bucket> \
  --source-region ap-northeast-2 \
  --region ap-northeast-2

이 방식은 저희가 필요로 했던 key 보존 조건을 만족했습니다. 예를 들어 기존에 user/{imageName}, food/{imageName}, comment/{imageName}처럼 접근하던 객체가 있다면, 새 버킷에서도 같은 key로 존재하게 됩니다. 덕분에 애플리케이션이 별도의 key mapping 로직을 새로 알 필요가 없었어요.

다만 1차 대량 복사만 하고 끝내면 위험합니다. 복사하는 동안에도 기존 서비스는 계속 구 버킷으로 Presigned POST를 발급할 수 있기 때문입니다.

5단계 — 전환 직전에 차이분을 한 번 더 줄입니다

런타임 설정을 바꾸기 직전에 같은 sync를 한 번 더 실행했습니다.

AWS_PROFILE=tasteam-v1 aws s3 sync s3://<source-bucket> s3://<target-bucket> \
  --source-region ap-northeast-2 \
  --region ap-northeast-2

이 단계는 DB의 lag_bytes = 0 확인과 완전히 같은 의미는 아닙니다. 그래도 전환 직전까지 구 버킷에 새로 쌓인 객체를 최대한 신 버킷으로 따라붙이는 역할을 합니다.

여기서 중요한 점은 --delete를 쓰지 않는 것입니다. 전환 이후 신 버킷에만 생긴 신규 업로드가 있을 수 있기 때문에, 소스에 없다는 이유로 타깃 객체를 지우는 옵션은 이관 컷오버 절차와 맞지 않습니다.

6단계 — 런타임 설정 전환

전환 직전 차이분 sync까지 끝난 뒤에는 SSM/운영 설정에서 신규 버킷 기준으로 값을 정렬했습니다.

  • STORAGE_BUCKET
  • STORAGE_BASE_URL
  • STORAGE_REGION

그리고 재배포를 통해 컨테이너가 최신 설정을 다시 읽도록 반영했습니다.

이 순간부터 새로 발급되는 Presigned POST는 신규 버킷을 기준으로 만들어집니다. 반대로 전환 직전에 이미 발급된 Presigned POST나 구 버전 애플리케이션을 통해 진행 중이던 요청은 구 버킷으로 업로드될 수 있습니다.

그래서 전환 직후에도 한 번 더 구 버킷에서 신 버킷으로 sync를 실행했습니다. 이 마지막 sync는 전환 경계에 걸친 업로드를 회수하기 위한 안전장치였습니다.

7단계 — 기능 검증

마지막으로 실제 서비스 흐름을 기준으로 검증했습니다.

  • Presigned POST 발급 API가 정상 응답하는지
  • S3 업로드가 정상 성공하는지
  • 업로드 후 조회 URL이 신규 버킷과 기존 업무 도메인 prefix 기준으로 조합되는지
  • 기존 이미지 이름이 새 버킷의 같은 key에서 정상 조회되는지
  • 이미지가 포함된 화면에서 실제 렌더링이 깨지지 않는지

이 단계는 단순히 aws s3 ls만 보는 것보다 훨씬 중요했습니다. 결국 사용자가 체감하는 건 버킷 이름이 아니라 업로드와 조회가 끊기지 않는가이기 때문입니다.

8단계 — 임시 정책 원복

검증이 끝난 뒤에는 이관을 위해 열었던 임시 Bucket Policy를 제거했습니다.

이렇게 하면 최종적으로는 V2 런타임 역할만 신규 버킷에 접근 가능한 상태로 정리됩니다.


왜 async를 최종 해법으로 쓰지 않았을까?

사실 이 글에서 가장 많이 받는 질문이 이 부분일 것 같습니다.

"비동기로 한쪽에 먼저 쓰고, 나머지는 백그라운드로 복사하면 되지 않나요?"

가능은 합니다. 하지만 이번 구조에서는 깔끔한 해법이 아니었습니다.

1. Presigned POST 구조에서는 비동기 이중쓰기가 생각보다 멀다

백엔드가 파일 바이트를 직접 받는 구조라면, 업로드 시점에 두 저장소로 동시에 밀어 넣는 전략을 비교적 쉽게 생각할 수 있습니다.

하지만 Presigned POST 기반 업로드는 다릅니다.

  • 클라이언트가 어느 버킷으로 업로드할지 먼저 결정돼야 하고
  • 업로드는 서버가 아니라 클라이언트가 직접 수행하며
  • 서버와 DB는 전체 S3 위치가 아니라 이미지 이름만 알고 있습니다
  • 조회 시점에는 런타임 버킷, 업무 도메인 prefix, 이미지 이름을 조합해서 최종 경로를 만듭니다

즉, "백엔드에서 async 처리"는 파일 저장 경로의 본질적인 제어 지점이 아니었어요. 오히려 저희에게 필요한 건 업로드 흐름을 새로 설계하는 것이 아니라, 기존 이름과 prefix 조합이 새 버킷에서도 그대로 해석되도록 만드는 것이었습니다.

2. 정합성 문제가 더 복잡해진다

비동기 이중쓰기에서 가장 까다로운 건 "성공/실패가 갈라지는 순간"입니다.

  • A 버킷 업로드 성공, B 버킷 복사 실패
  • 객체는 복사됐지만 특정 업무 도메인 prefix 아래에서 누락됨
  • 런타임 버킷을 바꿨는데 새 버킷에 같은 key가 아직 없음
  • 재시도 중 중복 복사 또는 orphan object 발생

DB에서는 transaction이나 replication lag 같은 기준점이 있지만, S3 이관에서는 이 기준점이 더 느슨합니다. 그래서 실패 처리 설계를 잘못하면 이관보다 운영 복잡도가 더 큰 시스템이 생길 수 있습니다.

3. 단발성 이관에 비해 투자 비용이 컸다

이번 작업의 본질은 "앞으로 영원히 두 버킷에 쓰는 시스템"을 만드는 것이 아니라, 기존 객체를 안전하게 옮기고 운영 기준을 V2 버킷으로 정리하는 것이었습니다.

그런데 async dual-write는 한 번의 이관을 위해

  • 애플리케이션 코드 변경
  • 비동기 작업 모니터링
  • 실패 재처리 설계
  • 보상/정리 로직

까지 모두 가져와야 합니다.

이건 분명 가능한 방법이지만, 저희 상황에서는 문제를 해결하는 비용보다 복잡도를 새로 만드는 비용이 더 컸다고 판단했습니다.


S3 이관 체크리스트

마지막으로, Presigned POST 기반 업로드 서비스를 S3로 이관할 때 확인하면 좋았던 항목들을 정리해 봤습니다.

  • 신규 버킷 생성 및 Public Access Block, CORS, SSE 설정
  • 런타임 IAM 역할에 ListBucket, GetObject, PutObject, DeleteObject 권한 부여
  • STORAGE_BUCKET, STORAGE_BASE_URL, STORAGE_REGION 설정 정리
  • 이관 시점에만 필요한 임시 Bucket Policy 허용
  • aws s3 sync --dryrun으로 대상 검증
  • 1차 aws s3 sync 수행
  • 런타임 전환 직전 차이분 sync 수행
  • 런타임 전환 직후 마지막 sync 수행
  • 이관 sync에는 --delete를 사용하지 않는지 확인
  • user, food, comment 등 업무 도메인 prefix별 객체 key가 유지됐는지 확인
  • Presigned POST 발급 API 정상 동작 확인
  • 업로드 후 조회 URL이 신규 버킷과 기존 업무 도메인 prefix 기준인지 확인
  • 기존 이미지 이름이 신규 버킷의 같은 key에서 조회되는지 확인
  • 이미지가 포함된 실제 화면에서 렌더링 검증
  • 이관 완료 후 임시 Bucket Policy 제거

돌아보며

1. "무중단"이라는 단어도 저장소마다 의미가 다릅니다

DB에서의 무중단은 CDC와 컷오버, lag 수렴 같은 개념으로 설명할 수 있습니다. 반면 S3에서는 대개 복사 전략, 전환 시점, 검증 방식이 더 중요합니다.

같은 "마이그레이션"이라도 저장소의 성격이 다르면 접근 방식도 달라져야 한다는 걸 다시 느꼈습니다.

2. 업로드 아키텍처가 마이그레이션 전략을 결정합니다

Presigned POST 기반 업로드는 평소에는 효율적입니다. 서버가 파일 바이트를 직접 받지 않아도 되고, 업로드 경로가 단순해지니까요.

하지만 마이그레이션 시점에는 이 구조가 중요한 제약이 됩니다. 누가 실제로 파일을 쓰는가가 바뀌면, 이중쓰기 전략도 완전히 달라지기 때문입니다.

3. 단발성 이관일수록 운영 단순성이 중요합니다

한 번의 이관을 위해 시스템에 영구적인 복잡도를 심는 건 조심해야 합니다.

이번에는 aws s3 sync 방식이 가장 화려한 해법은 아니었지만,

  • 검증이 쉽고
  • 롤백 판단이 명확하고
  • 코드 변경이 작고
  • 필요한 object key 보존 조건을 만족하고
  • 이관 후 구조가 깔끔하게 남는

현실적인 선택이었습니다.

처음에는 "DB도 무중단으로 옮겼으니 S3도 비슷하게 하면 되겠지"라고 생각했습니다. 하지만 실제로는 저장소의 특성이 다르면, 운영 전략도 달라져야 했습니다.

이번 경험에서 얻은 가장 큰 교훈은 이것이었습니다.

좋은 마이그레이션은 가장 멋진 설계가 아니라, 현재 구조에서 가장 적은 복잡도로 가장 명확하게 검증할 수 있는 방식이라는 점입니다. 저희 S3 이관에서는 그 기준이 "기존 이미지 이름과 업무 도메인 prefix가 새 버킷에서도 같은 object key를 가리키는가"였습니다.