📁 File1. 전송 프로토콜01. HTTP Basics — 파일 전송의 공통 언어

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.1HTTP/2HTTP/3
전송 계층TCPTCPUDP (QUIC)
다중화✗ (직렬)✓ (스트림)✓ (스트림)
Head-of-line blocking요청 레벨TCP 레벨없음
헤더 압축gzip(experimental)HPACKQPACK
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=NN초 동안 신선
s-maxage=NCDN 전용 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/ vs cache-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 Modified
  • 304는 바디 없이 헤더만 → Egress 비용 0.
  • ETag 생성: 단일 객체는 MD5, multipart는 각 파트 MD5의 MD5 + -N (이 함정은 06번 문서에서).

Last-Modified + If-Modified-Since

ETag의 단순 버전. 1초 단위 정밀도라 고빈도 변경 파일에는 약하다. 일반적으로 ETag 권장.

콘텐츠 관련 헤더

헤더예시용도
Content-Typevideo/mp4MIME — 브라우저 디코더 선택
Content-Length5242880바디 크기 (바이트)
Content-Dispositionattachment; filename="report.pdf"강제 다운로드 + 파일명
Content-Encodinggzip, br본문 압축
Content-Rangebytes 0-1023/5242880Range 응답에서 사용 (다음 문서)
Accept-Rangesbytes서버가 Range 요청 지원함을 광고

상태코드 (파일 도메인 자주 쓰는 것만)

코드의미언제
200OK정상 GET/PUT
201Created새 객체 업로드 성공
204No ContentDELETE 성공
206Partial ContentRange 요청 응답
301/308영구 리다이렉트CDN → Origin
304Not Modified조건부 GET 캐시 hit
403Forbiddenpresigned 만료 / 서명 불일치
404Not Found객체 없음
412Precondition FailedIf-Match 등 실패
416Range Not SatisfiableRange가 객체 크기 초과
503Service UnavailableS3 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에 박혀 외부 사용자에게 403CDN이 쿼리 무시하고 캐시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로 헤더를 직접 보는 습관이 디버깅의 절반을 해결한다.