01. HTTP Basics — 파일 전송의 공통 언어
파일 전송 99%는 HTTP 위에서 일어난다. HTTP는 단순한 텍스트 프로토콜로 시작했지만, 지금은 TCP를 떠나 UDP 위로 옮겨가는 중이다. 이 문서는 1.1·2·3의 차이, 그리고 파일 도메인에서 진짜 중요한 헤더만 추린다.
한 줄 답
HTTP는 “헤더 + 바디” 구조의 텍스트 명령이고, 파일 전송에서 진짜 중요한 건 (1) 어떤 다중화 모델인가, (2) 어떤 캐시 헤더를 쓰는가, (3) 어떤 메서드/상태코드인가 — 이 셋이다.
Why — 왜 HTTP가 표준이 됐는가
1990년대에는 FTP, Gopher, NNTP 등 파일별 프로토콜이 따로 있었다. 그런데 HTTP가 이겼다. 이유:
- 방화벽 친화적: 80/443 포트만 열면 끝. FTP는 PORT 모드 등 능동/수동 두 채널 필요.
- 양방향 비대칭 효율: 클라이언트 단순, 서버 복잡 — 웹 모델과 정확히 일치.
- 무상태(stateless): 서버에 세션을 안 들고 있어도 되니 수평 확장이 자연스럽다.
- 위에 모든 걸 얹기 쉽다: REST·GraphQL·gRPC-Web·WebSocket·WebDAV 모두 HTTP 위.
→ 결국 전송 프로토콜이 아니라 분산 시스템의 공통 ABI가 됐다.
How — HTTP/1.1 vs 2 vs 3 다중화 모델
HTTP/1.1 (1997, RFC 2068)
- TCP 1개 = 요청 1개를 순차 처리.
keep-alive로 TCP는 재사용하지만 요청은 직렬. - 브라우저는 도메인당 6개 TCP를 병렬로 열어 우회했다.
- Head-of-line blocking: 1번 요청이 느리면 2번이 기다린다.
HTTP/2 (2015, RFC 7540)
- 하나의 TCP 위에 N개 스트림을 다중화. 바이너리 프레임 기반.
- HPACK으로 헤더 압축 (반복되는
User-Agent,Cookie를 인덱스로). - 서버 푸시 (실패한 실험 — 거의 비활성화됨).
- 여전히 TCP의 head-of-line blocking은 남는다 — TCP 패킷 1개 잃으면 모든 스트림이 멈춘다.
HTTP/3 (2022, RFC 9114)
- QUIC 위에서 동작 (UDP + TLS 1.3 통합).
- 패킷 손실은 그 스트림만 영향. 다른 스트림은 계속 흐른다.
- 0-RTT 연결: 캐시된 세션 키로 첫 패킷에 데이터를 실어 보냄.
- 모바일에 강하다 — Wi-Fi → LTE 전환에도 connection ID로 이어짐.
비교표
| 항목 | HTTP/1.1 | HTTP/2 | HTTP/3 |
|---|---|---|---|
| 전송 계층 | TCP | TCP | UDP (QUIC) |
| 다중화 | ✗ (직렬) | ✓ (스트림) | ✓ (스트림) |
| Head-of-line blocking | 요청 레벨 | TCP 레벨 | 없음 |
| 헤더 압축 | gzip(experimental) | HPACK | QPACK |
| TLS | 별도 | 별도(권장 필수) | 통합 |
| 0-RTT | ✗ | ✗ | ✓ |
| Connection migration | ✗ | ✗ | ✓ (mobile) |
| 큰 파일 단일 다운로드 | ★★★ | ★★★ | ★★★ |
| 다수 작은 파일 | ★ | ★★★ | ★★★ |
| 패킷 손실 환경 | ★★ | ★ | ★★★ |
실전 메모: 5GB 단일 비디오 다운로드는 HTTP/1.1·2·3 차이가 거의 없다. 차이는 다수의 작은 파일 (HLS 세그먼트 수백 개, 이미지 갤러리)에서 폭발한다.
What — 진짜 중요한 헤더와 메서드
메서드 (파일 도메인 한정)
| 메서드 | 용도 | 멱등성 | S3 |
|---|---|---|---|
GET | 다운로드 | ✓ | GetObject |
PUT | 업로드 (전체 교체) | ✓ | PutObject, UploadPart |
POST | 생성 또는 multipart 업로드 시작 | ✗ | CreateMultipartUpload |
HEAD | 메타데이터만 (바디 없이) | ✓ | HeadObject — 존재 확인 핵심 |
DELETE | 삭제 | ✓ | DeleteObject |
PATCH | 부분 수정 | ✗ | tus 프로토콜에서 사용 |
캐시 관련 헤더 (가장 중요)
Cache-Control (RFC 7234)
Cache-Control: public, max-age=31536000, immutable
Cache-Control: no-cache, no-store, must-revalidate
Cache-Control: private, max-age=300| 디렉티브 | 의미 |
|---|---|
public | 모든 캐시(브라우저+CDN) 저장 OK |
private | 브라우저만, CDN은 안 됨 (사용자별 데이터) |
max-age=N | N초 동안 신선 |
s-maxage=N | CDN 전용 max-age (브라우저보다 우선) |
no-cache | 매번 검증해야 (캐시는 함) |
no-store | 캐시 자체를 금지 |
immutable | 만료 전엔 절대 검증 안 함 (해시 들어간 URL용) |
stale-while-revalidate=N | 만료돼도 N초간 옛 응답 쓰면서 백그라운드 갱신 |
이 패턴: media 파일은
public, max-age=31536000(1년 immutable), 첨부 attachment는no-cache, no-store, must-revalidate. S3 키 prefix(cache-enabled/vscache-disabled/)로 분기.
ETag + If-None-Match (조건부 GET)
# 첫 요청
GET /video.mp4
HTTP/1.1 200 OK
ETag: "9b5c2..."
Content-Length: 5242880
# 다음 요청
GET /video.mp4
If-None-Match: "9b5c2..."
HTTP/1.1 304 Not Modified304는 바디 없이 헤더만 → Egress 비용 0.- ETag 생성: 단일 객체는 MD5, multipart는 각 파트 MD5의 MD5 +
-N(이 함정은 06번 문서에서).
Last-Modified + If-Modified-Since
ETag의 단순 버전. 1초 단위 정밀도라 고빈도 변경 파일에는 약하다. 일반적으로 ETag 권장.
콘텐츠 관련 헤더
| 헤더 | 예시 | 용도 |
|---|---|---|
Content-Type | video/mp4 | MIME — 브라우저 디코더 선택 |
Content-Length | 5242880 | 바디 크기 (바이트) |
Content-Disposition | attachment; filename="report.pdf" | 강제 다운로드 + 파일명 |
Content-Encoding | gzip, br | 본문 압축 |
Content-Range | bytes 0-1023/5242880 | Range 응답에서 사용 (다음 문서) |
Accept-Ranges | bytes | 서버가 Range 요청 지원함을 광고 |
상태코드 (파일 도메인 자주 쓰는 것만)
| 코드 | 의미 | 언제 |
|---|---|---|
200 | OK | 정상 GET/PUT |
201 | Created | 새 객체 업로드 성공 |
204 | No Content | DELETE 성공 |
206 | Partial Content | Range 요청 응답 |
301/308 | 영구 리다이렉트 | CDN → Origin |
304 | Not Modified | 조건부 GET 캐시 hit |
403 | Forbidden | presigned 만료 / 서명 불일치 |
404 | Not Found | 객체 없음 |
412 | Precondition Failed | If-Match 등 실패 |
416 | Range Not Satisfiable | Range가 객체 크기 초과 |
503 | Service Unavailable | S3 throttle, CDN origin down |
실전 curl 예시
# 1) HEAD로 메타데이터만
curl -I https://example.com/video.mp4
# HTTP/2 200
# content-type: video/mp4
# content-length: 5242880
# accept-ranges: bytes
# etag: "9b5c2dc..."
# 2) 조건부 GET — 캐시 hit이면 304
curl -H 'If-None-Match: "9b5c2dc..."' https://example.com/video.mp4 -o /dev/null -w "%{http_code}\n"
# 304
# 3) presigned PUT 업로드
curl -X PUT --upload-file ./local.mp4 \
-H 'Content-Type: video/mp4' \
'https://bucket.s3.amazonaws.com/key?X-Amz-Algorithm=...&X-Amz-Signature=...'
# 4) HTTP 버전 강제
curl --http3 https://example.com/ # QUIC
curl --http2-prior-knowledge https://... # cleartext h2 (사실상 안 씀)What-if — 흔한 함정
| 함정 | 증상 | 원인 | 해결 |
|---|---|---|---|
| 같은 이미지를 매 요청마다 새로 받음 | 트래픽 5배 | Cache-Control 누락 | public, max-age=... |
| 새 버전 배포해도 옛 이미지가 보임 | 사용자 컴플레인 | immutable에 캐시 무효화 안 함 | URL에 해시 (logo.9b5c2.png) |
| presigned URL이 캐시됨 | 만료된 URL이 CDN에 박혀 외부 사용자에게 403 | CDN이 쿼리 무시하고 캐시 | CDN의 쿼리 포함 캐시키 또는 CloudFront Signed URL 사용 |
Content-Length 누락 | 진행률 바 작동 안 함 | streaming 응답 | Transfer-Encoding: chunked 시 의도된 동작 |
| 비디오 재생이 처음부터만 됨 | 시킹 안 됨 | Accept-Ranges: bytes 없음 / 서버가 Range 미지원 | nginx aio 설정, S3는 자동 지원 |
| HTTP/2에서 헤더가 의도와 다르게 동작 | 대소문자 이슈 | HTTP/2는 헤더를 소문자로 강제 | 코드는 항상 소문자로 |
Insight — 흥미로운 이야기
“HTTP/2의 서버 푸시는 왜 죽었나”
2015년 등장 당시 “혁명”으로 불렸던 서버 푸시는 2022년 Chrome이 기본 비활성화했다. 이유: 서버는 클라이언트가 이미 캐시한 리소스인지 알 수 없어서 매번 푸시했고, 결국 대역폭을 낭비했다. 103 Early Hints가 그 자리를 대체했다.
“QUIC을 만든 건 Google이지만, 첫 사용자는 YouTube였다”
모바일 환경에서 비디오 첫 프레임 시간(TTFF)을 줄이려고 만들어진 게 QUIC이다. 즉, HTTP/3는 비디오를 위한 프로토콜로 출발했다. 우리가 파일 도메인 문서에서 HTTP/3를 다루는 게 우연이 아닌 이유.
“브라우저는 왜 도메인당 동시 6개?”
RFC 2616이 “도메인당 2개 권장”이라고 적었던 시절의 잔재다. Chrome은 6, Firefox 6, Safari 6. DNS prefetch와 도메인 샤딩이 한때 유행했던 이유. HTTP/2 이후엔 무의미해졌고, 도메인 샤딩은 오히려 안티패턴이 됐다.
요약 + Mermaid
HTTP는 1.1 → 2 → 3로 가며 다중화의 위치가 바뀌었다 (요청 → 스트림 → UDP). 파일 도메인에서 진짜 중요한 건 캐시 헤더(
Cache-Control/ETag)와 상태코드(특히 206/304)다. curl로 헤더를 직접 보는 습관이 디버깅의 절반을 해결한다.