2. Overwrite Options and Transfer Volume

결과는 맞는데 같은 파일을 하루에 열 번 넘게 내려받던 파이프라인

  • Haram Lee
  • 2026-09-14
  • studies / Topics / Data Loading

결과가 맞으면 충분할까?

  • 파티션을 통째로 덮어쓰는 적재에서 무엇이 낭비될 수 있을까?
  • STS는 이미 옮긴 파일을 어떻게 알아보고 건너뛸까?
  • ETag가 같으면 정말 내용이 같을까?

2.1 The Bill That Did Not Match

1장과 같은 파이프라인이다. STS가 S3의 그날 날짜 폴더(dt=YYYY-MM-DD/)를 GCS 스테이징으로 복사하고, load job이 스테이징의 그날 파일 전체로 날짜 파티션을 WRITE_TRUNCATE 모드로 덮어쓴다.

어느 사용자가 이 파이프라인을 2시간 주기로 돌리고 있었는데, S3에서 나가는 전송 비용이 예상보다 훨씬 컸다. S3에 하루 동안 새로 쌓이는 양의 몇 배를 내보낸 만큼 전송 요금이 나왔다.

S3 egress는 첫 단계에서만 생긴다. STS가 S3에서 객체를 읽어 AWS 밖으로 내보내는 순간이다. 둘째 단계의 load job은 GCP 안에서 일어나므로 전송 비용과 관계없다. 그러니 볼 곳은 STS였다.

2.2 Measuring with STS Counters

추측 대신 STS 작업 이력을 읽었다. gcloud transfer operations list로 보면 실행마다 복사한 객체 수와 바이트, 그리고 건너뛴 객체 수(objectsFromSourceSkippedBySync)가 카운터로 남아 있다.

날짜 (UTC)실행 수하루 복사량 ÷ 하루치 원본
1일차267.7배
2일차247.9배
3일차247.9배

모든 실행이 성공했고, 건너뛴 객체는 전부 0이었다. 같은 날짜 폴더를 읽는 실행의 복사량은 시간이 갈수록 커졌다. 하루 동안 쌓인 파일을 2시간마다 처음부터 다시 내려받고 있었다.

flowchart LR
  r1["02:00 실행<br/>그날 폴더 전체 복사"] --> r2["04:00 실행<br/>그날 폴더 전체 다시 복사"] --> r3["06:00 실행<br/>또 전체 복사"] --> rn["..."]

원인은 STS 설정 한 줄이었다. overwriteObjectsAlreadyExistingInSink: true, 지금 API로 말하면 overwriteWhen: ALWAYS다. 싱크에 같은 이름의 객체가 이미 있어도 매번 원본에서 다시 복사한다. 이 값은 예전 파이프라인 코드에서 복사돼 온 것이었다. 예전 코드는 매번 새 폴더에 썼기 때문에 이 옵션이 아무 영향을 주지 않았다. 하루 한 번 도는 파이프라인에서도 문제가 드러나지 않았다. 짧은 주기를 고른 첫 사용자가 나타나고서야 보였다.

2.3 Correctness and Transfer Volume Are Separate

처음 들은 반론은 이랬다. “어차피 파티션을 덮어쓰니까 괜찮은 것 아닌가?”

결과만 보면 맞다. load job이 그날 파일 전체로 파티션을 통째로 바꾸니까 테이블은 언제나 맞다. 하지만 그 사실은 첫 단계에서 같은 파일을 또 내려받는 것을 막지 못한다. 결과의 정확성과 전송량은 서로 다른 축이다.

“시간별 증분으로 바꾸면?“도 답이 아니었다. 폴더 단위를 한 시간으로 줄여도, 덮어쓰기 옵션이 그대로면 그 한 시간 폴더를 실행마다 또 내려받는다. 반대로 옵션만 바꾸면 폴더 단위와 관계없이 재전송이 사라진다. 게다가 시간별 증분을 하려면 원본에 시간 폴더가 있어야 하고, BigQuery 테이블을 HOUR 파티션으로 바꿔야 하며, 테이블당 파티션 수 제한(4,000개)에도 걸린다.

2.4 How DIFFERENT Decides

STS의 overwriteWhen에는 값이 세 가지 있다. DIFFERENT, NEVER, ALWAYS다. 문서상 STS의 기본 동작은 원래 DIFFERENT다. 객체마다 다음 순서로 판정한다.

flowchart TD
  start["소스 객체 하나"] --> exists{"싱크에 같은 키가 있나?"}
  exists -->|"없음"| copy["복사"]
  exists -->|"있음"| mode{"overwriteWhen"}
  mode -->|"ALWAYS"| copy
  mode -->|"NEVER"| skip["건너뜀"]
  mode -->|"DIFFERENT"| cmp{"ETag나 체크섬이 같은가?"}
  cmp -->|"같음"| skip
  cmp -->|"다름"| copy

건너뛴 객체는 S3 GET 자체가 일어나지 않으므로 egress도 생기지 않는다.

남은 질문은 “ETag가 같으면 내용이 같은가"였다. 단일 PUT으로 올린 S3 객체의 ETag는 내용 전체의 MD5다. 메타데이터는 계산에 들어가지 않는다. 예외는 두 가지인데 둘 다 안전한 쪽이다.

경우ETag결과
멀티파트 업로드파트별 MD5를 다시 MD5한 값에 -N같은 내용이라도 파트 크기가 다르면 ETag가 달라져 다시 복사한다
SSE-KMS, SSE-C 암호화MD5가 아니고 올릴 때마다 바뀜매번 다시 복사한다. 절감은 없지만 틀리지도 않는다

즉 “ETag가 같으면 내용이 같다"는 성립하고, 그 반대는 성립하지 않는다. 건너뛰기는 같을 때만 일어나니 잘못된 데이터를 건너뛰는 방향으로는 실수하지 않는다.

문서를 읽다가 마음에 걸린 빈틈도 하나 있었다. STS 문서에는 멀티파트 객체에 체크섬 메타데이터가 없으면 크기로 비교할 수 있다는 취지의 언급이 있다. 크기로만 비교하면 이름과 크기가 같고 내용이 다른 파일을 건너뛸 수 있다. 실제 스테이징 객체를 확인해 보니 모두 단일 PUT 객체로 정상 MD5를 갖고 있어서 이 경우에 해당하지 않았다.

2.5 Verifying with the Counter

이 옵션을 스트림 설정에서 고를 수 있게 했다. 기본값은 기존 동작을 바꾸지 않도록 ALWAYS로 두고, 사용자가 DIFFERENT를 고를 수 있게 했다. 예전 형태의 불리언 옵션은 따로 노출하지 않았다. 둘을 같이 노출하면 서로 모순되는 설정이 가능해지기 때문이다. STS도 overwriteWhen이 있으면 옛 불리언을 무시한다.

효과는 카운터로 확인했다. 먼저 작은 테스트 스트림에서 2회차부터 objectsFromSourceSkippedBySync가 1회차에 복사한 객체 수와 정확히 같아지는 것을 봤다. 이어서 문제의 파이프라인에 적용했다. 적용 직후 실행은 그날 폴더의 파일을 모두 복사했다. 40분 뒤 다음 실행은 그 파일을 전부 건너뛰고 새로 생긴 파일만 복사했다. 예전 방식으로 옮겼을 양의 10분의 1 남짓만 나갔다.

멱등성은 이 옵션에서 나오는 것이 아니다. 재실행해도 결과가 같은 건 load job이 파티션을 통째로 덮어쓰기 때문이다. 옵션은 스테이징을 채우는 방법만 바꾼다. 결과는 그대로이고 옮기는 바이트만 줄어든다.

2.6 What It Does Not Change

  • 스테이징이 지워진 날짜를 다시 돌리면 전부 다시 복사한다. 건너뛰려면 싱크에 비교할 객체가 있어야 하는데, 스테이징 버킷의 수명 규칙이 지우고 나면 비교 대상이 없다. 수명 규칙이 짧은 테스트 버킷에서 사본이 지워진 뒤 첫 실행이 전부 복사하는 것으로 확인했다.
  • 줄어드는 것은 egress와 GET뿐이다. S3 LIST, 스테이징 저장 용량, load job은 그대로다. load는 여전히 그날 파티션 전체를 다시 쓴다.
  • 삭제는 반영되지 않는다. 증분 전송에서는 싱크에만 있는 객체를 지우지 않으므로 S3에서 지운 파일이 스테이징에 남는다. 옵션을 바꾸기 전과 같다.

그럼 ALWAYS는 왜 있을까. 내용은 같지만 객체를 다시 써야 할 때다. 메타데이터만 바꿨을 때(ETag가 그대로라 DIFFERENT는 건너뛴다), 스토리지 클래스나 암호화 설정을 기존 객체에도 적용하고 싶을 때, 싱크의 쓰기 이벤트로 후속 처리를 하는 구조라 다시 써야 이벤트가 발생할 때다. 이 파이프라인의 스테이징은 어느 것에도 해당하지 않았다.

1장과 이번 일은 닮은 데가 있다. 둘 다 아무도 이유를 모르던 기본값에서 시작했다. autodetect=True와 overwrite=true는 “원래 그랬다” 말고는 근거가 없었다. 기본값에는 이유가 적혀 있어야 하고, 문서가 말하지 않는 동작은 카운터와 대조군으로 확인한다.

Discussion