HTTP 기반으로 파일을 업로드할 경우 다음과 같은 문제가 있다.

  1. 파일 업로드가 중단되었을 때, 중단된 곳부터 다시 업로드할 수 있는 방법이 없다.
  2. 파일 업로드의 진행 상태를 알 수가 없다.
  3. 단순히 파일을 업로드하는 기능밖에는 없다.
  4. 큰 사이즈의 파일을 업로드하는데 문제가 있다.

이런 단점을 해결하기 위해 tus를 사용할 수 있다. 여기서는 tus의 동작 방식을 간단하게 정리해 보자.

tl;dr, 동작 방식

먼저 빠르게 동작 방식만 정리해 보자.

기본적인 tus의 동작 방식은 간단하다. 큰 사이즈의 파일을 한 번에 업로드하지 말고 잘게 나누어진 파일을 지속적으로 업로드하는 것이 기본 개념이다. 이 방법으로 얻을 수 있는 장점은 다음과 같다.

  1. 업로드를 중단된 시점부터 이어서 진행할 수 있다: 파일 업로드가 도중에 중단되더라도 그 시점의 조각부터 이어서 업로드하면 된다.
  2. 진행률을 알 수 있다: 전체 파일을 동일한 사이즈의 조각으로 나눈다면 현재까지 업로드 된, 그리고 앞으로 업로드해야 할 조각의 수를 알 수 있다.

이 업로드는 기본 HTTP 방식으로 진행된다.

사용하는 곳

그렇게 많이 쓰이는 것은 아닌 것 같고, 일단은 소개 페이지에 보면 다음과 같은 회사에서 사용한다고 나와 있다.

서버/클라이언트 구현체

소개는 상당히 그럴듯했지만 그냥 사용하기에는 절차가 꽤 수고스러워서, 서버와 클라이언트가 준비되어야 한다. 이 문서를 작성하며 진행된 테스트는 다음의 서버/클라이언트를 사용하였다.

  • Server: tusd
  • Client
    • tus-py-client: 정상 동작하지 않는 것 같다.
    • go-tus: 동작은 하는데 기능이 완전하지 않다.

기본 컨셉

TUS의 기본 컨셉은 클라이언트의 업로드 대상 파일에 연결되는, 서버의 고유한 파일에 파일 조각을 순차적으로 HTTP 업로드하는 것이다.

서버의 고유한 파일은 Unique ID, UID를 키로 가지며 이 UID는 각각 파일의 실제 데이터를 가지는 파일 UID.bin, 파일의 메타데이터를 가지는 UID.info 파일로 나누어 저장한다.

업로드의 동작 방식

여기서부터는 실제로 업로드가 어떤 순서로 이루어지는지 알아보자.

tus에서는 파일의 업로드를 항상 “이어 업로드"의 개념으로 보는 것 같다. 왜냐하면 문서를 읽어 보았을 때, 클라이언트에서는 파일 사이즈가 0이라 하더라도 여기에 파일을 덧붙여 올리는 개념으로 적혀 있기 때문이다. 이는 Core Protocol의 PATCH를 보면 알 수 있는데, 어쨌든 업로드는 다음과 같은 순서로 진행된다.

  1. 클라이언트는 서버에 파일이 존재하는지 확인한다.
  2. 서버는 파일이 있는 것을 확인했다면, 어디까지 업로드되었는지 클라이언트에게 전달한다.
  3. 클라이언트에서는 전달받은 정보를 통해, 해당 조각을 업로드한다.
  4. 서버는 기존 파일에 3에서 업로드한 파일 조각을 붙이고 정상 응답을 전달한다.
  5. 클라이언트는 다음 조각을 다시 업로드한다.
  6. 파일 업로드가 완료될 때 까지 3~5 반복

Core Protocol

tus의 기본적인 목적은 “어떻게 중단된 업로드를 이어나가는가?“이다. 따라서 “이어 업로드"를 위해 필요한 가장 최소한의 기능들이 있으며 이를 tus에서는 Core Protocol이라 한다. 모든 tus의 서버와 클라이언트들은 코어 프로토콜을 반드시 구현하여야 한다. Core Protocol은 업로드 URL과 그에 대응하는 HTTP Method의 쌍으로 이루어지며, 그 내용은 아래와 같다.

요청 HEAD는 파일의 어느 오프셋부터 업로드가 이루어져야 할지를 결정하기 위해 사용한다.

클라이언트는 파일을 업로드하기 전에 항상 어느 오프셋부터 업로드해야 하는지를 서버에 물어 보아야 한다. 이미 업로드를 완료했을 수도 있고, 업로드가 중단되었을 수도 있고, 아예 업로드를 진행한 적이 없었을 수도 있기 때문이다.

서버에 HEAD요청을 보내면 응답 헤더에 Upload-Offset이 전달된다. 만약 업로드가 진행되지 않았다면 Upload-Offset은 0이 되며 업로드가 완료되었다면 Upload-Offset이 파일 사이즈가 될 것이다.

HEAD 메소드를 호출할 경우와 그 요청과 응답의 예는 다음과 같다.

Request:

HEAD /files/24e533e02ec3bc40c387f1a0e460e216 HTTP/1.1
Host: tus.example.org
Tus-Resumable: 1.0.0
Response:

HTTP/1.1 200 OK
Upload-Offset: 70
Tus-Resumable: 1.0.0

PATCH

파일 한 조각을 올리기 위해서 PATCH Method를 사용한다. 일반적인 http 파일 업로드와 마찬가지로 body에 데이터를 담아 파일을 보내며, 올바른 업로드를 위해 몇 가지 정보를 헤더로 실어 보낸다.

그 요청과 응답의 예는 다음과 같다.

Request:

PATCH /files/24e533e02ec3bc40c387f1a0e460e216 HTTP/1.1
Host: tus.example.org
Content-Type: application/offset+octet-stream
Content-Length: 30
Upload-Offset: 70
Tus-Resumable: 1.0.0

[remaining 30 bytes]
Response:

HTTP/1.1 204 No Content
Tus-Resumable: 1.0.0
Upload-Offset: 100

클라이언트에서 Upload-Offset 70부터 길이 30의 조각을 서버로 보낸다. 이를 받은 서버는 응답으로 100까지 업로드되었음을 Upload-Offset으로 알린다. 파일이 무사히 업로드됐으면 204 No Content를 돌려줘 새로운 조각을 보내달라는 요청을 한다. 결국 클라이언트는 서버에 모든 조각을 올릴 때까지 PATCH를 반복해서 호출하게 된다.

OPTIONS

OPTIONS Method는 서버의 설정이나 상태 등을 확인할 때 사용한다. 예를 들어 서버의 버전이라거나, 사용 가능한 기능이 무엇인지를 먼저 확인하고 이 결과로 클라이언트에서 분기를 주면 될 것이다.

그 요청과 응답의 예는 다음과 같다.

Request:

OPTIONS /files HTTP/1.1
Host: tus.example.org
Response:

HTTP/1.1 204 No Content
Tus-Resumable: 1.0.0 Tus-Version: 1.0.0,0.2.2,0.2.1
Tus-Max-Size: 1073741824
Tus-Extension: creation,expiration

Tus-Extension에 대해서는 아래에서 조금 더 자세히 설명할 것이다.

Protocol Extension

Core Protocol을 통해 파일이 클라이언트로부터 서버로 전달되는 과정을 살펴볼 수 있었다. 하지만 이런 문제가 있다. 서버에 아직 파일이 업로드된 적이 한 번도 없는 경우에는 어떻게 하는가? 이어서 업로드를 하려고 해도 파일이 존재하지 않으므로 이어서 업로드를 할 수 없게 된다. 따라서 파일을 최초로 생성하는 API가 필요함을 알 수가 있다.

이런 식으로 Core Protocol 이외에 필요한 기능을 더 구현할 수 있다. 이를 Protocol Extension이라 하며, 당연하게도 서버/클라이언트 모두 이 기능에 대한 구현이 되어 있어야 한다.

사용 가능한 모든 Protocol Extension은 Core Protocol OPTIONS Method, Tus-Extension 헤더를 통해 그 리스트를 확인할 수 있어야 한다.

여기서는 tus 문서에서 소개하는 기본적인 Extension 중에 Creation, Expiration, Checksum을 확인해 보자.

Creation

빈 파일을 최초로 생성하는 API다. 이는 Core Protocol과 마찬가지로 반드시 서버/클라이언트에 구현되어야 하는 기능이다. 문서상으로는 SHOULD로 표시되지만, 실제로 이게 없으면 파일을 업로드할 수 없게 될 것이다.

서버는 랜덤한 영문자로 이루어진 ID를 발급하게 되며 이 ID기준으로 업로드 폴더에 다음과 같은 파일이 생성된다.

  • ID.bin: 실제 파일의 데이터를 저장한다. 당연히 생성 순간은 사이즈가 0이다.
  • ID.info: 파일 업로드를 위해 필요한 기본적인 데이터를 json형태로 저장한다.

서버는 파일 생성 후 ID와 함께 201 Created를 응답으로 전달한다. 클라이언트는 이 ID에 HEAD/PATCH Method를 사용하여 “이어 업로드"를 하게 될 것이다.

Creation의 주의할 점

설명을 보면 알겠지만 파일의 원래 이름은 보존되지 않는다. 즉, ID가 발급된 시점에서 기존 파일 이름은 ID로 대체되는 것이다. 그리고 이 ID라는 것이 어떤 규칙이 있어서 항상 같은 파일은 같은 ID로 발급되는 것이 아니다. 따라서 매번 업로드될때마다 ID가 랜덤으로 발급되므로, 이런 이유로 인해 다음과 같은 것들을 알 수 있다.

  1. 클라이언트가 Creation에 대한 성공 응답을 받았다면, 적어도 업로드가 완료되는 순간까지는 이 ID와 실제 파일 이름을 매핑해서 들고 있어야 한다. 만약 이를 모를 경우 업로드가 중단-재개되었을 때 어디로 이어서 업로드를 해야 하는지 전혀 알 수가 없게 된다. ID는 어떤 법칙이나 규칙을 통해 항상 같은 이름으로 발급되지 않고 항상 랜덤으로 발급된다.
  2. 그러므로 클라이언트에서는 매번 같은 파일을 업로드해도 매번 다른 ID를 발급받게 될 것이다. 이를 피하기 위해 업로드가 완료된 경우 원본을 삭제하거나 하는 식의 방법을 만들어야 하는 것 같다.
  3. 수많은 클라이언트에서 업로드한 ID.bin 파일이 존재하므로, 서버에서는 당연히 이 파일이 어떤 의도로 업로드되었는지 알 수가 없다. 따라서 클라이언트에서는 파일 업로드가 완료된 시점에서 어떤 추가적인 작업을 진행하도록 서버에 알림을 주어야 할 수 있다.
  4. Upload-Metadata라는 헤더를 통해 클라이언트에서 서버에 파일과 함께 특정 정보를 전달할 수 있다. 예를 들어 바로 위에서 원본 파일 이름을 보존할 수 없다고 했지만, 이 Upload-Metadata에 파일 이름의 정보를 전달할 경우 ID.info파일에 해당 정보가 기록되므로, 결국에는 ID.info파일을 열어보는 것으로 원래의 파일이 어떤 것인지를 알 수 있게 된다. 물론 이것도 ID.info가 굉장히 많은 경우 그다지 좋은 방법은 아니다.

Expiration

업로드가 중단된 후 이어서 업로드를 할 때, 허용하는 기간을 말한다. 예를 들어 어떤 파일을 업로드 중에 급작스런 상황으로 인해 업로드가 중단되었다면, 적어도 특정 기간 안에서 이어서 업로드가 가능하다는 뜻이다. 서버에서는 주기적으로 이 시간을 체크하여 더 이상 이어서 업로드가 불가능한 파일을 삭제하는 것으로 디스크의 사용량을 최적화할 수 있다.

Expiration은 Creation API 호출 시 Upload-Expires 헤더를 통해 지정할 수 있다. 클라이언트는 매 PATCH 호출 시마다 이 헤더를 받게 되며, 만약 이어 업로드가 불가능하게 된 경우 서버는 클라이언트에게 410 Gone, 혹은 404 Not Found를 응답으로 전달하여야 한다.

Checksum

파일 업로드의 무결성을 확인할 수 있는 기능이다. 무결성은 Checksum을 가지고 확인하게 되며, 놀랍게도 전체 파일에 대한 Checksum으로 무결성을 확인하는 것이 아닌, PATCH로 업로드되는 매 조각에 대해 무결성을 확인한다.

서버에서는 OPTIONS Method의 응답으로 Tus-Checksum-Algorithm을 전달하게 되며, 클라이언트는 이 알고리즘 중 하나를 선택하여 파일과 함께 Upload-Checksum 헤더에 Checksum을 전달한다. 문제가 없다면 서버는 기존 PATCH의 응답과 마찬가지로 204 No Content를 전달하며, 문제 발생시 460 Checksum Mismatch를 전달한다.

업로드 흐름도

결국 지금까지의 내용을 살펴보면 다음과 같은 흐름으로 업로드가 진행된다는 것을 알 수 있다.