Skip to content

필사 모드: HTTP 세맨틱 완전 해설 — 메서드, 상태 코드, 쿠키, CORS, CSP, HSTS, SameSite 끝장 가이드 (2025)

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

들어가며 — HTTP는 "단순한 텍스트 프로토콜"이 아니다

GET /api/users/42 HTTP/1.1

Host: api.example.com

이 네 줄이 HTTP의 모든 것인 것처럼 보이지만, 현대 웹은 수십 개의 헤더와 세맨틱이 얽힌 복잡한 협약 시스템이다. `POST`와 `PUT`의 차이, `200`과 `204`의 차이, `302`와 `307`의 차이, `Cache-Control`의 10가지 지시어, `Content-Type`과 `Accept`의 협상 규칙, `Cookie`의 6가지 속성, CORS의 preflight/credentials 규칙, CSP/HSTS/Referrer-Policy/Permissions-Policy 같은 보안 헤더들...

**하나라도 잘못 이해하면 버그 또는 보안 사고**로 이어진다. 2020년 이후 SameSite=Lax 기본값 전환으로 전 세계 광고/분석 도구가 대거 부서졌고, CORS preflight 한 번에 10,000ms 왕복이 생기면 모바일 로딩이 치명적으로 느려진다. CSP `unsafe-inline` 하나를 방심하면 XSS가 들어오고, HSTS preload 목록에 올려놓고 도메인을 잃어버리면 **복구가 수년** 걸린다.

이 글은 HTTP 프로토콜의 **세맨틱 레이어**를 처음부터 해부한다. 전송(TLS, HTTP/3)은 다른 글에서 다뤘으므로 여기서는 "RFC 9110 HTTP Semantics"와 관련 보안 표준을 중심으로 본다. 매일 쓰는 API, 매일 보는 브라우저 에러, 매일 읽는 로그의 "왜"를 명쾌하게 설명하는 게 목표다.

1. HTTP 버전 — 세맨틱은 공통, 전송만 달라졌다

2022년 RFC 9110 "HTTP Semantics"가 HTTP/1.1, HTTP/2, HTTP/3의 **공통 세맨틱**을 분리해 정의했다.

- **전송(framing)**: HTTP/1.1 (텍스트, RFC 9112), HTTP/2 (바이너리 + 멀티플렉싱, RFC 9113), HTTP/3 (QUIC 기반, RFC 9114)

- **세맨틱**: 메서드, 상태, 헤더, 콘텐츠 협상, 인증 등 (RFC 9110, 9111 cache)

즉 `GET /foo HTTP/1.1`과 `GET /foo` (HTTP/2 HEADERS 프레임) + `GET /foo` (HTTP/3 QPACK)은 **전송 방식만 다를 뿐 의미는 같다**. 이 글은 세맨틱에 집중.

2. 메서드 — 9개지만 실전은 6개

2.1 RFC 9110 표준 메서드

| 메서드 | Safe | Idempotent | Cacheable | 대표 용도 |

| --------- | ----- | ---------- | --------- | --------------------- |

| `GET` | ✅ | ✅ | ✅ | 리소스 조회 |

| `HEAD` | ✅ | ✅ | ✅ | GET의 헤더만 |

| `POST` | ❌ | ❌ | 조건부 | 생성/임의 액션 |

| `PUT` | ❌ | ✅ | ❌ | 전체 교체 |

| `DELETE` | ❌ | ✅ | ❌ | 삭제 |

| `PATCH` | ❌ | ❌ | ❌ | 부분 수정 (RFC 5789) |

| `OPTIONS` | ✅ | ✅ | ❌ | 지원 메서드 조회 / CORS preflight |

| `CONNECT` | ❌ | ❌ | ❌ | 터널 (HTTPS proxy) |

| `TRACE` | ✅ | ✅ | ❌ | 디버그 에코 (보안상 대부분 비활성화) |

2.2 Safe의 의미

**Safe = 서버의 상태를 바꾸지 않는다**. GET으로 "좋아요"를 올리게 만들면 **prefetch, link preview, crawler**가 의도치 않게 호출해 상태가 바뀐다. 2005~2006년 Google Web Accelerator가 GET 링크를 prefetch해서 "Delete"가 불릴 수 있다는 게 큰 이슈였다.

**규칙**: 상태 변경은 반드시 POST/PUT/DELETE/PATCH.

2.3 Idempotent의 의미

**같은 요청을 여러 번 보내도 서버 상태가 한 번 보낸 것과 같음**.

- PUT `/users/42` (body: `{name: "Alice"}`) 여러 번 → 최종 상태 동일

- POST `/users` (body: `{name: "Alice"}`) 여러 번 → 사용자 여러 명 생성!

- DELETE `/users/42` 여러 번 → 첫 번째는 200, 이후는 404 (상태는 "존재하지 않음"으로 동일)

**네트워크 재시도와의 관계**: Idempotent 메서드는 **클라이언트/프록시가 안전하게 재시도** 가능. POST는 안 된다. 실무에서 POST 재시도를 원하면 **idempotency key** 헤더로 서버가 중복 제거하는 패턴을 쓴다(Stripe, AWS 등).

2.4 PUT vs PATCH — 혼동 1위

- **PUT**: "리소스 전체 교체". body가 완전한 표현

- **PATCH**: "부분 수정". body가 변경사항(diff, JSON Patch, JSON Merge Patch)

PUT 시 빠진 필드는 `null` 또는 default가 된다(또는 서버가 거절). PATCH는 **포함된 필드만** 변경. 실무에서 대부분은 PATCH가 맞다.

2.5 PATCH의 body 포맷

- **JSON Merge Patch** (RFC 7396): 단순. `{name: "Bob"}`

- **JSON Patch** (RFC 6902): 배열 조작, 이동, 조건부 — 복잡하지만 강력

[{ "op": "replace", "path": "/name", "value": "Bob" }]

대부분의 API가 Merge Patch를 쓰지만 배열 중 일부만 바꾸는 경우 Patch가 낫다.

3. 상태 코드 — 10개만 알면 80% 커버

3.1 1xx — Informational (거의 안 쓰임)

- `100 Continue`: 큰 body 전송 전 "보내도 돼?" 확인

- `101 Switching Protocols`: WebSocket 업그레이드

- `103 Early Hints` (RFC 8297): 최종 응답 전 preload 힌트 — HTTP/2+에서 성능 개선용

3.2 2xx — Success

- `200 OK`: 기본

- `201 Created`: POST로 새 리소스 생성 → `Location` 헤더에 URL

- `202 Accepted`: 비동기 작업 수락 (아직 미완료)

- `204 No Content`: 성공, 응답 body 없음 (DELETE, 빈 PUT 결과)

- `206 Partial Content`: Range 요청 응답

3.3 3xx — Redirection 4형제 (가장 혼동)

| 코드 | 메서드 유지 | 영구성 | 의미 |

| ----- | --------------- | ----------- | ----------------------------------- |

| `301` | **아니요** (대부분 GET으로) | 영구 | Moved Permanently. 검색엔진에 "도메인 옮김" |

| `302` | **아니요** (대부분 GET으로) | 일시 | Found. 과거 "Found"의 모호한 이름 |

| `303` | **아니요** (무조건 GET) | 일시 | See Other. POST 후 GET 패턴(PRG) |

| `307` | **예** | 일시 | Temporary Redirect. 메서드 그대로 |

| `308` | **예** | 영구 | Permanent Redirect. 메서드 그대로 |

**실전 선택**:

- POST form → 성공 → `303` + `Location: /success`: 새로고침해도 POST 재전송 안 됨

- HTTPS 강제 리디렉션: `301` 또는 `308` (HSTS로 대체 권장)

- 로드 밸런서의 일시적 다른 호스트 안내: `307`

3.4 4xx — Client Error

- `400 Bad Request`: 요청 자체가 malformed

- `401 Unauthorized`: 인증 필요 / 실패 (이름이 틀림 — 사실 "Unauthenticated"가 맞음)

- `403 Forbidden`: 인증됐으나 권한 없음

- `404 Not Found`: 리소스 없음 또는 존재를 감추고 싶을 때

- `405 Method Not Allowed`: GET만 지원하는 리소스에 POST 등 → `Allow` 헤더 필수

- `406 Not Acceptable`: `Accept` 헤더와 맞는 표현 없음

- `409 Conflict`: 낙관적 잠금 실패, 중복 등

- `410 Gone`: 의도적으로 영구 삭제. `404`보다 명확

- `412 Precondition Failed`: `If-Match` 등 조건부 요청 실패

- `413 Payload Too Large`

- `415 Unsupported Media Type`: `Content-Type` 서버가 모름

- `418 I'm a teapot`: 농담. RFC 2324 만우절 → 실제로 쓰는 서비스도 있음 (Google)

- `422 Unprocessable Entity`: 문법 OK, 의미론적 오류 (validation 실패)

- `429 Too Many Requests`: Rate limit → `Retry-After` 헤더

3.5 5xx — Server Error

- `500 Internal Server Error`: 일반 서버 에러

- `502 Bad Gateway`: upstream(백엔드) 응답 이상 → 대부분 "백엔드 죽음"

- `503 Service Unavailable`: 과부하/점검. `Retry-After` 권장

- `504 Gateway Timeout`: upstream 응답 없음

3.6 401 vs 403 — 결정 트리

로그인 안 됨 → 401 Unauthorized (이름만 Unauthorized, 의미는 Unauthenticated)

로그인 됐는데 권한 없음 → 403 Forbidden

존재를 숨기고 싶음 → 404 Not Found

"비밀 리소스"는 존재 여부 자체가 정보가 된다(예: 사설 지라 티켓). 이 경우 404가 보안상 더 안전.

4. 헤더 — 주요 카테고리

4.1 Content Negotiation

클라이언트가 선호를 표현 → 서버가 선택.

Accept: application/json, text/html;q=0.9

Accept-Language: ko-KR, ko;q=0.9, en;q=0.5

Accept-Encoding: br, gzip

`q=` (quality, 0.0~1.0)로 우선순위. 기본은 1.0. 서버는 `Content-Type`, `Content-Language`, `Content-Encoding`으로 응답 + `Vary` 헤더로 "이 응답은 이 헤더에 의존함" 캐시 힌트.

4.2 Range Requests

GET /video.mp4 HTTP/1.1

Range: bytes=0-999

HTTP/1.1 206 Partial Content

Content-Range: bytes 0-999/1048576

Content-Length: 1000

비디오 스트리밍, 다운로드 재개에 필수. `Accept-Ranges: bytes` 헤더로 서버가 지원 여부 표시.

4.3 Conditional Requests

GET /article/42 HTTP/1.1

If-None-Match: "abc123"

If-Modified-Since: Mon, 15 Apr 2026 10:00:00 GMT

HTTP/1.1 304 Not Modified

`ETag` 또는 `Last-Modified` 기반. 응답 body 전송 절약.

- **Strong ETag**: `"abc123"` — byte-exact 매칭

- **Weak ETag**: `W/"abc123"` — 의미론적 매칭 (압축 방식 달라도 OK)

4.4 Compression

- `gzip`: 20년 표준

- `br` (Brotli): Google 2015, 15~25% 더 작음. 현재 거의 모든 브라우저 지원

- `zstd`: 2024년 RFC 8478 → 일부 브라우저가 지원 시작

원문 대비 40~80% 압축. CDN 대부분 자동. 동적 vs 사전 압축 구분.

4.5 보안 관련은 별도 섹션에서 집중 다룸 (6~10절)

5. 캐시 — `Cache-Control` 완벽 해부

5.1 응답 헤더

- `no-store`: 절대 저장 금지 (개인정보)

- `no-cache`: 저장은 OK, **사용 전 반드시 revalidate**

- `private`: 공용 캐시(프록시/CDN) 금지, 브라우저만

- `public`: 모든 캐시 가능

- `max-age=<초>`: 신선함 기간

- `s-maxage=<초>`: 공용 캐시용 (max-age 오버라이드)

- `must-revalidate`: stale이면 반드시 origin 확인

- `stale-while-revalidate=<초>`: stale 응답 즉시 반환 + 백그라운드 갱신

- `stale-if-error=<초>`: origin 장애 시 stale 허용

- `immutable`: URL이 바뀌면 콘텐츠도 바뀜(hash URL) — 절대 revalidate 안 함

5.2 요청 헤더 (드물게)

- `Cache-Control: no-cache`: "fresh 응답 받고 싶어" (Ctrl+F5)

- `Cache-Control: max-age=0`: 만료 강제

5.3 실전 템플릿

**정적 자산 (hash URL)**:

Cache-Control: public, max-age=31536000, immutable

**HTML (frequently changing)**:

Cache-Control: no-cache

**API 응답 (개인 데이터)**:

Cache-Control: private, max-age=60

**CDN 공유 + 브라우저 짧게**:

Cache-Control: public, max-age=60, s-maxage=3600

5.4 Vary

`Vary: Accept-Encoding, Accept-Language` → 캐시가 이 헤더들 조합별로 다른 엔트리를 유지. 잘못 쓰면 캐시 히트율 폭락.

6. 쿠키 — 복잡한 상태의 왕

6.1 기본 구조

Set-Cookie: session=abc123; Path=/; Max-Age=3600; Secure; HttpOnly; SameSite=Lax

6.2 속성

- **`Domain=`**: 공유할 도메인. `.example.com` = 서브도메인 포함. 명시 안 하면 발급한 도메인만.

- **`Path=`**: 특정 경로 하위만

- **`Max-Age=<초>`**: 만료 (절대값은 `Expires=`)

- **`Secure`**: HTTPS에서만 전송

- **`HttpOnly`**: JS에서 `document.cookie` 접근 불가 (XSS 방어)

- **`SameSite=Lax|Strict|None`**: CSRF/트래킹 방어 핵심

- **`Partitioned`**: CHIPS — 제3자 쿠키의 per-top-site 저장

6.3 SameSite — 2020년 이후 최대 변화

- **`Strict`**: 외부 사이트에서 온 어떤 요청에도 쿠키 안 보냄. 외부 링크로 로그인 상태 사라짐.

- **`Lax` (현재 기본값)**: Top-level GET 탐색은 허용, 그 외 크로스사이트 요청에는 쿠키 차단

- **`None`**: 모든 크로스사이트 요청에 쿠키 보냄. **반드시 `Secure` 함께** (브라우저가 강제)

**중요한 예시**:

사용자가 evil.com에서 <form action="bank.com/transfer">를 submit → POST

- SameSite=Strict: 쿠키 안 보냄 → 로그인 상태 없음 → 요청 거부

- SameSite=Lax: POST는 cross-site → 쿠키 안 보냄 → 거부 ✅

- SameSite=None: 쿠키 보냄 → CSRF 성공 ❌

2020년 Chrome 80부터 명시 안 한 쿠키는 **Lax 기본**. iframe, 광고, 분석 스크립트가 대거 깨지면서 혼란.

6.4 Third-party cookie의 종말

2024년 Chrome은 3rd-party cookie 단계적 폐지 시작. 2025년 전면 제한 예정.

- **광고/분석이 의존해온 모델** 붕괴

- 대안: Google의 Privacy Sandbox (Topics API, FLEDGE, CHIPS)

- CHIPS(`Partitioned`): 3rd-party가 **top-level site별로 격리된 저장소** 사용

6.5 쿠키의 크기 제한

- RFC 6265 권장: 쿠키 하나 4096 byte, 도메인당 50개, 총 3000개

- 현실: 브라우저마다 조금 다름 → **토큰을 쿠키에 저장할 땐 JWT 길이 주의**. HTTP/2 헤더 압축(HPACK)이 있어도 초기 요청에서 쿠키가 TCP segment 여러 개 차지 가능

6.6 Prefix

- `__Secure-`: Secure 속성 필수

- `__Host-`: Secure + Path=/ + Domain 없음 필수. 서브도메인 takeover 방어

Set-Cookie: __Host-session=abc; Secure; Path=/; SameSite=Lax

7. CORS — 크로스오리진의 미스터리

7.1 Same-Origin Policy

브라우저 보안의 근간: `https://a.com`의 JS는 `https://b.com`의 응답을 읽을 수 없다. "origin" = scheme + host + port.

7.2 Simple Request — preflight 없음

- 메서드: GET, HEAD, POST

- Content-Type: `application/x-www-form-urlencoded`, `multipart/form-data`, `text/plain`만

- 커스텀 헤더 없음

서버가 `Access-Control-Allow-Origin: *` 또는 `Access-Control-Allow-Origin: https://a.com` 반환 시 브라우저가 JS에 응답 공개.

7.3 Preflight — OPTIONS 왕복

비-simple 요청은 **본 요청 전에 OPTIONS로 preflight**:

OPTIONS /api/users HTTP/1.1

Origin: https://a.com

Access-Control-Request-Method: PUT

Access-Control-Request-Headers: Content-Type, Authorization

HTTP/1.1 200 OK

Access-Control-Allow-Origin: https://a.com

Access-Control-Allow-Methods: GET, POST, PUT, DELETE

Access-Control-Allow-Headers: Content-Type, Authorization

Access-Control-Max-Age: 86400

`Max-Age`로 preflight 결과를 캐시. 0~24시간이 일반적(Chromium은 2시간 상한).

7.4 Credentials (쿠키, Auth 헤더)

브라우저의 `fetch(url, { credentials: 'include' })` 시:

- 서버: `Access-Control-Allow-Credentials: true`

- 서버: `Access-Control-Allow-Origin`에 **`*` 불가**, 구체 origin 지정 필수

7.5 흔한 삽질

- **`*`은 credentials와 양립 불가**: 서버에서 `ACAO: *` + `ACAC: true` 하면 브라우저 거부

- **Preflight 매번 날아감**: `Max-Age` 설정하자

- **Authorization 헤더**: `Access-Control-Allow-Headers`에 명시 필요

- **Exposed headers**: JS에서 읽으려면 `Access-Control-Expose-Headers`에 나열

7.6 CORS ≠ 보안

CORS는 브라우저 내에서만 강제. **서버 간 요청, curl, Postman은 CORS 무시**. 실제 인증/권한은 서버가 직접 체크해야.

8. CSP — XSS를 근본적으로 막는 헤더

8.1 기본 구조

Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; img-src 'self' data:; style-src 'self' 'unsafe-inline'

브라우저가 허용된 소스 외의 스크립트/리소스 로드/실행 차단.

8.2 주요 디렉티브

- `default-src`: 기본

- `script-src`, `style-src`, `img-src`, `font-src`, `connect-src`, `frame-src`, `media-src`

- `object-src 'none'`: Flash/plugin 차단 (권장 기본)

- `base-uri 'self'`: `<base>` 태그 남용 방어

- `frame-ancestors`: `X-Frame-Options`의 후계 (clickjacking)

- `form-action`: form submit 대상 제한

- `upgrade-insecure-requests`: HTTP를 자동 HTTPS로

8.3 `unsafe-inline` — 위험한 익숙함

대부분의 legacy 사이트가 `<script>` 인라인 코드를 쓴다. CSP 적용 시 막혀서 `'unsafe-inline'` 추가하게 됨. **하지만 이러면 XSS 보호가 사실상 사라진다**.

8.4 nonce vs hash vs strict-dynamic

**Nonce** (난수):

Content-Security-Policy: script-src 'nonce-rAnDom123'

요청마다 랜덤 생성, 매번 HTML에 삽입. 서버 렌더링 앱에 적합.

**Hash**:

Content-Security-Policy: script-src 'sha256-BASE64...'

정적 스크립트의 SHA-256으로 허용. 내용 바뀌면 재계산.

**strict-dynamic**:

Content-Security-Policy: script-src 'nonce-X' 'strict-dynamic'

nonce 있는 스크립트가 `document.createElement('script')`로 더 많은 스크립트를 로드하는 것도 신뢰. allow-list 없이 운영.

8.5 Reporting

Content-Security-Policy-Report-Only: default-src 'self'; report-to csp-endpoint

Report-To: {"group":"csp-endpoint","max_age":31536000,"endpoints":[{"url":"/csp-report"}]}

위반 시 실제 차단 없이 JSON 리포트만. 프로덕션 도입 전 dry-run 필수.

8.6 Google의 strict CSP

Google은 모든 서비스에 다음과 유사한 strict CSP 적용:

Content-Security-Policy: script-src 'nonce-X' 'strict-dynamic'; object-src 'none'; base-uri 'self'; require-trusted-types-for 'script'

9. HSTS — HTTP 강제 차단

9.1 기본

Strict-Transport-Security: max-age=31536000; includeSubDomains

브라우저가 이 헤더를 본 후, 지정 기간 동안 **HTTP 요청을 자동으로 HTTPS로 변환**. MITM으로 HTTP 다운그레이드 공격 방지.

9.2 includeSubDomains

`example.com`에 설정 → 모든 서브도메인(`api.example.com`, `www.example.com`)에 적용. 하나의 서브도메인이라도 HTTPS 없으면 접근 불가.

9.3 Preload — 브라우저 내장 목록

Strict-Transport-Security: max-age=63072000; includeSubDomains; preload

hstspreload.org에 제출 후 심사 통과 → Chrome/Firefox/Safari에 내장. **첫 방문 전부터 HTTPS 강제**.

**위험**: 한번 preload에 올라가면 내리기 **매우 어렵다**(브라우저 업데이트 주기). 서브도메인 하나가 HTTPS 인증서 갱신 실패로 만료되면 전체 서비스 접근 불가. 테스트/스테이징 도메인에 preload 쓰지 말 것.

10. Referrer-Policy — 개인정보 누출 방지

10.1 왜 위험한가

기본적으로 브라우저가 `Referer: https://bank.com/account/123` 같은 URL을 외부 사이트에 보낸다. 사용자 경로, 검색어, 내부 ID가 유출.

10.2 정책 값

- `no-referrer`: 절대 안 보냄

- `no-referrer-when-downgrade` (과거 기본값): HTTPS→HTTP일 때만 안 보냄

- `origin`: origin만 (`https://bank.com/`)

- `origin-when-cross-origin`: 동일 origin엔 전체, 외부엔 origin만

- `same-origin`: 동일 origin에만 보냄

- `strict-origin`: origin만, downgrade 시 안 보냄

- `strict-origin-when-cross-origin` (**현재 기본값**, 2020년~): 동일 origin 전체, 외부 origin만, downgrade 안 보냄

- `unsafe-url`: 전부 보냄 (권장 안 함)

11. 추가 보안 헤더

11.1 X-Frame-Options → CSP frame-ancestors

X-Frame-Options: DENY

또는 (legacy)

X-Frame-Options: SAMEORIGIN

현대:

Content-Security-Policy: frame-ancestors 'none'

Clickjacking 방어.

11.2 X-Content-Type-Options

X-Content-Type-Options: nosniff

브라우저의 content type sniffing 비활성화. `Content-Type: text/plain`으로 보낸 파일을 브라우저가 "HTML 같은데?"로 해석해 XSS 당하는 걸 막음.

11.3 Permissions-Policy (구 Feature-Policy)

Permissions-Policy: geolocation=(), camera=(), microphone=()

브라우저 API 사용 제한. iframe에도 상속.

11.4 Cross-Origin-* 헤더

- `Cross-Origin-Opener-Policy: same-origin`: 새 탭이 `window.opener` 접근 차단

- `Cross-Origin-Embedder-Policy: require-corp`: 교차 출처 리소스는 `Cross-Origin-Resource-Policy` 헤더 있어야만 embed

- `Cross-Origin-Resource-Policy: same-origin`: 이 리소스가 다른 origin에서 embed되는 걸 차단

**SharedArrayBuffer, high-resolution timer** 같은 강력 기능 쓰려면 COOP+COEP 필수(Spectre 방어).

11.5 Timing-Allow-Origin

`Resource Timing API`로 JS가 network timing 접근 시 CORS처럼 origin 체크.

12. 인증 — Basic, Bearer, Digest, Session

12.1 Basic

Authorization: Basic dXNlcjpwYXNzd29yZA==

`user:password`를 base64 인코딩. **TLS 없으면 평문 수준**. 내부 관리 페이지 외 실전 사용 지양.

12.2 Bearer (OAuth2, JWT)

Authorization: Bearer eyJhbGc...

"이 토큰 가진 사람은 누구든 접근" — 대부분의 현대 API. 토큰 유출 시 즉시 취소 가능해야 함.

12.3 Digest

RFC 7616. challenge-response + nonce로 평문 전송 없이 인증. 복잡해서 거의 안 쓰임(TLS가 이겼다).

12.4 Session (Cookie 기반)

서버가 세션 ID를 쿠키로 발급. 서버는 세션 스토어(Redis, DB)에 상태 보관. 전통적이고 강력. JWT의 단점(취소 어려움)을 해결.

12.5 JWT vs Session — 2025년의 합의

- **세션**: 서버 상태 필요, 즉시 취소 가능, 중앙 인증 불리

- **JWT**: stateless, 분산 친화, 짧은 유효기간 + refresh token 패턴 권장

**혼합**: Access token은 JWT, Refresh token은 DB 저장 → 장점 결합.

13. WebSocket 업그레이드

GET /chat HTTP/1.1

Upgrade: websocket

Connection: Upgrade

Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==

Sec-WebSocket-Version: 13

HTTP/1.1 101 Switching Protocols

Upgrade: websocket

Connection: Upgrade

Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

이후 같은 TCP 연결이 WebSocket 프레임으로 전환. HTTP/2/3에서는 다른 negotiation 메커니즘.

14. 에러와 재시도 — Retry-After, 429

14.1 429 Too Many Requests

HTTP/1.1 429 Too Many Requests

Retry-After: 60

또는

Retry-After: Wed, 15 Apr 2026 10:00:00 GMT

클라이언트는 `Retry-After`만큼 기다린 뒤 재시도. 무조건적 exponential backoff보다 서버 협조적.

14.2 503 + Retry-After

서비스 점검 시 `503` + `Retry-After`로 "5분 뒤에 와" 표시. 검색 엔진도 이해.

14.3 idempotency key

POST /payments HTTP/1.1

Idempotency-Key: d8f3a1b2-...

HTTP/1.1 200 OK ← 1차 성공

네트워크 끊김으로 다시 같은 키로 보내면 서버는 첫 응답을 반환(중복 결제 방지). Stripe, AWS가 대중화.

15. Content-Type — 오해의 근원

15.1 Charset

Content-Type: text/html; charset=utf-8

**UTF-8 명시 필수**. 생략 시 브라우저가 추측하다가 한글 깨짐. JSON은 RFC 8259에서 UTF-8 고정이라 `charset` 불필요.

15.2 형식 구분

- `application/json`: REST API 표준

- `application/x-www-form-urlencoded`: HTML form 기본

- `multipart/form-data`: 파일 업로드

- `text/html; charset=utf-8`: 웹 페이지

- `application/octet-stream`: 바이너리

- `application/pdf`, `image/png`, `video/mp4`: 미디어

- `application/problem+json` (RFC 7807): 에러 응답 표준

15.3 Boundary in multipart

Content-Type: multipart/form-data; boundary=----abcd1234

본문에서 `--abcd1234`로 각 파트 구분. 프레임워크가 자동 처리하지만 로그에서 보면 이해됨.

16. HTTP/2 / HTTP/3 세맨틱 차이점

16.1 헤더는 compressed

- HTTP/2: HPACK (정적 + 동적 테이블)

- HTTP/3: QPACK (유사, 순서 의존성 완화)

같은 Cookie 값이 반복 요청에서 1~2 byte로 압축. 첫 요청은 전체 전송이라 여전히 쿠키 크기 중요.

16.2 대소문자

HTTP/2+에선 **헤더 이름은 소문자 필수**. HTTP/1.1은 대소문자 무시지만 관용으로 `Content-Type` 스타일.

16.3 Trailers (HTTP/2+에서 유용)

응답 body 끝에 추가되는 헤더. gRPC가 status를 trailer로 보냄.

HTTP/1.1 200 OK

Transfer-Encoding: chunked

Trailer: Grpc-Status

...chunks...

Grpc-Status: 0

16.4 Server Push (HTTP/2) — 이미 deprecated

서버가 요청 없이 리소스 push. 실전 효율이 `103 Early Hints`보다 떨어져 Chrome이 2022년 제거. HTTP/3는 아예 스펙에서 뺌.

17. 실전 체크리스트

API 설계

- [ ] RESTful 메서드 의미 정확히 사용

- [ ] 상태 코드 적절한 범위 선택 (2xx/4xx/5xx)

- [ ] POST는 **생성 + 201 Created + Location**, 업데이트는 PUT/PATCH

- [ ] idempotency-key 헤더 지원 (결제 등 중요)

- [ ] 에러 응답 `application/problem+json` 고려

- [ ] 페이지네이션은 Link 헤더 또는 cursor

- [ ] Rate limit: 429 + X-RateLimit-* 헤더

캐싱

- [ ] 정적 자산 hash URL + `max-age=31536000, immutable`

- [ ] HTML은 `no-cache` (revalidate)

- [ ] API는 `private, max-age=short`

- [ ] `stale-while-revalidate` 적극 활용

- [ ] `Vary` 헤더 정확히 (너무 많으면 캐시 히트 폭락)

쿠키 & 인증

- [ ] HttpOnly + Secure + SameSite=Lax(또는 Strict) 기본

- [ ] `__Host-` / `__Secure-` prefix

- [ ] 크기 주의 (JWT 너무 크면 헤더 오버헤드)

- [ ] Logout = 서버 세션 무효화 + 쿠키 삭제 지시

보안 헤더

- [ ] CSP `script-src` nonce + strict-dynamic (점진 도입)

- [ ] HSTS `max-age=31536000; includeSubDomains`

- [ ] `X-Content-Type-Options: nosniff`

- [ ] `Referrer-Policy: strict-origin-when-cross-origin` (기본)

- [ ] `frame-ancestors 'none'` 또는 `'self'`

- [ ] Permissions-Policy로 불필요한 브라우저 API 차단

CORS

- [ ] 구체적 origin 허용 (credentials 있으면 `*` 불가)

- [ ] `Access-Control-Max-Age` 설정 (preflight 캐시)

- [ ] 필요한 헤더만 expose

- [ ] 서버 간 요청은 CORS 아닌 내부 인증 (mTLS 등)

18. 디버깅 도구

18.1 curl

$ curl -v https://api.example.com/users

$ curl -X POST -H 'Content-Type: application/json' -d '{"a":1}' ...

$ curl -I ... # HEAD

`-v`로 모든 헤더 표시. `--http3`, `--http2` 옵션도 지원.

18.2 httpie

사람이 읽기 쉬운 CLI:

$ http GET api.example.com/users token==abc

18.3 Browser DevTools — Network tab

- **Status**: 200/304/3xx/4xx/5xx

- **Response Headers / Request Headers**

- **Timing**: DNS, TLS, waiting, downloading

- **Priority**: HTTP/2 priority hint

- **Protocol**: h2, h3, http/1.1 구분

18.4 Security headers 스캐너

- securityheaders.com: 사이트 URL 입력 시 헤더 검증 + A~F 등급

- Mozilla Observatory: 더 엄격

18.5 CORS tester

Chrome DevTools Console:

fetch('https://api.example.com/data', { credentials: 'include' })

.then(r => console.log(r.status))

.catch(console.error)

에러 메시지에 어느 헤더가 빠졌는지 정확히 나옴.

마무리 — HTTP는 계속 진화한다

2025년의 HTTP는 1996년의 HTTP/1.0과 **세맨틱은 비슷하지만 운영 복잡도는 수십 배**다. 메서드와 상태 코드는 그대로지만, 보안 헤더만 해도 CSP/HSTS/COOP/COEP/CORP/Permissions-Policy/Referrer-Policy가 각자 다른 문제를 해결한다. 쿠키는 SameSite로 재정의됐고, 서드파티 쿠키는 사라지고 있다. CORS는 여전히 오해 1위고, CSP는 적용 실패율 1위다.

**교훈 5가지**:

1. **메서드와 상태 코드는 표준을 따라라** — 창의력 발휘하지 말 것

2. **캐시는 설계 초반에** — 나중에 Cache-Control 추가는 항상 후회

3. **보안 헤더는 deny-by-default** — `default-src 'self'`부터 시작해 허용을 추가

4. **쿠키 속성 완전체** — HttpOnly + Secure + SameSite + `__Host-` prefix

5. **HTTP 에러는 정보를 담아라** — `application/problem+json` + 고유 에러 코드

HTTP는 웹의 공용어다. 이 공용어를 정확히 쓸 줄 아는 개발자가 만든 API는 다른 팀이 6개월 뒤에도 이해할 수 있고, 그 위에 새 피처를 쌓기 쉽고, 보안 감사에서 수월하다. 반대로 "대충 POST 쓰고 200 리턴" 스타일의 API는 **기술 부채가 헤더마다 쌓인다**.

다음 글에서는 **브라우저 보안 모델 — Origin, Site, Process isolation, Sandboxing, Spectre/Meltdown 방어**를 깊이 다룬다. HTTP 헤더가 서버와 브라우저 사이의 계약이라면, 브라우저 내부에서 그 계약을 어떻게 실행하는지가 그 다음 층이다. CSP가 말하는 "script-src"가 실제로 어떤 프로세스에서 어떻게 격리되는지까지 파고든다.

HTTP 1.0에서 3.0까지 30년. 그동안 메서드는 PATCH 하나가 추가됐고, 상태 코드는 몇 개 늘었다. 하지만 그 뒤의 **보안 헤더 생태계**는 매년 새 표준이 나온다. 웹은 계속 복잡해지고, 그만큼 HTTP의 세맨틱을 아는 개발자의 가치도 계속 커진다.

현재 단락 (1/324)

GET /api/users/42 HTTP/1.1

작성 글자: 0원문 글자: 16,500작성 단락: 0/324