콘텐츠로 이동

HTTP Basics

분류: Layer 1 - 백엔드 기초 | 작성일: 2026-03-21

HTTP(HyperText Transfer Protocol)는 클라이언트와 서버가 리소스에 대한 요청(Request)과 응답(Response)을 주고받기 위해 정한 애플리케이션 계층 프로토콜이다.

정식 L1 도입부인 L1 백엔드 & API 입구에서 요청·응답부터 통신 방식 선택까지의 지도를 확인했다면, 이 문서는 그 첫 기준점인 “HTTP 요청 하나가 어떤 의미를 갖는가”를 독립적으로 다룬다. 뒤의 API 설계, 인증/인가, 웹 보안, WebSocket/gRPC 문서는 모두 이 의미를 전제로 한다. 처음 읽을 때는 명령어를 외우기보다 요청의 구성요소, 메서드의 의미, 상태 코드의 실패 신호, 헤더와 캐시의 약속을 잡는 데 집중한다. Keep-Alive timeout 수치, HTTP/2·3 전송 최적화, CDN/프록시 운영 세부는 첫 회독에서 전부 익히지 않아도 된다. 처음에는 “왜 같은 요청이라도 메서드·상태 코드·헤더가 달라지면 시스템 전체의 의미가 바뀌는가”를 잡고, 전송 버전과 튜닝값은 2회독에서 확인한다.

읽기 순서는 단순하다. 이 문서에서는 HTTP 요청 하나의 의미를 잡고, 다음 API Design 문서에서는 그 의미를 안정적인 클라이언트-서버 계약으로 만드는 법을 본다. 그다음 Auth 문서에서는 같은 HTTP 표면 위에서 신원 확인과 권한 판단이 어떻게 분리되는지 읽으면 된다.

백엔드와 플랫폼 업무에서 대부분의 장애 보고는 HTTP 표면으로 나타난다. 사용자는 “API가 안 된다”고 말하고, 모니터링은 401, 403, 502, 504 같은 숫자를 보여주며, 브라우저는 CORS 에러처럼 실제 요청이 서버까지 가지 않은 상황도 네트워크 실패처럼 보이게 만든다.

HTTP를 안다는 것은 모든 내부 구현을 안다는 뜻이 아니다. 다음 질문에 답할 수 있다는 뜻이다.

  • 이 요청은 어떤 리소스에 어떤 의도를 표현하는가?
  • 서버는 성공, 실패, 재시도 가능성, 캐시 가능성을 어떤 상태 코드와 헤더로 말하는가?
  • 같은 증상이라도 인증 문제인지, 권한 문제인지, 프록시 문제인지, 캐시 문제인지 어떻게 갈라볼 수 있는가?

2.5. 선행 기술의 한계 — HTTP가 맡은 일

섹션 제목: “2.5. 선행 기술의 한계 — HTTP가 맡은 일”

TCP(Transmission Control Protocol)는 바이트 스트림을 안정적으로 전달해 주지만, 그 바이트가 “사용자 목록 조회”인지 “결제 생성”인지 “캐시된 응답을 다시 써도 되는지”는 말해주지 않는다. TLS(Transport Layer Security)도 전송 중 데이터의 암호화·무결성과 서버 신원 검증을 담당할 뿐, 요청의 의도와 응답의 의미를 정하지 않는다.

HTTP는 이 빈칸을 채운다. 메서드는 의도, URL은 대상 리소스, 헤더는 메타데이터, 상태 코드는 결과를 표현한다. 그래서 HTTP는 단순 전송 계층 위에 “웹 애플리케이션이 서로 오해하지 않기 위한 의미 체계”를 얹은 프로토콜이라고 볼 수 있다.

실제 요청에서 DNS(Domain Name System)는 A·AAAA 같은 주소 레코드를 해석해 호스트 이름을 IP 주소에 연결한다. 아래 예시처럼 TLS가 Edge LB에서 종료되면 클라이언트와 Edge 사이의 보호는 그 지점에서 끝나며, Edge 이후 구간은 별도 TLS 연결이나 내부 네트워크 정책으로 보호해야 한다. LB(load balancer)의 L4 분산은 IP·포트와 TCP 연결 단위로 뒷단을 고르고, L7 라우팅은 HTTP host·path·header를 읽어 요청별 목적지를 고른다. 이 계층들은 HTTP 메시지의 의미를 정하지 않지만 요청이 앱에 도달하고 응답이 돌아오는 경로를 만든다.

URL 입력부터 HTTP 응답까지
sequenceDiagram
participant Client as Browser or Client
participant DNS
participant Edge as Edge LB / TLS termination
participant App as API Server
participant DB

Client->>DNS: hostname 조회
DNS-->>Client: IP 주소 반환
Client->>Edge: TCP 연결 수립
Client->>Edge: TLS handshake
Client->>Edge: HTTP request
Edge->>App: HTTP request 전달
App->>DB: 필요 시 데이터 조회
DB-->>App: rows
App-->>Edge: HTTP status + headers + body
Edge-->>Client: HTTP response

위 그림에서 DNS, TCP, TLS는 HTTP 바깥의 계층이다. 하지만 실제 장애는 이 경계가 섞여 보인다. 예를 들어 TLS 인증서 오류는 HTTP 상태 코드가 나오기 전 실패이고, 502는 HTTP 상태 코드지만 보통 프록시와 앱 서버 사이의 연결 문제가 원인이다.

HTTP 메시지는 크게 시작 줄(start line), 헤더(headers), 빈 줄, 바디(body)로 나뉜다. 요청과 응답의 모양은 다르지만 역할은 대칭적이다.

GET /users/1 HTTP/1.1
Host: api.example.com
Accept: application/json
Authorization: Bearer eyJ...
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: private, no-store
X-Request-Id: req-7f3a
{ "id": 1, "name": "Ada" }
구성요소요청에서의 의미응답에서의 의미
시작 줄GET /users/1 HTTP/1.1처럼 메서드, 경로, 버전을 담는다HTTP/1.1 200 OK처럼 버전과 상태 코드를 담는다
헤더인증, 본문 형식, 캐시 조건, 추적 ID 같은 메타데이터를 담는다응답 형식, 캐시 정책, 쿠키 설정, 추적 ID를 담는다
바디생성/수정 요청의 데이터가 들어간다조회 결과, 오류 상세, 생성된 리소스 표현이 들어간다

작은 관찰 예시는 다음과 같다.

Terminal window
curl -v https://httpbin.org/get
> GET /get HTTP/1.1
> Host: httpbin.org
> User-Agent: curl/8.5.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: application/json
< Content-Length: 340

이 출력에서 >는 클라이언트가 보낸 요청, <는 서버가 돌려준 응답이다. 디버깅할 때 첫 단계는 “내가 보낸 메서드, 경로, 헤더가 의도와 맞는가”와 “서버가 상태 코드와 헤더로 무엇을 말했는가”를 나누어 읽는 것이다.

3.1. URL, query, header, body의 역할 나누기

섹션 제목: “3.1. URL, query, header, body의 역할 나누기”

HTTP 요청을 설계하거나 읽을 때 자주 헷갈리는 지점은 “이 정보가 URL에 있어야 하는가, 헤더에 있어야 하는가, body에 있어야 하는가”다. 절대 규칙은 아니지만 첫 판단 기준은 있다.

위치담기 좋은 정보예시피해야 할 것
path리소스의 정체성/users/1, /orders/42/items사용자마다 달라지는 인증 토큰
query조회 조건, 정렬, 페이지?page=2&sort=createdAt너무 큰 JSON 객체, 민감정보
header요청 해석에 필요한 메타데이터Authorization, Accept-Language, If-None-Match비즈니스 데이터 전체
body생성/수정할 표현이나 복잡한 명령 입력{ "name": "Ada" }캐시 키로 쉽게 드러나야 하는 단순 조회 조건

Worked example로 검색 API를 보자.

GET /products?keyword=keyboard&page=2
Accept: application/json

이 요청은 “상품 목록이라는 리소스를 keyword와 page 조건으로 조회”한다. 조회 조건이 짧고 URL로 표현 가능하므로 GET과 query가 자연스럽다.

POST /products/search
Content-Type: application/json
{
"keyword": "keyboard",
"filters": {
"price": { "min": 10000, "max": 50000 },
"brands": ["A", "B", "C"]
}
}

두 번째 요청은 복잡한 검색 조건을 body에 담는다. 의미는 조회에 가깝지만 URL 길이, 구조화된 조건, 보안 로그 노출을 고려해 POST를 선택할 수 있다. 이때 POST라고 해서 반드시 “생성”만 뜻하는 것은 아니다. HTTP 메서드는 CRUD 이름표가 아니라 요청 의도와 전송 제약을 표현하는 약속이다.

HTTP 메서드는 서버에게 “무엇을 하려는지”를 알려주는 동사다. 메서드 선택은 코드 스타일 문제가 아니라 캐시, 재시도, 중복 실행 위험과 연결된다.

HTTP 메서드 선택 기준

GET

서버 상태를 바꾸지 않고 리소스를 조회한다. 안전하고 캐싱에 가장 친화적이다.

목록 조회, 상세 조회, 검색 조건이 URL query로 표현될 때

POST

서버에 새 처리를 요청한다. 생성뿐 아니라 결제, 작업 실행처럼 결과가 매번 달라질 수 있는 명령에도 쓴다.

회원가입, 결제 요청, 복잡한 검색 body가 필요할 때

PUT

리소스 전체 표현을 교체한다. 같은 요청을 여러 번 보내도 최종 상태가 같도록 설계한다.

클라이언트가 프로필 전체의 새 상태를 알고 있을 때

PATCH

리소스 일부 필드만 변경한다. 부분 변경을 표현하지만 패치 연산 자체가 항상 멱등인 것은 아니다.

이름만 변경, 알림 설정 하나만 토글할 때

DELETE

리소스 삭제를 요청한다. 삭제 후 최종 상태는 같아야 하지만 응답 코드는 첫 요청과 재시도가 다를 수 있다.

세션 제거, 임시 리소스 삭제처럼 제거 의도가 명확할 때

두 단어는 HTTP 메서드를 이해하는 핵심이다.

  • 안전성(safety): 요청이 서버의 의미 있는 상태를 바꾸지 않는 성질이다. GET은 조회가 목적이므로 안전해야 한다.
  • 멱등성(idempotency): 같은 요청을 여러 번 보내도 최종 상태가 한 번 보낸 것과 같은 성질이다. PUT /users/1로 같은 전체 프로필을 여러 번 저장하면 최종 상태는 같다.
메서드안전한가멱등인가재시도 감각
GET네트워크 실패 시 비교적 재시도하기 쉽다
POST아니오보통 아니오결제, 주문 생성은 중복 실행 위험이 있어 Idempotency-Key가 필요할 수 있다
PUT아니오같은 전체 상태를 다시 저장하는 재시도에 적합하다
PATCH아니오설계에 따라 다름count += 1 같은 패치는 재시도하면 값이 달라질 수 있다
DELETE아니오첫 번째는 204, 두 번째는 404가 와도 최종 상태는 삭제됨으로 같다

반례를 하나 보자. POST /payments가 네트워크 타임아웃으로 응답을 못 받았다고 해서 같은 요청을 그대로 다시 보내면 결제가 두 번 생성될 수 있다. 이때 Idempotency-Key: pay-20260407-001 같은 키를 함께 보내면 서버가 “같은 의도의 재시도”로 인식해 중복 생성을 막을 수 있다.

4.2. 메서드 선택을 손으로 판단하는 예시

섹션 제목: “4.2. 메서드 선택을 손으로 판단하는 예시”

다음 세 상황은 초보자가 자주 섞는 예시다.

상황자연스러운 선택이유
사용자 프로필 전체를 저장한다PUT /users/1/profile클라이언트가 전체 새 표현을 알고 있고, 같은 요청을 반복해도 최종 상태가 같다
알림 수신 여부 하나만 바꾼다PATCH /users/1/settings리소스 일부 필드만 바뀐다
주문 결제를 시작한다POST /orders/42/payments외부 결제 승인처럼 부수 효과가 있고 결과가 매번 달라질 수 있다

PATCH의 반례도 중요하다.

PATCH /counters/like
{ "op": "increment", "by": 1 }

이 요청은 일부 변경이지만 멱등이 아니다. 네트워크 실패 후 같은 요청을 재시도하면 좋아요 수가 1이 아니라 2 증가할 수 있다.

PATCH /users/1/settings
{ "emailNotification": false }

이 요청은 부분 변경이면서 사실상 멱등으로 설계할 수 있다. 같은 값을 여러 번 저장해도 최종 상태가 false로 같기 때문이다. 그래서 “PATCH는 무조건 재시도 위험”이 아니라, 패치 문서가 상태를 지정하는지 연산을 지정하는지까지 봐야 한다.

5. 상태 코드 — 결과와 디버깅 방향

섹션 제목: “5. 상태 코드 — 결과와 디버깅 방향”

상태 코드는 서버가 요청 결과를 세 자리 숫자로 요약한 것이다. 모든 코드를 외우기보다 첫 자리와 대표 코드를 디버깅 방향으로 연결한다.

범위의미먼저 볼 것
2xx성공바디가 있는지, 생성인지, 바디 없는 성공인지
3xx리다이렉션 또는 캐시 재검증Location, ETag, Cache-Control 헤더
4xx클라이언트 요청 문제요청 형식, 인증, 권한, 존재하지 않는 리소스
5xx서버 또는 중간 서버 문제앱 예외, 프록시, 로드밸런서, 타임아웃

대표 코드는 다음 정도를 먼저 잡으면 된다.

코드의미실패 신호로 읽는 법
200 OK성공, 보통 바디 있음바디 내용이 맞는지도 확인해야 한다
201 Created생성 성공Location 헤더나 생성된 리소스 ID를 같이 본다
204 No Content성공, 바디 없음DELETE 성공, 토글 성공처럼 결과 바디가 필요 없을 때 자연스럽다
304 Not Modified캐시 재사용 가능서버가 바디를 다시 보내지 않아도 된다는 뜻이다
400 Bad Request요청 형식 오류JSON 형식, 필수 필드, validation 실패를 본다
401 Unauthorized인증 실패토큰 없음, 만료, 서명 오류를 먼저 본다
403 Forbidden인가 실패신원은 확인됐지만 역할, 소유권, 정책이 막은 것이다
404 Not Found리소스 없음경로 오타와 실제 리소스 존재 여부를 나눠 본다
429 Too Many Requests요청량 제한Retry-After 헤더나 rate limit 정책을 본다
500 Internal Server Error앱 내부 오류서버 로그와 예외 trace를 본다
502 Bad Gateway프록시가 뒷단의 잘못된 응답을 받음앱 크래시, 연결 종료, keep-alive 불일치를 의심한다
503 Service Unavailable일시적 처리 불가점검, 과부하, readiness 실패를 본다
504 Gateway Timeout프록시가 뒷단 응답을 기다리다 시간 초과슬로우 쿼리, 외부 API 지연, timeout 설정을 본다

퀴즈

401과 403을 같은 인증 실패로 처리하면 무엇을 놓치는가?

힌트: HTTP 상태 코드는 디버깅 출발점을 좁히는 신호다.

정답 보기

401은 신원을 증명하지 못한 상태라 토큰 존재와 만료를 먼저 보고, 403은 신원은 확인됐지만 권한이 부족한 상태라 역할과 정책을 봐야 한다.

5.1. 502와 504는 둘 다 프록시 표면의 실패다

섹션 제목: “5.1. 502와 504는 둘 다 프록시 표면의 실패다”

502504는 앱 서버가 직접 만든 오류라기보다 중간 서버가 관찰한 실패인 경우가 많다. 로드밸런서, 리버스 프록시, CDN 같은 컴포넌트가 앞에 있으면 클라이언트는 앱이 낸 응답과 중간 서버가 낸 응답을 모두 HTTP 상태 코드로 받는다.

  • 502 Bad Gateway: 뒷단 서버가 연결을 끊었거나, 잘못된 응답을 보냈거나, 재시작 중이라 프록시가 정상 HTTP 응답을 받지 못한 신호다.
  • 504 Gateway Timeout: 프록시는 뒷단에 요청을 보냈지만 정해진 시간 안에 응답을 못 받은 신호다.

간단한 판단 기준은 “이상한 응답 또는 연결 종료면 502, 너무 오래 기다린 끝이면 504”다. 502가 재시도하면 곧바로 성공하는 패턴이면 keep-alive timeout 불일치나 배포 중 연결 종료를 의심하고, 504가 특정 API에서만 반복되면 DB 쿼리나 외부 API 지연을 먼저 의심한다.

헤더는 요청이나 응답의 본문을 해석하기 위한 메타데이터다. HTTP를 공부할 때 헤더를 “부가 정보”로만 보면 디버깅이 어려워진다. 인증, 캐시, 형식 협상, CORS, 추적은 모두 헤더로 표현된다.

헤더방향역할
Content-Type요청/응답바디가 JSON인지, HTML인지, 폼 데이터인지 알려준다
Accept요청클라이언트가 받고 싶은 응답 형식을 알려준다
Authorization요청bearer token 같은 인증 정보를 명시적으로 전달한다
Cookie요청브라우저가 조건에 맞는 쿠키를 자동 첨부한다
Set-Cookie응답서버가 브라우저에 쿠키 저장 규칙을 내려준다
Cache-Control요청/응답캐시 보관, 재검증, 공유 가능 여부를 정한다
ETag응답리소스 버전 식별자다
If-None-Match요청클라이언트가 가진 ETag와 비교해 달라고 요청한다
X-Request-Id요청/응답로그와 추적 시스템에서 같은 요청을 찾게 해준다

둘 다 인증과 연결될 수 있지만 동작 방식이 다르다.

구분CookieAuthorization 헤더
누가 붙이나브라우저가 도메인, 경로, SameSite 조건에 맞으면 자동 첨부한다클라이언트 코드가 명시적으로 붙인다
저장 위치 감각브라우저 쿠키 저장소앱 메모리, secure storage, 서버 간 secret 관리
서버가 내려주는 방식Set-Cookie 응답 헤더보통 로그인 응답 바디나 별도 토큰 발급 응답
주요 위험CSRF, SameSite 설정 오류, 쿠키 범위 과대토큰 노출, 만료 처리 누락, 재시도 중 헤더 누락
잘 맞는 곳브라우저 중심 세션, HttpOnly 쿠키 기반 로그인모바일 앱, 서버 간 호출, 명시적 API 클라이언트

쿠키가 자동으로 붙는다는 성질은 편리하지만, 브라우저 보안 정책과 강하게 묶인다. 반대로 Authorization: Bearer ...는 자동 첨부가 아니므로 CORS preflight 대상이 되기 쉽고, 클라이언트가 모든 요청에 헤더를 넣어야 한다.

용어

Stateless

HTTP 서버가 이전 요청을 기억하지 않는다는 뜻이다. 그래서 인증, 추적 ID, 멱등성 키처럼 다음 판단에 필요한 정보는 매 요청마다 다시 전달해야 한다.

실무에서는 Authorization 헤더, Cookie, Idempotency-Key, X-Request-Id가 이 한계를 보완한다.

6.2. Content-Type과 Accept는 서로 다른 방향이다

섹션 제목: “6.2. Content-Type과 Accept는 서로 다른 방향이다”

Content-TypeAccept는 모두 “형식”과 관련 있지만 방향이 다르다.

  • Content-Type: 지금 보내는 body가 어떤 형식인지 말한다.
  • Accept: 응답으로 어떤 형식을 받고 싶은지 말한다.
POST /users
Content-Type: application/json
Accept: application/json
{ "name": "Ada" }

이 요청은 “내가 보내는 body는 JSON이고, 응답도 JSON이면 좋겠다”는 뜻이다. 서버가 JSON body만 받는데 클라이언트가 Content-Type: text/plain으로 보내면 415 Unsupported Media Type이 자연스럽다. 서버가 JSON 응답만 만들 수 있는데 클라이언트가 Accept: text/csv만 허용하면 406 Not Acceptable을 고려할 수 있다.

실무에서는 400으로 뭉뚱그려 처리하는 API도 많지만, 학습 관점에서는 실패 원인을 나눠 보는 것이 중요하다. JSON 문법이 깨졌으면 body parsing 문제이고, Content-Type이 틀렸으면 형식 협상 문제이며, 필수 필드가 없으면 validation 문제다. 모두 4xx지만 고쳐야 할 위치가 다르다.

X-Request-Id, Traceparent, Correlation-Id 같은 헤더는 HTTP 자체의 필수 요소는 아니지만, 분산 시스템에서 요청 하나를 따라가기 위해 자주 쓴다.

Client -> API Gateway
X-Request-Id: req-20260407-0001
API Gateway -> App
X-Request-Id: req-20260407-0001
App -> Payment API
X-Request-Id: req-20260407-0001

이 값이 유지되면 사용자가 본 504를 API Gateway 로그, 앱 로그, 외부 결제 API 로그에서 같은 요청으로 묶을 수 있다. 반대로 중간 계층이 추적 헤더를 새로 만들거나 제거하면 “어느 요청이 어디서 느려졌는가”를 사람 손으로 맞춰야 한다. 상태 코드가 결과라면 추적 헤더는 원인 위치를 찾는 실마리다.

7. 캐시 — 같은 응답을 다시 보내지 않기 위한 의미 체계

섹션 제목: “7. 캐시 — 같은 응답을 다시 보내지 않기 위한 의미 체계”

HTTP 캐시는 “응답을 저장해도 되는가”, “얼마나 오래 그대로 써도 되는가”, “서버에 다시 확인해야 하는가”를 헤더로 정한다. 캐시는 성능 기능이지만, 잘못 설정하면 사용자가 오래된 데이터나 다른 사용자의 데이터를 볼 수 있다.

Cache-Control은 캐시 정책의 중심 헤더다.

directive의미자주 쓰는 상황
max-age=300300초 동안 신선하다고 본다공개 이미지, 버전이 붙은 정적 리소스
no-cache저장은 가능하지만 쓰기 전에 서버 재검증이 필요하다변경 가능성이 있지만 재검증으로 절약하고 싶을 때
no-store저장하지 말아야 한다개인정보, 인증된 사용자별 응답
private사용자 개인 캐시에만 저장 가능하다로그인 사용자별 응답
public공유 캐시도 저장할 수 있다CDN에 올려도 되는 공개 응답

반례가 중요하다. GET /users/me200 OK로 잘 응답해도 Cache-Control: public이 붙으면 CDN이나 공유 캐시가 사용자별 응답을 섞어 저장할 수 있다. 인증이 붙은 응답은 기본적으로 private, no-store 또는 그에 준하는 정책을 먼저 검토한다.

ETag는 리소스 버전 식별자다. 클라이언트가 이미 가진 버전과 서버의 현재 버전이 같으면 서버는 바디를 다시 보내지 않고 304 Not Modified만 돌려줄 수 있다.

ETag 기반 304 판단 흐름
flowchart TD
A["첫 GET /users/1"] --> B["200 OK + ETag v1 + body"]
B --> C["다음 요청: If-None-Match v1"]
C --> D{"서버 리소스가 바뀌었나?"}
D -->|아니오| E["304 Not Modified - body 없음"]
D -->|예| F["200 OK + ETag v2 + 새 body"]

작은 수치 감각을 붙이면 캐시의 의미가 선명해진다. 100KB짜리 응답을 1분에 1,000번 재검증한다고 하자. 매번 200으로 바디를 보내면 약 100MB를 전송한다. 같은 리소스가 바뀌지 않아 304로 헤더만 오가면 바디 전송은 거의 사라진다. 단, 304도 네트워크 왕복은 필요하므로 완전한 무료는 아니다.

첫 요청
GET /users/1
-> 200 OK
-> ETag: "user-1-v3"
-> { "id": 1, "name": "Ada" }
재검증 요청
GET /users/1
If-None-Match: "user-1-v3"
-> 304 Not Modified
-> body 없음

캐시는 실패해도 500을 만들지 않을 때가 많다. 그래서 상태 코드만 보는 모니터링은 놓치기 쉽다.

증상의미먼저 볼 개념
사용자가 옛 데이터를 보는데 서버 5xx가 없다ETag 비교나 재검증 헤더 전달이 깨졌을 수 있다ETag, If-None-Match, CDN 헤더 전달
사용자별 응답이 다른 사용자에게 보인다공유 캐시에 개인 응답이 저장됐을 수 있다Cache-Control: private, no-store
304가 와야 할 상황에서 계속 200과 큰 바디가 온다캐시 절약은 실패했지만 기능 오류로는 보이지 않을 수 있다ETag 생성 규칙, 프록시 헤더 제거

7.4. 캐시 정책을 고르는 작은 결정표

섹션 제목: “7.4. 캐시 정책을 고르는 작은 결정표”

캐시 정책은 성능과 정확성의 균형이다. 첫 설계에서는 응답을 세 그룹으로 나누면 판단이 쉬워진다.

응답 종류예시기본 정책 감각이유
버전이 붙은 공개 정적 파일/assets/app.a1b2.jspublic, max-age=31536000, immutable파일명이 바뀌면 새 리소스이므로 오래 저장해도 된다
공개 API 조회/products/1짧은 max-age 또는 ETag 재검증자주 바뀌지 않지만 최신성 요구가 있을 수 있다
로그인 사용자별 API/users/meprivate, no-store 또는 매우 보수적 캐시사용자별 데이터가 공유 캐시에 섞이면 안 된다
결제/주문 결과/payments/42보수적 캐시, 재검증 우선재무 상태는 오래된 응답이 큰 사고로 이어질 수 있다

정량 감각도 필요하다. 공개 상품 상세가 50KB이고 초당 100번 조회된다면 원본 서버는 초당 5MB, 1분이면 300MB의 body를 보낸다. 짧은 CDN 캐시나 ETag 재검증만 제대로 동작해도 원본 부하와 지연이 크게 줄어든다. 반대로 사용자별 5KB 응답을 잘못 공유 캐시하면 전송량 절감보다 데이터 노출 위험이 훨씬 크다.

캐시 설계의 좋은 질문은 “얼마나 빨라지는가”보다 먼저 “오래된 응답을 보아도 되는가”다. 오래된 응답이 가격 표시처럼 잠깐 허용될 수 있는지, 결제 완료 여부처럼 허용되면 안 되는지를 먼저 나누면 max-age와 재검증 전략이 따라온다.

HTTP는 애플리케이션 계층 의미 체계이고, 실제 전송은 TCP/TLS 같은 아래 계층에 기대는 경우가 많다. 성능 문제를 볼 때는 “HTTP 메시지 의미”와 “연결을 어떻게 재사용하는가”를 분리해서 봐야 한다.

TCP 연결을 새로 맺을 때는 3-way handshake가 필요하고, HTTPS라면 TLS handshake도 추가된다. 이 왕복 비용은 같은 서버에 많은 요청을 보낼 때 누적된다. Keep-Alive는 한 번 맺은 연결을 여러 HTTP 요청에 재사용해 이 비용을 줄인다.

같은 서버에 10회 연속 요청하는 상황
Without Keep-Alive:
요청마다 TCP + TLS handshake 반복
평균 응답 시간 예시: 첫 요청과 이후 요청이 모두 약 250ms
With Keep-Alive:
첫 요청에서 연결 수립, 이후 요청은 연결 재사용
평균 응답 시간 예시: 첫 요청 이후 약 50ms

이 숫자는 환경마다 달라지지만 판단 방향은 같다. 같은 origin에 짧은 요청을 많이 보낸다면 연결 재사용이 중요하다. 반대로 열린 연결을 너무 오래 유지하면 파일 디스크립터와 메모리를 점유하므로 idle timeout도 같이 봐야 한다.

시나리오

간헐적 502가 트래픽이 늘 때만 보이는 상황

ALB 앞단의 Node.js/NestJS API에서 재시도하면 대부분 성공하는 502가 보인다. 앱 로그에는 명확한 예외가 없고, 장애가 특정 API 로직에만 묶이지 않는다.

먼저 로드밸런서 idle timeout과 앱 서버 keepAliveTimeout의 상대값을 비교한다. 프록시가 연결을 재사용하려는 순간 앱이 먼저 닫으면 502로 보일 수 있다.

대표적인 수치 경계는 Node.js 서버의 기본 keep-alive timeout이 짧고, AWS ALB의 기본 idle timeout은 60초라는 점이다. 앱 서버가 5초 후 연결을 닫는데 ALB가 60초 동안 살아 있다고 믿고 재사용하면, 프록시는 뒷단이 연결을 닫은 순간을 502로 관찰할 수 있다. 여기서 중요한 것은 특정 명령어가 아니라 “양쪽 timeout의 상대 순서”다.

HTTP 버전은 같은 의미 체계를 더 효율적으로 싣기 위한 전송 방식의 차이로 먼저 이해한다.

버전핵심 변화첫 독서에서 잡을 경계
HTTP/1.1하나의 연결에서 요청/응답을 순서대로 처리하는 모델이 기본이다Keep-Alive로 연결을 재사용하지만 동시성은 제한적이다
HTTP/2하나의 TCP 연결 안에서 여러 스트림을 멀티플렉싱하고 헤더를 압축한다스트림은 나뉘지만 TCP 패킷 손실의 영향은 공유할 수 있다
HTTP/3QUIC 위에서 스트림을 독립적으로 다루고 연결 마이그레이션을 지원한다보통 앱 서버 코드보다 CDN/엣지 계층에서 먼저 만난다

작은 JSON API 응답을 같은 origin으로 많이 주고받는 서비스는 HTTP/2의 멀티플렉싱과 헤더 압축 이득을 체감하기 쉽다. 반대로 대용량 동영상 파일처럼 한 요청이 전송량 대부분을 차지하면 버전 차이보다 CDN, range request, 캐시 정책이 더 큰 변수가 될 수 있다. HTTP/3도 앱 코드를 바꾸는 문제라기보다 모바일 네트워크 전환, 패킷 손실, 엣지 지원 여부를 함께 보는 선택이다.

HTTP/1.1 연결 흐름
Client -> request 1 -> Server
Client <- response 1 <- Server
Client -> request 2 -> Server
Client <- response 2 <- Server
HTTP/2 멀티플렉싱 흐름
Client -> request 1, stream 1 -> Server
Client -> request 2, stream 2 -> Server
Client <- response 2, stream 2 <- Server
Client <- response 1, stream 1 <- Server

백엔드 입문 단계에서는 “NestJS 같은 앱 서버가 항상 HTTP/3를 직접 처리해야 한다”고 생각하지 않아도 된다. 흔한 구조는 브라우저나 모바일 앱과 CDN/로드밸런서 사이에서 HTTP/2 또는 HTTP/3를 쓰고, 엣지와 앱 서버 사이에서는 HTTP/1.1이나 HTTP/2를 쓰는 방식이다. 그래서 HTTP/2/3의 첫 질문은 “어느 구간에서 어떤 버전이 끝나는가”다.

Keep-Alive는 항상 켜면 좋은 스위치가 아니라 연결 비용과 연결 보유 비용의 교환이다.

상황연결 재사용 이점경계
같은 API 서버에 짧은 요청을 반복handshake 비용을 여러 요청에 분산한다idle timeout과 최대 연결 수를 본다
요청이 드물고 서버 리소스가 작음이점이 작을 수 있다열린 연결이 파일 디스크립터를 점유한다
서버 간 호출이 burst로 몰림커넥션 풀이 지연을 줄일 수 있다너무 큰 풀은 뒷단을 압박한다
프록시와 앱 timeout이 다름재사용 중 502가 날 수 있다양쪽 timeout의 상대값을 맞춘다

수치 반례를 보자. 파일 디스크립터 한도가 1,024인 작은 컨테이너에서 idle 연결 900개를 오래 붙잡고 있으면, 실제 처리 중인 요청이 많지 않아도 새 연결을 받을 여유가 작아진다. Keep-Alive는 요청 지연을 줄이는 장치지만, 연결 수 관측 없이 timeout만 길게 늘리면 다른 실패를 만든다.

8.4. 다른 통신 기술로 옮겨가는 질문

섹션 제목: “8.4. 다른 통신 기술로 옮겨가는 질문”

HTTP에서 배운 원리는 WebSocket, gRPC, 메시지 큐를 볼 때도 기준점이 된다.

HTTP에서 배운 질문gRPC에서의 연결WebSocket에서의 연결
상태는 어디에 있는가?Unary RPC는 호출 단위로 독립적이다연결 이후에는 애플리케이션이 상태 복원을 책임진다
다중화 단위는 무엇인가?HTTP/2 스트림으로 여러 RPC를 한 채널에 싣는다기본 WebSocket은 연결당 하나의 양방향 채널이다
연결 비용은 어떻게 나누는가?gRPC 채널을 재사용해야 한다연결 유지와 재연결 전략이 중요하다
메타데이터는 어디에 있는가?HTTP/2 metadata가 헤더 역할을 한다핸드셰이크 이후에는 메시지 설계에 따라 다르다

이 절은 심화 토픽의 예고편이다. 자세한 비교는 뒤의 WebSocket & gRPC 문서에서 다루고, 여기서는 “HTTP가 기준점이 된다”는 정도만 잡으면 된다.

9. CORS — HTTP가 아니라 브라우저가 강제하는 출처 정책

섹션 제목: “9. CORS — HTTP가 아니라 브라우저가 강제하는 출처 정책”

CORS(Cross-Origin Resource Sharing)는 서버 간 HTTP 통신의 일반 규칙이 아니라, 브라우저가 웹 페이지를 대신해 요청을 보낼 때 적용하는 보안 정책이다. 같은 HTTP 요청이라도 curl, 서버 코드, 브라우저 fetch에서 결과가 다르게 보일 수 있는 이유가 여기에 있다.

핵심 용어는 세 가지다.

  • Origin: scheme, host, port의 조합이다. https://app.example.comhttps://api.example.com은 host가 다르므로 다른 origin이다.
  • Preflight: 브라우저가 실제 요청 전에 OPTIONS 요청으로 허용 여부를 묻는 과정이다.
  • CORS 응답 헤더: 서버가 Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers 등으로 허용 범위를 말한다.
브라우저 fetch
Origin: https://app.example.com
Authorization: Bearer ...
preflight OPTIONS /users
Access-Control-Request-Method: GET
Access-Control-Request-Headers: authorization
서버 응답에 허용 헤더가 없으면:
브라우저가 실제 GET을 보내지 않거나 응답을 JS에 노출하지 않는다.

중요한 경계는 “CORS 에러가 서버가 죽었다는 뜻은 아니다”라는 점이다. 백엔드 로그에 OPTIONS만 있고 실제 GET이 없다면, HTTP 요청 자체가 브라우저 정책 단계에서 막힌 것이다.

Preflight는 성능에도 영향을 준다. Access-Control-Max-Age가 없으면 브라우저는 짧은 시간 뒤 다시 preflight를 보낼 수 있다. 예를 들어 10초마다 polling하는 API에서 preflight 캐시가 5초라면 실제 요청만큼 OPTIONS 요청도 반복되어 체감 요청 수가 거의 두 배가 된다. 다만 이 값은 브라우저별 상한이 있으므로, 숫자를 외우기보다 “preflight도 HTTP 요청이고 캐시할 수 있다”는 원리를 잡는다.

서버 개발자가 curl로 확인했을 때는 성공하지만 브라우저에서만 실패하는 상황을 보자.

curl -H "Authorization: Bearer token" https://api.example.com/users
-> 200 OK
browser fetch("https://api.example.com/users", {
headers: { Authorization: "Bearer token" }
})
-> CORS policy error

이 둘은 같은 HTTP 서버를 향하지만 같은 조건이 아니다. 브라우저 요청에는 Origin이 있고, Authorization 같은 비단순 헤더가 있으면 preflight가 먼저 간다. 서버가 실제 GET /users는 잘 처리해도 OPTIONS /users에 CORS 허용 헤더를 주지 않으면 브라우저는 실제 요청을 보내지 않거나 응답을 JavaScript에 넘기지 않는다.

또 다른 반례는 쿠키다.

fetch("https://api.example.com/me", {
credentials: "include"
})

credentials 설정과 쿠키의 SameSite/Secure 속성은 브라우저가 쿠키를 요청에 첨부할 수 있는지를 결정한다. 반면 구체적인 Access-Control-Allow-Origin(ACAO)과 Access-Control-Allow-Credentials: true(ACAC)는 credential이 포함된 cross-origin 응답을 JavaScript에 노출해도 되는지를 주로 통제한다. 특히 preflight가 없는 단순 credential 요청은 조건에 맞는 쿠키가 이미 전송되고 서버의 상태 변경까지 끝난 뒤, ACAO나 ACAC가 없어서 응답만 JavaScript에 노출되지 않을 수 있다. 따라서 CORS는 CSRF 방어가 아니며, 상태 변경 요청에는 SameSite 정책, CSRF 토큰, Origin 검증 같은 별도 방어가 필요하다. credential 응답에는 와일드카드 ACAO를 사용할 수 없다는 점도 함께 기억한다.

9.2. CORS 실패 신호를 HTTP 개념으로 번역하기

섹션 제목: “9.2. CORS 실패 신호를 HTTP 개념으로 번역하기”
관찰HTTP 관점 번역먼저 볼 것
서버 로그에 OPTIONS만 있다preflight에서 멈췄다allowed method/header/origin
GET은 200인데 브라우저 콘솔은 CORS 에러응답을 JS에 노출하지 않았다Access-Control-Allow-Origin
쿠키 로그인만 브라우저에서 풀린다쿠키 첨부 조건이 안 맞는다credentials, SameSite, Secure
curl은 항상 성공한다curl은 브라우저 CORS 정책을 강제하지 않는다브라우저 Network 탭의 preflight

운영 런북을 외우기 전에 증상, 의미, 먼저 볼 HTTP 개념을 연결한다. 다음 표는 긴 명령어 대신 첫 판단을 좁히기 위한 지도다.

증상의미먼저 확인할 개념
401이 난다요청자가 신원을 증명하지 못했다Authorization, Cookie, 토큰 만료
403이 난다신원은 알지만 접근 정책이 막았다role, scope, 소유권, RBAC/IAM 정책
400이 난다요청 형식이나 필드가 서버 계약과 맞지 않는다Content-Type, JSON shape, validation
429가 난다요청량 제한에 걸렸다rate limit window, Retry-After
배포 직후 502가 늘어난다프록시가 뒷단의 정상 응답을 못 받았다앱 기동 상태, readiness, keep-alive timeout
특정 API만 504가 난다뒷단 처리가 timeout 안에 끝나지 않았다DB 쿼리, 외부 API, timeout budget
브라우저는 CORS 에러, 서버 로그는 조용하다브라우저가 실제 요청을 막았을 수 있다preflight, allowed origin/header/method
200 OK인데 사용자가 옛 데이터를 본다상태 코드는 성공이지만 캐시 의미가 틀렸을 수 있다Cache-Control, ETag, CDN
curl은 성공, 브라우저만 실패한다HTTP 서버보다는 브라우저 정책 차이일 수 있다CORS, Cookie SameSite, credentials

HTTP 실패는 항상 5xx로 나타나지 않는다.

  1. 캐시 silent failure: 200 OK 또는 304 Not Modified가 오지만, 사용자가 기대한 최신 데이터가 아니다. ETag 생성 기준, CDN의 헤더 전달, Cache-Control을 본다.
  2. CORS silent failure: 백엔드에는 실제 GET/POST 로그가 없고 브라우저에는 CORS 에러만 보인다. 서버 장애가 아니라 preflight 허용 실패일 수 있다.
  3. 인증 헤더 silent failure: 클라이언트 코드가 Authorization을 특정 요청에서 빠뜨리면 서버는 정상적으로 401을 돌려준다. 앱 입장에서는 장애가 아니라 요청 계약 위반이다.

이런 실패를 줄이려면 상태 코드만 보지 말고 요청/응답 헤더와 body의 의미를 같이 기록해야 한다. X-Request-Id 같은 추적 헤더는 HTTP 계층과 앱 로그를 연결하는 최소 장치다.

다음 세 사례는 긴 운영 절차 없이도 HTTP 개념으로 첫 방향을 잡을 수 있다.

사례 A: 로그인 API는 성공했는데 다음 API가 401이다

POST /login -> 200 OK
GET /me -> 401 Unauthorized

먼저 “로그인은 성공했지만 다음 요청에 신원 정보가 전달됐는가”를 본다. 토큰 방식이면 Authorization 헤더가 빠졌는지, 쿠키 방식이면 Set-Cookie가 저장됐는지와 다음 요청의 Cookie 헤더가 붙었는지 본다. 이 문제를 권한 부족인 403으로 처리하면 role 정책을 뒤지느라 시간을 잃는다.

사례 B: 상품 상세가 가끔 오래된 가격을 보여준다

GET /products/1 -> 200 OK
Cache-Control: public, max-age=600

10분 동안 공개 캐시가 가격을 재사용할 수 있다는 뜻이다. 가격 최신성이 중요하다면 짧은 max-ageETag 재검증이 더 맞을 수 있다. 반대로 버전이 붙은 이미지라면 긴 max-age가 자연스럽다. 같은 200 OK라도 응답의 시간 의미가 다르다.

사례 C: 배포 직후 502가 2분간 증가했다가 사라진다

Client -> ALB -> App
응답: 502 Bad Gateway
재시도: 200 OK

앱 코드의 특정 예외보다 뒷단 연결과 준비 상태를 먼저 의심한다. 컨테이너가 아직 readiness를 통과하지 못했거나, 이전 연결을 프록시가 재사용하는 동안 앱이 재시작했을 수 있다. 여기서 핵심 개념은 502가 “앱이 비즈니스 오류를 돌려줬다”가 아니라 “중간 서버가 정상 응답을 받지 못했다”는 신호라는 점이다.

11. 선택 부록 — 손으로 확인하는 최소 관찰

섹션 제목: “11. 선택 부록 — 손으로 확인하는 최소 관찰”

아래 명령은 런북이 아니라 개념 확인용이다. 실제 운영 복구는 팀의 배포 환경과 권한 모델을 따라야 한다.

요청/응답 헤더 보기
Terminal window
curl -v https://httpbin.org/get
확인할 것:
- 요청 시작 줄: GET /get HTTP/1.1
- 요청 헤더: Host, User-Agent, Accept
- 응답 시작 줄: HTTP/1.1 200 OK
- 응답 헤더: Content-Type, Content-Length
ETag 재검증 흐름 보기
Terminal window
curl -I https://api.example.com/users/1
curl -I -H 'If-None-Match: "v1-abc123"' https://api.example.com/users/1
정상 관찰:
- 변경 없음: 304 Not Modified, body 없음
- 변경 있음: 200 OK, 새 ETag, 새 body
실패 신호:
- ETag가 매 요청 불필요하게 바뀌면 캐시 효율이 떨어진다.
- 인증된 사용자 응답에 public cache가 붙으면 데이터 노출 위험이 생긴다.
CORS preflight 관찰
Terminal window
curl -X OPTIONS https://api.example.com/users \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: GET" \
-H "Access-Control-Request-Headers: authorization" \
-i
확인할 것:
- Access-Control-Allow-Origin이 요청 Origin과 맞는가
- Access-Control-Allow-Headers에 authorization이 있는가
- Access-Control-Max-Age가 preflight 반복 비용을 줄일 만큼 설정됐는가
타임아웃 표면 보기
Terminal window
curl -o /dev/null -s -w "HTTP Code: %{http_code}\nTotal: %{time_total}s\n" \
https://api.example.com/health
해석:
- 200 + 짧은 Total: HTTP 표면은 정상
- 504 + 긴 Total: 프록시가 뒷단 응답을 기다리다 포기했을 가능성
- 502 + 짧은 Total: 뒷단 연결 종료, 배포 중 재시작, keep-alive 불일치 가능성
개념 A개념 B차이점
401 Unauthorized403 Forbidden401은 신원 확인 실패, 403은 신원 확인 후 권한 부족
PUTPATCHPUT은 전체 교체, PATCH는 일부 변경
안전성멱등성안전성은 상태를 안 바꾸는가, 멱등성은 여러 번 보내도 최종 상태가 같은가
HTTPHTTPSHTTPS는 HTTP 메시지를 TLS로 보호해 전송한다
500502500은 앱 내부 오류, 502는 중간 서버가 뒷단의 정상 응답을 받지 못한 것
502504502는 잘못된 응답/연결 종료, 504는 응답 시간 초과
200 OK304 Not Modified200은 새 표현을 보내고, 304는 클라이언트가 가진 표현을 재사용하라는 뜻
CookieAuthorizationCookie는 브라우저 자동 첨부, Authorization은 클라이언트가 명시적으로 첨부
CORS서버 간 접근 제어CORS는 브라우저가 강제하는 출처 정책이지 모든 HTTP 클라이언트의 공통 차단 규칙이 아니다
Content-TypeAcceptContent-Type은 보낸 body 형식, Accept는 받고 싶은 응답 형식이다
  • API 서버 간 통신: 요청 의도, timeout, 재시도, 인증 헤더를 읽는다.
  • 클라이언트와 백엔드 통신: 상태 코드, CORS, Cookie, cache 정책을 맞춘다.
  • 외부 서비스 연동: 401, 403, 429, 5xx를 계약 문서와 연결해 해석한다.
  • 배포 후 헬스체크: 200만 보는 것이 아니라 readiness, timeout, 프록시 상태 코드를 함께 본다.
  • 모니터링: 상태 코드 family, latency, request id, cache hit/miss를 함께 본다.

HTTP Basics 복습 체크

  • HTTP를 요청/응답 기반 애플리케이션 프로토콜로 설명할 수 있다
  • 요청의 메서드, URL, 헤더, 바디와 응답의 상태 코드, 헤더, 바디를 구분해 읽을 수 있다
  • GET, POST, PUT, PATCH, DELETE를 안전성/멱등성/재시도 관점으로 비교할 수 있다
  • 2xx, 3xx, 4xx, 5xx 상태 코드 family를 디버깅 방향과 연결할 수 있다
  • 401과 403, 502와 504의 차이를 설명할 수 있다
  • Cookie와 Authorization 헤더의 차이를 자동 첨부와 명시 첨부 관점으로 설명할 수 있다
  • ETag, If-None-Match, Cache-Control, 304의 관계를 작은 예시로 설명할 수 있다
  • Keep-Alive가 TCP/TLS handshake 비용을 줄이지만 timeout 불일치로 502를 만들 수 있음을 설명할 수 있다
  • HTTP/1.1, HTTP/2, HTTP/3의 경계를 어느 구간에서 적용되는지 중심으로 설명할 수 있다
  • CORS가 브라우저가 강제하는 정책이며 preflight 실패가 실제 요청 부재로 보일 수 있음을 설명할 수 있다

REST, Content Negotiation, Cookie SameSite, Session, JWT, CORS preflight, ETag, Cache-Control, Idempotency-Key, Retry-After, Keep-Alive, HTTP/2 stream, HTTP/3 QUIC, WebSocket Upgrade, gRPC metadata

17. 요약 — 이것만 기억해도 된다

섹션 제목: “17. 요약 — 이것만 기억해도 된다”
요청이 실패했다
├── 4xx: 요청자가 고칠 가능성이 크다
│ ├── 400: 요청 형식/데이터가 잘못됐다
│ ├── 401: 신원 증명이 안 됐다
│ ├── 403: 신원은 알지만 권한이 없다
│ ├── 404: 대상 리소스가 없다
│ └── 429: 너무 많이 요청했다
└── 5xx: 서버나 중간 계층을 봐야 한다
├── 500: 앱 내부 오류
├── 502: 프록시가 뒷단 정상 응답을 못 받음
├── 503: 일시적 처리 불가
└── 504: 뒷단 응답 시간 초과
  1. HTTP는 TCP/TLS 위에서 요청의 의도와 응답의 의미를 정하는 애플리케이션 프로토콜이다.
  2. 메서드는 의도, URL은 대상, 헤더는 메타데이터, 상태 코드는 결과, 바디는 표현을 담는다.
  3. 안전성, 멱등성, 캐시 가능성을 이해해야 재시도와 중복 실행 위험을 판단할 수 있다.
  4. 캐시, 인증, CORS, 연결 재사용은 모두 헤더와 상태 코드로 표면화된다.
  5. API 디버깅의 출발점은 긴 명령어가 아니라 요청/응답을 의미 단위로 읽는 것이다.