HTTP 기본 구조
HTTP 요청·응답 메시지와 메서드의 의미·안전성·멱등성을 익히고 HTTP 버전별 전송 방식의 차이를 이해합니다.
DNS가 도메인 이름을 IP 주소로 변환해 주었으니, 이제 실제로 서버와 데이터를 주고받을 차례입니다.
웹에서 이 통신을 담당하는 프로토콜이 HTTP(Hypertext Transfer Protocol)입니다.
웹 개발자가 매일 사용하는 프로토콜이지만, GET과 POST의 차이를 넘어서 HTTP의 구조를 정확히 이해하면, API 설계, 성능 최적화, 보안 설정에서 훨씬 나은 판단을 내릴 수 있습니다.
요청과 응답 포맷
HTTP는 요청-응답(Request-Response) 패턴으로 동작합니다.
클라이언트가 요청을 보내면 서버가 응답을 돌려보냅니다.
POST /api/users HTTP/1.1
Host: example.com
Content-Type: application/json
Content-Length: 2
{}HTTP/1.1 201 Created
Location: /api/users/1
Content-Type: application/json
Content-Length: 2
{}위 메시지는 빈 JSON 객체 2바이트를 사용하는 형식 예시입니다. 실행 응답이 아니며, 줄 끝은 실제 HTTP/1.1 전송에서 CRLF를 사용합니다.
HTTP/1.1의 시작 줄과 헤더는 텍스트 형식이고, 본문에는 이미지 같은 바이너리도 올 수 있습니다. 시작 줄 뒤의 헤더 목록은 빈 줄로 끝납니다.
다만 HTTPS라면 TLS로 암호화되므로 패킷 캡처에서 본문을 그대로 읽을 수 없습니다.
HTTP/2와 HTTP/3는 바이너리 프레이밍을 사용하지만, 메서드·URI·상태 코드·헤더·본문이라는 논리적 의미는 HTTP Semantics로 유지됩니다.
예시
위 요청의 첫 줄은 메서드 POST, 대상 경로 /api/users, 메시지 버전 HTTP/1.1을 나타냅니다. Host는 이 연결에서 요청할 호스트를 구분하고 Content-Type은 뒤 본문을 JSON으로 해석하라는 정보입니다. Content-Length의 2는 헤더를 포함한 메시지 전체 길이가 아니라 ASCII 문자 {와 }로 된 본문의 바이트 수입니다. 한글 문자열을 넣는 다른 예에서는 글자 수를 그대로 쓰지 않고 실제 인코딩한 바이트 수를 계산해야 합니다. 헤더와 본문 사이의 빈 줄이 없으면 어디서 본문이 시작하는지 같은 방식으로 읽을 수 없습니다.
HTTP 메서드
HTTP 메서드는 클라이언트가 서버에 어떤 동작을 원하는지 표현합니다.
HTTP · 메서드 의미
안전성과 멱등성은 요청의 의도한 효과를 분류한다
안전성은 상태 변경을 요청하는지, 멱등성은 같은 요청을 반복해도 의도한 효과가 같은지를 봅니다. 응답 코드나 본문이 매번 동일하거나 부수적 로깅이 없다는 뜻은 아닙니다.
| 메서드 | 요청 의미와 본문 | 안전 / 멱등 |
|---|---|---|
| GET | 조회; 페이지 로드, API 데이터 조회. 보통 없음 | 예 / 예 |
| POST | 처리/생성; 회원가입, 게시글 작성, 파일 업로드. 본문을 주로 사용 | 아니오 / 아니오 |
| PUT | 전체 대체; 프로필 전체 수정. 표현을 본문에 전달 | 아니오 / 예 |
| PATCH | 부분 수정; 이메일만 변경. 변경 내용을 본문에 전달 | 아니오 / 설계에 따라 |
| DELETE | 삭제; 계정 삭제, 게시글 삭제. 보통 없음 | 아니오 / 예 |
| OPTIONS | 메서드 확인; CORS 프리플라이트. 보통 없음 | 예 / 예 |
| HEAD | 헤더만 조회; 리소스 존재 확인, 크기 확인. 응답 본문 없음 | 예 / 예 |
- GET
- 요청 의미와 본문: 조회; 페이지 로드, API 데이터 조회. 보통 없음안전 / 멱등: 예 / 예
- POST
- 요청 의미와 본문: 처리/생성; 회원가입, 게시글 작성, 파일 업로드. 본문을 주로 사용안전 / 멱등: 아니오 / 아니오
- PUT
- 요청 의미와 본문: 전체 대체; 프로필 전체 수정. 표현을 본문에 전달안전 / 멱등: 아니오 / 예
- PATCH
- 요청 의미와 본문: 부분 수정; 이메일만 변경. 변경 내용을 본문에 전달안전 / 멱등: 아니오 / 설계에 따라
- DELETE
- 요청 의미와 본문: 삭제; 계정 삭제, 게시글 삭제. 보통 없음안전 / 멱등: 아니오 / 예
- OPTIONS
- 요청 의미와 본문: 메서드 확인; CORS 프리플라이트. 보통 없음안전 / 멱등: 예 / 예
- HEAD
- 요청 의미와 본문: 헤더만 조회; 리소스 존재 확인, 크기 확인. 응답 본문 없음안전 / 멱등: 예 / 예
예/아니오는 메서드가 정의한 의도에 대한 분류입니다. GET의 부수적 로깅이나 반복 요청의 응답 코드 차이까지 없다는 뜻은 아닙니다.
GET /search?q=network&page=1 HTTP/1.1
* 데이터를 URL 쿼리스트링에 포함
* 브라우저 히스토리에 남음
* 북마크 가능
* 실무상 브라우저/서버/프록시별 URL 길이 제한의 영향을 받음
* 캐시 가능
* 뒤로 가기 시 캐시·이력 복원 정책에 따라 재요청할 수 있음
POST /api/users HTTP/1.1
Content-Type: application/json
{"name": "홍길동"}
* 데이터를 본문(Body)에 포함
* URL과 달리 POST 본문은 주소 표시줄에 나타나지 않음
* 북마크 불가
* 본문 크기는 서버·프록시·애플리케이션의 제한을 받음
* 기본적으로 캐시 활용이 어렵지만, 명시적 캐시 조건을 둘 수 있음
* 뒤로 가기에서 재전송이 필요하면 재제출 확인이 나타날 수 있음GET 요청에도 메시지 본문 자체를 붙이는 것이 문법적으로 완전히 불가능한 것은 아니지만, 표준 의미가 일반적으로 정의되어 있지 않아 실무에서는 쓰지 않는 편이 안전합니다.
멱등성과 안전성
HTTP 메서드를 분류하는 두 가지 중요한 속성이 있습니다.
안전성(Safety): 클라이언트가 요청한 의미가 서버 상태 변경을 요구하지 않는 메서드입니다.
GET, HEAD, OPTIONS가 안전합니다.
서버가 로그를 남기거나 통계를 갱신하는 부수 효과까지 모두 금지한다는 뜻은 아닙니다.
멱등성(Idempotency): 같은 요청을 한 번 보내든 여러 번 보내든 서버에 남는 의도한 효과가 동일한 메서드입니다.
응답 코드나 응답 본문이 매번 완전히 같아야 한다는 뜻은 아닙니다.
예시: 반복해도 같은 효과인가요? 학습용 API에서 PUT /settings/theme은 요청 표현의 mode로 현재 설정을 대체합니다. mode=dark인 같은 요청을 두 번 처리하면 마지막 설정도 dark입니다. 두 호출의 로그가 남거나 응답 코드가 달라도 의도한 설정 효과는 같습니다. 반면 POST /counter/add가 전달한 값을 현재 카운터에 더하는 계약이라면, 초기값 7에 값 8을 두 번 처리한 결과는 23입니다. POST에 중복 방지 계약을 따로 설계할 수 있지만 메서드 이름만으로 그 계약이 생기지는 않습니다. RFC9110의 멱등성과 재시도를 이 차이와 연결해 읽으세요.
독립 판단하기 PUT /settings/theme으로 mode=light를 두 번 적용했고, 첫 응답은 200과 JSON, 두 번째는 204와 빈 본문이었습니다. 다른 작성자는 없습니다. 응답이 다르므로 멱등성이 깨졌다고 판단할 수 있을까요? 그리고 서버에 도착한 POST 주문 생성의 응답만 중간에서 사라졌다면, 같은 주문을 다시 보내도 한 번만 생성된다고 HTTP 메서드만으로 보장할 수 있을까요?
효과와 응답을 나누는 풀이
최종 mode는 한 번 처리했을 때도 두 번 처리했을 때도 light이므로, 응답 표현 차이는 이 멱등성을 깨뜨리지 않습니다. POST 주문은 이미 처리됐는지 모르는 상태입니다. API의 중복 식별자·조회 방법 등 계약을 확인하지 않고 재전송하면 중복 생성될 수 있습니다. 반대로 그런 계약이 있는 API의 안전한 재시도 가능성까지 POST라는 이름만으로 부정하지 않습니다.
import json
from http.client import HTTPSConnection
def http_request(method, host, path, body=None):
"""다양한 HTTP 메서드로 요청 전송"""
conn = HTTPSConnection(host)
headers = {
"Content-Type": "application/json",
"Accept": "application/json",
}
json_body = json.dumps(body) if body else None
conn.request(method, path, body=json_body, headers=headers)
response = conn.getresponse()
data = response.read().decode()
print(f"{method} {path}")
print(f" Status: {response.status} {response.reason}")
print(f" Content-Type: {response.getheader('Content-Type')}")
if data and len(data) < 200:
print(f" Body: {data}")
print()
conn.close()
return response.status, data
# 사용 예시 (httpbin.org는 HTTP 테스트 서비스)
# http_request("GET", "httpbin.org", "/get")
# http_request("POST", "httpbin.org", "/post", {"name": "test"})
# http_request("PUT", "httpbin.org", "/put", {"name": "updated"})
# http_request("DELETE", "httpbin.org", "/delete")보존 코드의 입력 확인
위 Python 함수는 네트워크 요청 함수의 간단한 형태이며 호출 예는 주석입니다. if body를 사용하므로 None뿐 아니라 빈 객체 {}, 빈 배열 [], 숫자 0, False도 본문을 만들지 않습니다. JSON의 빈 객체를 의도했다면 HTTP 형식 예의 {}와 같은 본문을 이 함수에 그대로 기대할 수 없습니다. 확장할 때는 “입력이 없는 경우”를 None으로 구분해 직렬화하고, 응답 인코딩·압축·타임아웃과 오류 시 연결 정리를 별도로 설계합니다. 원 함수가 임의의 JSON·바이너리 응답을 모두 처리하는 클라이언트는 아닙니다.
HTTP 버전별 차이
| 버전 | 연도와 전송 특성 |
|---|---|
| HTTP/1.0 | 연도: 1996; 연결 방식: 기본적으로 요청마다 새 연결; 특징: 연결 재사용 확장도 사용됨 |
| HTTP/1.1 | 연도: 1997; 연결 방식: Keep-Alive (연결 재사용); 특징: 파이프라이닝 지원, 실무 채택 제한 |
| HTTP/2 | 연도: 2015; 연결 방식: 멀티플렉싱 (1연결, 병렬 전송); 특징: 바이너리, 헤더 압축, 서버 푸시(효용 제한) |
| HTTP/3 | 연도: 2022; 연결 방식: QUIC (UDP 기반); 특징: 스트림 독립성, TLS 1.3, 0-RTT |
HTTP/3는 TCP 연결 단위의 HOL Blocking을 QUIC 스트림 구조로 크게 완화합니다.
다만 같은 스트림 내부에서는 순서 있는 전달이 필요하므로, 손실의 영향이 완전히 사라지는 것은 아닙니다.
다음 절에서는 서버가 응답할 때 사용하는 상태 코드와 요청/응답에 포함되는 헤더를 살펴보겠습니다.