back

대용량 동영상 S3 이어 올리기

3달 전 작성

본 글은 회사 블로그영문으로 게재할 초안을 공개한 것입니다


이미지

사진 속 가위는 피스카스(Fiskars) 사의 제품으로 지금은 우리에게 친숙한 가위 디자인의 원형(prototype) 역할을 한 제품이며, 기능주의 디자인의 대표적인 사례 중 하나입니다. 바우 하우스 운동을 계기로 전개된 기능주의 디자인은 미국 건축가 루이스 설리반(Louis Sullivan)의 유명한 표현으로 그 추구하는 바를 이해할 수 있습니다.

Form Follows Function

이 정신은 서비스 스타트업의 연구 개발에도 적용될 수 있습니다. 기업의 규모가 커져 연구 개발에 잉여 인력이 발생한다면 미래 먹거리를 찾기 위해 잉여 연구력을 발현하는 것은 당연지사입니다. 하지만, 맨 파워가 절대적으로 부족한 스타트업이라면 연구 개발은 회사의 기술 장식물(technical ornament)이 아닌 사용자가 서비스에서 체감할 수 있는 기능을 위해 존재하는 것이 우선입니다.

Alleys의 기술 블로그에서는 그렇게 적용된, 비록 작지만 사용자들에게 직접 영향을 주는 기능들을 고민하고 만들어 가는 과정을 소개해보려 합니다. 그 시작으로, 그동안 국내 사용자에게도 절실했지만, 무엇보다 네팔 매핑 프로젝트에서 강력히 제기된 불편함을 해결한 "S3 이어 올리기" 기능을 소개하려 합니다.

네팔은 크라우드 소싱(crowd-sourcing)으로 지도 매핑 활동이 활발히 일어나는 곳입니다. 다만, 인터넷 인프라가 좋은 편은 아니라 대용량 동영상을 업로드하기에는 어려움이 많습니다. 그곳 사람들은 인터넷이 느리고 자주 끊긴다는 사실 자체에는 충분히 적응되어 있지만, 우리 서비스는 국내의 좋은 인터넷 환경 덕에 네팔의 네트워크 환경에 잘 대응하지 못하고 있었습니다.

요구 사항은 매우 단순했습니다 - 시간을 오려 들여서라도 업로드할테니 중간에 끊겼을 때 이어서 올릴 수 있게 해주세요!

AlleysMap은 촬영 영상 저장을 위해 아마존 웹 서비스(Amazon Web Services)의 스토리지 서비스인 S3를 사용하고 있습니다. S3는 많은 장점을 갖는 서비스임에 틀림 없지만, 어떤 로직을 바탕으로 행동을 세세하게 제어하기에는 한계가 있습니다. 수MB 정도의 작은 파일 여러 개를 올리고 받는 경우라면 업로드 완료된 파일에 대해 DB에 등록해 관리하고 추후 파일을 추가하는 형태로 대응하면 충분합니다. 하지만, 작은 파일이 수백MB, 큰 녀석은 수GB에 달하는 동영상 파일 하나를 업로드하는 과정은 사용자에게도, 클라이언트에게도, 서버에게도 녹록한 과정은 아닙니다.

이 때문에 AWS 공식 SDK를 사용해 개발된 앱에서는 업로드 중에 네트워크 연결이 불안정하여 연결이 끊기는 경우, 혹은 사용자가 의도적으로 업로드를 중단하는 경우, 업로드되던 파일은 무시되어 다시 0%부터 업로드를 시작해야 하는 맹점이 있습니다. 그리고 구현해야 하는 수많은 기능을 줄 세워놓고 있는 개발팀 입장에서는 거창한 기술이나 클라이언트 쪽의 복잡성을 도입하기 보다는 AWS 공식 SDK라는 큰 틀 안에서 간단한 접근으로 그럴싸한 효과를 얻을 방법을 고민해야 했습니다.

가장 먼저 생각할 수 있는 방법은 버퍼 역할을 하는 자체 업로드 서버를 모바일 클라이언트와 S3 사이에 두고 클라이언트와 서버의 협주를 통해 최적의 업로드 경험을 제공하는 방식일 것입니다. 하지만, 이는 추가로 관리해야 하는 서버가 발생한다는 점은 차치하고, 가난한 스타트업에게 가장 중요한 비용 문제를 피해갈 수 없게 합니다. 본의 아니게 해외 여기 저기서 업로드가 발생하는 강제 진출 글로벌 서비스가 된 후로 이미 충분히 좋은 인터넷 환경을 갖는 한국에만 업로드 서버를 배포하는 것은 크게 의미가 없습니다.

더구나 S3에서 제공하는 업로드 가속 기능은 고정비 추가 없이 여러 나라에서 빠른 업로드 경험을 제공할 수 있기에 절대 포기할 수 없는 부분이기도 합니다.

이에 기능 구현을 위한 설계에 돌입하기 전에 지켜야 할 가치들을 정리해 보았습니다.

  • 단순해야 합니다. 벌레(bug)는 복잡하고 어두운 곳에 살기 마련입니다. 설계가 복잡하지 않아야 구현하는 사람도 오해하지 않고, 결과물인 코드도 복잡하지 않아 벌레가 살기 어려워 집니다.
  • 사용자가 촬영한 영상이 가장 소중합니다. 서비스 특성상 사용자가 촬영한 영상에는 상당한 노동과 정성이 들어가 있습니다. 프로그램 상의 버그나 서비스 상의 실수로 촬영 영상이 날아가는 일은 반드시 피해야 합니다.
  • 쉽게 기능이 확장될 수 있어야 합니다. 단시간 내에 개발을 완료하고 사용자를 만나야 하는 제품에 새로 추가할 기능이 충분한 완성도를 가질 때까지 기다릴 수는 없습니다. 가장 쉽고 안전하게 핵심 가치를 전달하고 이후 점차적으로 기능을 개선해갈 수 있어야 합니다.

이미지

  • 위험성은 서버가 안고 가야 합니다. 보통 개발 범위(scope)를 서버 대 클라이언트(iOS, 안드로이드, 웹)로 구분하지만, 사실 배포 후 특성을 고려하면 서버와 웹, 그리고 모바일 클라이언트로 나누는 편이 유용한 경우가 많습니다. 서버나 웹은 이른바 달리는 차의 바퀴를 교체하는 작업이 가능하지만, 모바일 앱은 한번 릴리즈가 되고 나면 다음번 릴리즈까지 소요되는 시간이 길고, 빠르게 릴리즈가 되었다 해도 사용자가 업데이트해주기를 기다려야 하는 어려움이 있습니다.
  • 인터넷이 충분히 빠른 환경의 기존 사용자가 불필요한 불이익을 받아서는 안 됩니다.

이와 같은 나름의 가치 기준을 세운 후 설계에 들어갔습니다.


우선, 클라이언트는 영상 파일을 업로드할 때 파일을 분할해 올립니다.

  • 실제 촬영 중에 파일을 분리하지는 않습니다. 아이폰은 몰라도, 안드로이드는 기기 종류가 다양해 어설프게 촬영 중 파일을 분리하면 영상의 접합 부분이 부자연스럽게 되고 최악의 경우 해당 사용자가 적극적으로 알려주기 전까지 문제를 알지 못하는 사태가 발생할 수 있습니다.
  • 파일을 물리적으로 분할하지도 않습니다. 파일을 물리적으로 분할하게 되면 분할하는 시간만큼 분할 업로드가 필요없는 사용자에게는 불이익이됩니다. 우리에게는 심히 오래된 스트림(stream)이라는 강력한 도구가 있기에 이를 활용합니다.
  • 어느 정도의 크기의 몇 개의 조각으로 나눌지는 전적으로 클라이언트가 결정합니다. 서버는 분할 크기나 개수에 대한 어떤 요구도 하지 않습니다. 클라이언트가 자신의 상황에 적합한 크기로 나누면 그만입니다.
    • AWS S3 공식 SDK를 사용해 파일 업로드를 할 때, 파일 크기가 일정 크기 이상이어야 멀티파트 업로드(multi-part upload)로 속도 상의 이득을 볼 수 있습니다.
    • 하지만, iOS SDK의 경우 이를 제대로 지원하지 못하는 버그가 있어 더 작은 단위로 업로드하는 것이 이득일 수 있습니다.
  • 빠진 조각이나 순서에 대해 서버가 파악할 수 있도록 파일명을 약속된 형태로 맞춰 업로드합니다.

이렇게 분할된 파일을 올리다가 어떠한 이유로든 업로드가 중단되면 최소한 이전에 업로드한 분할 파일까지는 S3에 정상적으로 존재합니다. 모든 조각이 업로드가 완료되기 이전에는 API 서버에 업로드를 알리지 않습니다.

이제 업로드가 중단된 영상을 이어서 업로드할 때, 서버는 클라이언트에 업로드 상황을 알려주는 API를 제공합니다. 서버 개입 없이 클라이언트가 자신의 업로드 상황을 로컬 DB에 각자 저장해둘 수도 있으나, 앱 업그레이드 시마다 큰 말썽을 부리는 부분이 로컬 DB임을 고려하여 가급적 로컬 DB 사용은 배제합니다. 더구나 구현 과정에서 버그로 인해 로컬 DB가 꼬이는 날에는 사용자에게 죽노동으로 촬영한 영상을 날려먹는 최악의 경험을 선사하게 됩니다. 대신 클라이언트는 업로드 상황에 대해 서버에 문의하고, 서버는 아래와 같은 응답을 줍니다.

{
    "id": "hdyL00KW7PqJfHivWlR_2j",
    "sizes": [
      132427656,
      null,
      36244528
    ]
}

이 응답은 서버 역시 DB에서 가져온 내용이 아니라 실제 S3에 업로드된 파일을 조사하여 내주는 것이기 때문에 서버-클라이언트 사이에 일관성이 깨져 파일을 날려먹는 일은 발생하지 않습니다.

이렇게 분할된 파일의 크기 정보(1, 3번째 파일이 업로드되어 있고, 두번째 파일은 올라오지 않음)를 제공하면 클라이언트는 자신이 원하는 방식에 따라 원하는 작업을 수행할 수 있습니다. 어떤 대응이 가능할지 생각해 보겠습니다.

  • 가장 단순한 클라이언트 구현은 앱이 순차적으로 파일을 올리는 경우입니다 - 이 경우 중간 파일이 빠지는(null이 되는) 경우는 없습니다. 이제 앱은 sizes 배열의 길이만 보고 몇번째 분할 부분부터 업로드하면 되는지 판단해 업로드를 진행할 수 있습니다.
  • 잘라서 올린 파일의 크기를 클라이언트가 따로 기억할 필요가 없습니다. 위의 예를 보면 올리지 않은(혹은 못한) 2번째 파일을 원본 파일의 어디서부터 몇 바이트를 읽어 보내야 하는지 간단히 계산이 가능합니다.
  • 네트워크 상황에 따라 클라이언트가 지능적으로 분할할 파일 크기를 결정할 수 있습니다. 네팔이나 공용 와이파이처럼 네트워크 상황이 좋지 않다면 조금 더 작은 크기로 파일을 분할 업로드하여 성공 가능성을 높일 수 있습니다.
  • 영상을 업로드한 부분만큼 저장 공간을 확보하는 것이 중요하다면 (저장 공간 확보를 위한 비용으로 생각하고) 업로드 전에 분할된 파일을 만들어 업로드할 수 있습니다. 업로드가 완료되었음을 API를 통해 확인하는 즉시 부분별로 파일을 삭제할 수 있습니다.
  • 부분 파일을 삭제하는 전략으로 갈 때, 조금이라도 빨리 용량을 확보할 수 있도록 업로드 시간이 짧은 작은 파일부터 업로드한 후 삭제할 수 있습니다.
  • 추후 체크섬(checksum) 같은 업로드된 파일에 대한 메타 정보가 필요한 경우 하위 호환성(backward compatibility)은 해치지 않고 서버 응답에 필요한 정보가 추가될 수 있습니다.
{
    "id": "hdyL00KW7PqJfHivWlR_2j",
    "sizes": [
      132427656,
      null,
      36244528
    ],
    "checksums": [
      "FDCEE6EC79AD95E02EE250115A2F884D1AED03C2",
      null,
      "EE92F2ACF6D7FCBC8092F959D7BB8D88DA1B58A1"
    ]
}
  • 사용자가 영상을 긴 기간에 걸쳐 나눠 올리는 경우 서버 유지보수(maintenance) 과정에서 부분적으로 업로드된 파일이 만료되어 삭제되어도 괜찮습니다. 이후 해당 영상 업로드를 재시도할 때 서버는 "받은 파일이 없다"는 응답을 줄 것이고, 클라이언트는 처음부터 영상을 업로드할 수 있습니다 - 만약, 업로드 진행 상황에 대한 정보를 클라이언트가 관리했다면 분명 시나리오가 복잡해졌을 것입니다.

물론, 나열한 것 중 가장 단순한 형태만 구현해도 "이어 올리기"라는 기능 자체는 완벽하게 동작합니다. 이제 사용자의 피드백을 수집한 후 여력이 될 때 다른 기능을 추가로 구현해줄 수 있습니다.

클라이언트가 모든 파일에 대한 업로드를 마치면 Alleys API 서버에 영상 등록을 알립니다. 이미 분할 파일을 업로드하는 과정에서 앞서 설명했던 서버 응답을 바탕으로 모든 파일이 업로드 되었는지 클라이언트가 확인하였습니다. 하지만, 만에 하나 촬영 영상이 삭제되는 경우를 방지하기 위해 마지막으로 업로드된 파일에 대한 최소한의 정보를 API 서버에 전달합니다. API 서버는 돌다리도 두드려보는 심정으로 실제 S3에 약속대로 파일이 업로드되었는지 확인하고, 문제가 있다고 판단되면 등록을 거절합니다 - 거절된 영상은 등록 실패로 간주되어 원본 파일이 삭제되지 않기 때문에 영상이 등록되지 않고 버려지는 최악의 경우를 막을 수 있습니다.

영상을 처리하는 서버는 앞서 여러 단계에 걸쳐 상호 검증된 분할 파일을 다운받으면서 바로 병합(merge)하는 과정을 거치기에 성능상 추가로 들어가는 비용이 없습니다. 이후 과정은 기존 단일 파일로 업로드된 영상을 처리하는 과정과 동일합니다.


비록 설명은 거창하지만 설계 자체가 단순한 탓에 서버부터 클라이언트까지 실제 구현에는 짧은 시간이 걸렸고, 예상보다 매끄럽게 동작하여 빠르게 릴리즈가 가능했습니다.

다음 글은 보다 유용하고 흥미로운 소재로 만나뵐 수 있기를 기대하며, 이어 올리기가 동작하는 모습으로 첫 글을 마무리 짓겠습니다.

이미지