[실전 미디어 13] 업로드 파이프라인 설계

지난 편에서 아이폰 영상 문제를 다루며 업로드가 함수 본문을 통과하는 구조의 한계를 언급했다. 이번 편은 그 업로드 파이프라인 자체를 설계 관점에서 정리한다. 파일을 언제 스토리지에 저장할지, 함수를 통과시킬지 아니면 브라우저가 스토리지에 직접 올리게 할지, 취소된 글의 파일은 어떻게 처리할지 같은 결정들이다. 눈에 잘 안 보이지만 이 결정들이 사용자 경험과 비용, 정합성을 조용히 좌우했다.


1. 언제 저장할 것인가

가장 먼저 부딪힌 설계 갈림길은 파일을 언제 스토리지에 저장하느냐였다. 두 가지 선택지가 있었다. 하나는 사용자가 파일을 고르는 즉시 업로드해 저장해 두는 방식이고, 다른 하나는 사용자가 글쓰기를 마치고 발행 버튼을 누를 때 한꺼번에 올리는 방식이었다. 둘은 사용자 경험과 정합성에서 정반대의 장단점을 가졌다.


선택 즉시 저장은 발행이 빨랐다. 사용자가 글을 쓰는 동안 파일은 이미 백그라운드로 올라가 있으니, 발행 버튼을 누르면 텍스트만 저장하면 돼서 순식간에 끝났다. 여러 장을 올릴 때 특히 좋았다. 사용자가 글을 다 쓸 때쯤엔 이미지들이 진작 다 올라가 있어, 발행 순간의 대기가 거의 없었다. 체감 속도가 빨랐다.


반면 발행 시 저장은 정합성이 깔끔했다. 발행된 글에 딸린 파일만 스토리지에 존재하니, 올리다 만 파일이 남을 여지가 적었다. 대신 발행 순간에 모든 파일을 한꺼번에 올려야 해서, 파일이 많거나 크면 발행 버튼을 누르고 한참 기다려야 했다. 정합성은 좋지만 경험이 굼떴다. 무엇을 우선하느냐의 문제였다.


나는 사용자 경험을 위해 선택 즉시 저장 쪽으로 기울었다. 이미지를 고르면 곧바로 압축해 올리고, 화면에는 업로드된 결과를 보여주는 방식이다. 사용자는 글을 쓰는 내내 이미지가 잘 올라갔는지 확인할 수 있었고, 발행은 가벼웠다. 다만 이 선택에는 대가가 따랐고, 그 대가를 어떻게 감당하느냐가 다음 과제였다.


2. 즉시 저장의 그림자, 고아 파일

선택 즉시 저장의 대가는 고아 파일이었다. 사용자가 이미지를 여러 장 올리다가 마음을 바꿔 글쓰기를 취소하면, 이미 올라간 파일들은 어느 글에도 속하지 않은 채 스토리지에 남았다. 발행되지 않은 글의 파일, 즉 주인 없는 고아가 생기는 것이다. 이걸 방치하면 스토리지에 쓰레기가 쌓였다.


고아는 여러 경로로 생겼다. 글쓰기 취소가 대표적이고, 이미지를 올렸다가 본문에서 지우고 발행한 경우, 혹은 업로드 도중 연결이 끊긴 경우에도 짝 없는 파일이 남았다. 선택 즉시 저장을 택한 이상 이런 고아는 필연이었다. 문제는 이걸 없애느냐가 아니라, 어떻게 관리하고 정리하느냐였다.


한 가지 접근은 발행 시점에 실제로 본문에 쓰인 파일이 무엇인지 대조하는 것이었다. 발행할 때 본문에 등장하는 파일 목록을 확정하고, 올렸지만 본문에 없는 파일은 정리 대상으로 표시하는 식이다. 발행이 파일의 최종 소속을 결정하는 순간이 되는 것이다. 이렇게 하면 발행된 글은 자기 파일만 깔끔하게 갖게 됐다.


아예 발행되지 않은 글의 고아는 별도로 훑어 정리해야 했다. 어느 글에도 연결되지 않은 채 일정 시간이 지난 파일을 주기적으로 찾아 지우는 것이다. 이 정리는 뒤에 나올 스토리지 운영 편의 핵심 주제이기도 했다. 즉시 저장이 준 편의의 청구서를, 주기적 청소로 갚는 셈이었다.


3. 함수를 통과하는 구조의 천장

업로드의 물리적 경로도 설계의 큰 축이었다. 내 기본 구조에서는 브라우저가 보낸 파일이 엣지 함수의 본문을 거쳐 스토리지로 들어갔다. 함수가 파일을 받아 검증하고 스토리지에 넣는 흐름이라 통제가 쉬웠다. 시그니처 검증이나 크기 확인 같은 처리를 함수가 중간에서 다 할 수 있었기 때문이다.


하지만 이 통과 구조에는 천장이 있었다. 함수가 받을 수 있는 요청 본문에 한도가 있어서, 그보다 큰 파일은 함수에 닿기 전에 막혔다. 앞 편의 아이폰 영상이 이 천장에 부딪힌 대표 사례였다. 함수를 통과시키는 한 이 한도는 사라지지 않았고, 전송 오버헤드까지 감안하면 실제 상한은 더 낮았다. 편한 통제의 대가가 이 천장이었다.


이미지에는 이 천장이 거의 문제되지 않았다. 이미지는 브라우저에서 미리 압축해 작게 만들어 보내니 한도에 한참 못 미쳤다. 문제는 압축을 업로드 시점에 못 하는 동영상이었다. 동영상은 원본이 그대로 함수를 통과해야 하니, 큰 동영상일수록 천장에 가까워졌다. 미디어 종류에 따라 이 구조의 적합성이 갈렸다.


그래서 천장이 문제되는 건 사실상 큰 동영상뿐이었다. 대다수 업로드인 이미지는 통과 구조로 충분했고 통제도 편했다. 굳이 전체 업로드 구조를 갈아엎기보다, 천장이 걸리는 큰 파일에만 다른 경로를 두는 것이 합리적이었다. 여기서도 하나의 구조로 다 밀지 않고 경우에 따라 경로를 나누는 익숙한 사고가 나왔다.


4. 직접 업로드로 천장을 걷다

천장을 걷어내는 방법은 브라우저가 스토리지에 직접 올리게 하는 것이었다. 함수를 통과시키는 대신, 함수는 짧은 시간 유효한 업로드 허가만 발급하고, 브라우저는 그 허가로 스토리지에 파일을 곧장 올린다. 파일이 함수 본문을 지나지 않으니 함수 본문 한도라는 천장이 아예 사라졌다. 대용량 업로드의 근본 해법이었다.


이 방식은 서버 부담도 줄였다. 파일이 함수를 통과하지 않으니 함수의 처리 시간과 대역폭을 잡아먹지 않았다. 함수는 허가를 내주는 가벼운 일만 하고, 무거운 전송은 브라우저와 스토리지가 직접 처리했다. 서버를 얇게 유지한다는 서버리스 철학과도 잘 맞았다. 큰 파일일수록 이 방식의 이점이 컸다.


대신 통제가 어려워지는 대가가 있었다. 함수를 통과하지 않으니, 파일이 스토리지에 들어오는 순간에 시그니처를 검증하거나 크기를 막는 처리를 중간에서 하기 어려웠다. 허가를 내줄 때 조건을 걸거나, 업로드된 뒤에 검증하는 식으로 통제를 재구성해야 했다. 그리고 허가의 유효 시간은 되도록 짧게 잡아 오남용을 막아야 했다. 편의와 통제의 트레이드오프였다.


그래서 이 직접 업로드는 모든 파일이 아니라 천장이 걸리는 큰 동영상에만 선택적으로 적용하는 게 합리적이었다. 작고 흔한 이미지는 통제가 편한 기존 통과 구조로, 크고 드문 동영상은 천장 없는 직접 업로드로 보내는 것이다. 영상이 드문 사이트에서 전체를 직접 업로드로 바꾸는 건 과했다. 필요한 곳에만 복잡도를 지불하는 게 옳았다.


5. 우리의 절충안

이 모든 것을 종합한 내 절충안은 이랬다. 저장 시점은 선택 즉시로 해서 발행을 빠르게 하고, 그 대가인 고아 파일은 발행 시 대조와 주기적 청소로 관리한다. 업로드 경로는 대다수인 이미지를 통과 구조로 편하게 통제하고, 천장이 걸리는 큰 동영상만 직접 업로드나 한도 상향으로 대응한다. 하나의 정답이 아니라 경우별 조합이었다.


이 절충의 바탕에는 실제 사용 분포가 있었다. 대다수 업로드는 작은 이미지였고, 큰 동영상은 소수였다. 그래서 다수를 위한 기본 경로는 단순하고 편하게 유지하고, 소수의 문제 사례에만 복잡한 해법을 얹는 것이 전체 복잡도를 최소화했다. 분포를 무시하고 모든 경우를 똑같이 대우하면 시스템만 무거워졌다. 드문 예외를 위해 흔한 다수까지 복잡한 경로로 밀어 넣으면, 얻는 것보다 잃는 것이 컸다. 흔한 것은 단순하게, 드문 것은 특별하게 다루는 원칙이 여기서도 그대로 통했다.


돌아보면 업로드 파이프라인 설계는 순수하게 기술적인 결정이라기보다 우선순위의 문제였다. 속도냐 정합성이냐, 통제냐 확장성이냐 사이에서 무엇을 얼마나 취할지 고르는 일이었다. 정답이 정해져 있지 않으니, 내 사이트의 사용 패턴과 운영 여력을 근거로 균형점을 잡아야 했다. 설계란 결국 트레이드오프를 의식적으로 선택하는 일이었다. 모든 걸 다 가지려 하면 어느 것도 제대로 못 가지니, 무엇을 포기할지 먼저 정하는 게 오히려 설계의 출발이었다. 포기를 명시적으로 고르는 것과 얼떨결에 잃는 것은 전혀 달랐다.


파일을 잘 받아 저장하는 이야기는 여기까지다. 그런데 저장한 파일은 계속 스토리지에 쌓이고, 앞서 본 고아 파일도 생긴다. 이걸 방치하면 비용과 혼란이 눈덩이가 된다. 다음 편에서는 나가는 대역폭 비용을 0으로 만든 스토리지 선택과, 주인 없는 파일을 찾아 정리하는 운영 이야기를 다룬다.