Table of contents
- 웹의 공용어, HTTP
- 요청과 응답의 생김새
- 메서드 — 서버에 무엇을 시킬 것인가
- 상태 코드 — 결과를 세 자리로 말한다
- 헤더 — 본문 밖에서 오가는 메타정보
- HTTP/1.1 — 오랜 기본값의 한계
- HTTP/2 — 멀티플렉싱의 등장
- HTTP/3 — 아예 UDP 위로
- HTTP에는 구멍이 있다
- 왜 이제는 HTTPS가 기본값인가
웹의 공용어, HTTP
지금 이 글을 읽는 동안에도 브라우저 뒤에서는 HTTP(HyperText Transfer Protocol — 웹에서 클라이언트와 서버가 데이터를 주고받는 약속) 요청이 끊임없이 오간다. 이미지 하나, 스타일시트 하나, API 응답 하나가 모두 HTTP 위에 실린다. HTTP가 멈추면 웹이 멈춘다고 해도 과장이 아니다.
HTTP는 이름 그대로 하이퍼텍스트 전송에서 출발했지만, 지금은 JSON·바이너리·비디오 스트림까지 거의 모든 것을 나른다. 한 줄로 요약하면 HTTP는 “클라이언트가 요청을 보내고, 서버가 응답한다” 는 약속이다. 이 단순함이 웹을 지금 모습으로 키웠다.
4편에서 DNS가 이름을 IP로 바꿔주는 과정을 봤다. 이 편에서는 그 IP로 연결된 뒤 실제로 어떤 메시지가 오가는지, 그 메시지가 어떻게 HTTPS로 안전해지는지, 그리고 HTTP 자체가 어떻게 HTTP/2와 HTTP/3까지 진화했는지를 다룬다.
요청과 응답의 생김새
HTTP 메시지는 사람이 읽을 수 있는 텍스트다. 이게 HTTP의 큰 장점 중 하나다. curl이나 telnet만 있으면 맨손으로 요청을 만들 수도 있다.
요청은 이렇게 생겼다.
GET /posts/42 HTTP/1.1
Host: api.example.com
User-Agent: curl/8.1.0
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1...
세 부분으로 나뉜다. 시작 줄(GET /posts/42 HTTP/1.1), 헤더(키-값 여러 줄), 그리고 필요하면 본문이 이어진다. GET이라 본문은 없다. Host 헤더만 있어도 서버는 어느 도메인을 요청받았는지 알 수 있다. 가상 호스팅 시대에 꼭 필요한 장치다.
서버의 응답은 비슷한 모양이다.
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 58
Cache-Control: max-age=60
{"id":42,"title":"HTTP","author":"ioob"}
시작 줄이 상태 코드로 바뀌었고(200 OK), 헤더 뒤에 빈 줄 하나를 두고 본문이 온다. 빈 줄이 헤더와 본문의 경계를 알리는 구분자다. 단순하지만 40년째 이 약속으로 웹이 돌아간다.
curl -v 한 줄로 이 구조를 직접 관찰할 수 있다.
curl -v https://api.github.com/zen
# > GET /zen HTTP/2
# > Host: api.github.com
# > User-Agent: curl/8.1.0
# < HTTP/2 200
# < content-type: text/plain;charset=utf-8
# < content-length: 67
# ...
# Non-validated stamps diminish your credibility.
>가 요청, <가 응답이다. HTTP/2부터는 헤더 이름이 소문자로 정규화된다는 점만 알아두면 된다.
메서드 — 서버에 무엇을 시킬 것인가
시작 줄 맨 앞의 메서드는 서버에 어떤 동작을 시킬지 의도를 표현한다. 자주 쓰는 다섯 가지를 보자.
- GET: 자원을 조회한다. 본문이 없고, 같은 요청을 여러 번 보내도 서버 상태가 변하지 않는다(멱등, idempotent)
- POST: 자원을 생성하거나 서버에 데이터를 제출한다. 멱등 보장 없음. 같은 요청을 두 번 보내면 두 개가 생길 수 있다
- PUT: 자원을 전체 교체한다. 같은 요청을 여러 번 보내도 결과가 같다(멱등)
- PATCH: 자원의 일부만 수정한다. 멱등은 구현에 따라 다르다
- DELETE: 자원을 삭제한다. 멱등이다. 이미 삭제된 것을 다시 삭제해도 결과는 “없는 상태”로 같다
REST API 설계 문서에서 이 다섯 가지 구분이 빠지지 않는 이유가 있다. 재시도 전략이 메서드의 멱등성에 따라 달라지기 때문이다. 네트워크가 끊어져 응답을 못 받았을 때, GET·PUT·DELETE는 부담 없이 재시도할 수 있지만 POST는 중복 생성을 막을 방어가 필요하다. 클라이언트 SDK에서 “자동 재시도”를 구현할 때 먼저 체크하는 게 바로 이 멱등성이다.
OPTIONS, HEAD, CONNECT, TRACE 같은 메서드도 있지만 실무에서 자주 손대는 건 위 다섯 가지다. OPTIONS는 CORS(Cross-Origin Resource Sharing — 다른 도메인 자원 접근 정책) 사전 요청에 쓰이니 프론트엔드를 만든다면 한 번쯤 만난다.
상태 코드 — 결과를 세 자리로 말한다
응답의 첫 줄에 붙는 상태 코드는 세 자리 숫자다. 첫 자리가 그룹을 정한다. 이 그룹만 외워두면 로그를 읽는 속도가 확 빨라진다.
| 그룹 | 의미 | 대표 코드 |
|---|---|---|
| 1xx 정보 | 중간 상태. “계속해라” | 100 Continue, 101 Switching Protocols |
| 2xx 성공 | 요청이 정상 처리됨 | 200 OK, 201 Created, 204 No Content |
| 3xx 리다이렉트 | 다른 곳을 보라 | 301 Moved Permanently, 302 Found, 304 Not Modified |
| 4xx 클라이언트 오류 | 요청이 잘못됐다 | 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 429 Too Many Requests |
| 5xx 서버 오류 | 서버가 요청을 처리 못 했다 | 500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable, 504 Gateway Timeout |
실무에서 가장 자주 혼동되는 게 401과 403의 차이다. 401은 “누구인지 모르니 인증하라”, 403은 “누구인지는 알지만 권한이 없다”이다. 또 하나 자주 틀리는 게 502와 504. 502는 게이트웨이가 이상한 응답을 받았다는 뜻이고, 504는 응답 자체가 오지 않았다는 뜻이다. 원인 진단이 완전히 다르다.
304는 클라이언트의 조건부 요청에 대한 응답이다. 브라우저가 캐시된 자원의 ETag나 Last-Modified를 서버에 보내면, 서버가 “변한 게 없다”고 판단해 본문 없이 304만 돌려준다. 바이트를 아끼는 캐시 최적화의 핵심이다.
헤더 — 본문 밖에서 오가는 메타정보
본문은 내용물이고, 헤더는 그 내용물을 어떻게 다룰지에 대한 협상이다. 헤더만 잘 다뤄도 성능과 보안을 꽤 끌어올릴 수 있다. 실무에서 자주 만나는 네 부류를 보자.
Content-Type — 본문의 타입
본문이 JSON인지, HTML인지, 바이너리인지 알려준다. 없거나 틀리면 서버/브라우저가 본문 해석에 실패한다.
Content-Type: application/json; charset=utf-8
Content-Type: text/html; charset=utf-8
Content-Type: multipart/form-data; boundary=----xyz123
멀티파트는 파일 업로드에 쓰이고, boundary로 각 파트를 구분한다.
Authorization — 누구냐 너
API 요청에 인증 정보를 실어 보낸다. 가장 흔한 두 방식이 이렇다.
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Authorization: Basic dXNlcjpwYXNz
Bearer는 JWT 같은 토큰을 쓸 때, Basic은 ID:비밀번호를 Base64로 인코딩한 구식 방식이다. Basic은 인코딩일 뿐 암호화가 아니라, 평문 HTTP에 실어 보내면 그대로 노출된다. 지금 시대에 Basic을 쓴다면 반드시 HTTPS 위에서만 쓴다.
Cookie / Set-Cookie — 상태를 유지하는 방법
HTTP는 원래 상태가 없다(stateless). 매 요청이 독립적이다. 그래서 로그인 같은 “지속되는 상태”를 표현하려면 별도 장치가 필요한데, 그게 쿠키다.
# 서버가 응답에 세팅
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Lax; Max-Age=3600
# 다음부터 클라이언트가 요청에 실어 보냄
Cookie: session=abc123
HttpOnly는 자바스크립트에서 document.cookie로 읽지 못하게 막고(XSS 방어), Secure는 HTTPS에서만 보내게 하고, SameSite는 크로스 사이트 요청에서 쿠키 전송을 제한(CSRF 방어)한다. 이 세 가지는 요즘 사실상 기본 세팅이다.
Cache-Control — 어디에 얼마나 저장할 것인가
같은 자원을 반복해서 서버에서 받아올 필요는 없다. 브라우저 캐시, CDN 캐시, 프록시 캐시가 중간에서 응답을 저장했다 재사용한다. 그 규칙을 서버가 지시하는 게 Cache-Control이다.
Cache-Control: public, max-age=31536000, immutable # 해시된 정적 자원
Cache-Control: private, max-age=60 # 사용자별, 1분
Cache-Control: no-store # 절대 저장 금지
public은 중간 캐시가 저장 가능, private은 브라우저만 저장 가능이다. max-age는 초 단위. immutable은 “이 자원은 절대 안 바뀐다”는 약속인데, 해시가 붙은 파일명(app.3f2b9e.js)과 궁합이 좋다. 캐시 전략만 제대로 잡아도 트래픽 비용을 몇 배로 줄일 수 있다.
HTTP/1.1 — 오랜 기본값의 한계
브라우저와 서버의 대부분은 오랫동안 HTTP/1.1로 대화해왔다. 1997년에 나온 이 버전은 TCP 연결을 재사용하는 keep-alive와 여러 요청을 연속으로 보내는 파이프라이닝을 도입해 0.9/1.0 시절의 성능 문제를 많이 고쳤다.
그런데 한 가지 근본적인 제약이 남았다. 하나의 TCP 연결 위에서는 요청이 순서대로 처리된다. 앞 요청이 늦어지면 뒤 요청들도 같이 밀린다. 이걸 HOL(Head-of-Line) 블로킹이라 부른다.
sequenceDiagram
participant B as 브라우저
participant S as 서버
B->>S: GET /a.js
B->>S: GET /b.css
B->>S: GET /c.png
S-->>B: a.js 응답 (느림)
Note over B,S: b.css, c.png는 a.js 끝날 때까지 대기
S-->>B: b.css 응답
S-->>B: c.png 응답
브라우저들은 이 문제를 연결을 6개씩 병렬로 여는 꼼수로 회피했다. 그래서 2010년대 초반 프론트엔드 최적화 글을 보면 “이미지 스프라이트로 묶어라”, “CSS를 합쳐라” 같은 조언이 많았다. 요청 수 자체를 줄여야 1.1의 한계를 피할 수 있었기 때문이다.
HTTP/2 — 멀티플렉싱의 등장
2015년에 표준화된 HTTP/2는 구글의 SPDY 프로토콜을 바탕으로, HTTP/1.1의 성능 한계를 정면으로 푼다. 가장 큰 변화는 멀티플렉싱(multiplexing)이다.
하나의 TCP 연결 위에 여러 스트림을 동시에 올린다. 요청과 응답 둘 다 작은 프레임으로 쪼개져 인터리빙된다. 앞선 요청이 느려도 뒷 요청의 응답이 먼저 도착할 수 있다.
flowchart LR
subgraph C[단일 TCP/TLS 연결]
direction LR
S1["Stream 1<br/>GET /a.js"] --- S2["Stream 2<br/>GET /b.css"]
S2 --- S3["Stream 3<br/>GET /c.png"]
end
C --> SRV[서버]
SRV -->|프레임 인터리빙| C
그 밖에 HTTP/2가 가져온 것들이 있다.
- 헤더 압축(HPACK): 반복되는 헤더(대부분의 헤더가 이렇다)를 압축해서 전송량을 줄인다
- 서버 푸시: 서버가 클라이언트가 요청하기 전에 자원을 미리 보낸다 (최근엔 효용 논란으로 사용 감소)
- 바이너리 프레이밍: 텍스트 기반이 아니라 바이너리로 파싱이 단순해짐
HTTP/2 덕분에 “이미지 스프라이트, 파일 합치기” 같은 예전 최적화가 오히려 손해가 되기도 한다. 많은 작은 파일이 병렬로 날아오는 게 더 빠른 경우가 많기 때문이다. 시대가 바뀌면 최적화 전략도 바뀐다.
HTTP/3 — 아예 UDP 위로
HTTP/2가 남긴 문제가 하나 있다. 여러 스트림이 하나의 TCP 연결에 얹혀 있으니, TCP 레벨에서 패킷 하나가 손실되면 모든 스트림이 같이 멈춘다. TCP 자체의 HOL 블로킹이다. 멀티플렉싱으로 HTTP 계층의 HOL은 풀었지만, 전송 계층에서 다시 병목이 생긴 셈이다.
HTTP/3는 이걸 근본부터 뜯어고쳤다. TCP 대신 UDP 위에 QUIC이라는 프로토콜을 올리고, 그 위에서 HTTP를 구현한다. QUIC(Quick UDP Internet Connections — 구글이 시작해 IETF가 표준화한 전송 프로토콜)은 스트림을 전송 계층에서 직접 다룬다. 한 스트림의 패킷 손실이 다른 스트림에 영향을 주지 않는다.
flowchart TB
subgraph H3["HTTP/3"]
HTTP3[HTTP 의미론] --> QUIC[QUIC]
QUIC --> TLS13[TLS 1.3 내장]
TLS13 --> UDP[UDP]
end
subgraph H2["HTTP/2"]
HTTP2[HTTP 의미론] --> HF[프레이밍 레이어]
HF --> TLS[TLS 1.2/1.3]
TLS --> TCP[TCP]
end
또 한 가지 장점이 연결 수립 속도다. TCP+TLS는 최소 2RTT(Round Trip Time — 패킷이 왕복하는 시간)가 필요하지만, QUIC은 첫 연결 1RTT, 재연결은 0-RTT가 가능하다. 모바일처럼 네트워크가 자주 바뀌는 환경에서 이 차이가 크게 느껴진다. Cloudflare, Google, Meta 같은 곳의 주요 서비스는 이미 HTTP/3가 기본값이다.
HTTP에는 구멍이 있다
여기까지가 HTTP의 이야기다. 그런데 HTTP의 메시지는 기본적으로 평문이다. 같은 네트워크에 있는 누군가가 tcpdump나 Wireshark를 돌리면 요청 본문부터 헤더의 Authorization 토큰까지 전부 읽힌다. 공공 와이파이에서 로그인 정보를 HTTP로 보내는 게 왜 위험한지 여기에 있다.
위험은 세 가지다.
- 도청(eavesdropping): 제3자가 통신 내용을 훔쳐본다
- 변조(tampering): 중간에서 메시지를 바꿔치기한다
- 위장(impersonation): 엉뚱한 서버가 진짜인 척한다
HTTPS는 이 세 가지를 한꺼번에 방어한다. HTTPS는 별도의 프로토콜이 아니라 HTTP를 TLS(Transport Layer Security — 두 호스트 사이에 암호화된 채널을 만드는 프로토콜. SSL의 후계자) 위에 얹은 조합이다.
flowchart LR
C[브라우저] -->|TLS 암호화 터널| S[서버]
EVE["중간자<br/>(도청 시도)"] -.->|"보이는 건 암호문뿐"| C
EVE -.->|"보이는 건 암호문뿐"| S
TLS는 암호화로 도청을 막고, 메시지 인증 코드로 변조를 막고, 디지털 인증서로 서버의 신원을 확인해준다. 브라우저 주소창의 자물쇠 아이콘은 이 세 가지가 모두 통과됐다는 표시다.
왜 이제는 HTTPS가 기본값인가
2010년대 중반까지만 해도 “민감한 페이지만 HTTPS”가 일반적인 패턴이었다. 로그인, 결제는 HTTPS, 나머지는 HTTP. 지금은 모든 페이지가 HTTPS가 기본이다. 이유가 몇 가지 쌓였다.
- Let’s Encrypt의 등장(2016): 무료 자동화 인증서. 비용 장벽이 사라졌다
- 브라우저의 경고: Chrome은 2018년부터 HTTP 사이트에 “Not Secure” 표시를 띄운다
- HTTP/2, HTTP/3가 사실상 TLS 필수: 주요 브라우저는 HTTP/2를 TLS 없이 지원하지 않는다
- 프라이버시 법규: GDPR 같은 규제가 통신 암호화를 사실상 요구한다
HTTPS는 이제 보안이 필요한 곳에만 쓰는 옵션이 아니라, 웹의 기본 설정이다. 평문 HTTP를 쓰는 것 자체가 비정상으로 보이는 시대가 됐다.
그래서 자연스러운 다음 질문은 이것이다. TLS는 어떻게 작동하길래 공개된 네트워크 위에서 암호화된 채널을 만드는가? 인증서는 누가 발급하고, 브라우저는 왜 그 인증서를 믿는가? 이 질문에 답하는 게 다음 편이다.
다음 편에서는 TLS/SSL의 내부를 들여다본다. 대칭 키와 비대칭 키의 차이, 인증서와 인증기관(CA)의 역할, ClientHello부터 Finished까지의 핸드셰이크 과정, 그리고 TLS 1.2와 1.3의 결정적 차이까지 하나씩 풀어간다.

Loading comments...