HTTP 기본 구조
DNS가 도메인 이름을 IP 주소로 변환해 주었으니, 이제 실제로 서버와 데이터를 주고받을 차례입니다. 웹에서 이 통신을 담당하는 프로토콜이 HTTP(Hypertext Transfer Protocol)입니다.
웹 개발자가 매일 사용하는 프로토콜이지만, GET과 POST의 차이가 뭔가요?를 넘어서 HTTP의 구조를 정확히 이해하면, API 설계, 성능 최적화, 보안 설정에서 훨씬 나은 판단을 내릴 수 있습니다.
요청과 응답 포맷
HTTP는 요청-응답(Request-Response) 패턴으로 동작합니다. 클라이언트가 요청을 보내면 서버가 응답을 돌려보냅니다.
HTTP/1.1 메시지는 텍스트 기반으로 표현됩니다. 다만 HTTPS라면 TLS로 암호화되므로 패킷 캡처에서 본문을 그대로 읽을 수 없습니다. HTTP/2와 HTTP/3는 바이너리 프레이밍을 사용하지만, 메서드·URI·상태 코드·헤더·본문이라는 논리적 의미는 HTTP Semantics로 유지됩니다.
HTTP 메서드
HTTP 메서드는 클라이언트가 서버에 어떤 동작을 원하는지 표현합니다.
| 메서드 | 용도 | 본문 | 멱등 | 안전 | 사용 예 |
|---|---|---|---|---|---|
| GET | 조회 | 보통 없음 | ✓ | ✓ | 페이지 로드, API 데이터 조회 |
| POST | 처리/생성 | 있음 | ✗ | ✗ | 회원가입, 게시글 작성, 파일 업로드 |
| PUT | 전체 대체 | 있음 | ✓ | ✗ | 프로필 전체 수정 |
| PATCH | 부분 수정 | 있음 | 설계에 따라 | ✗ | 이메일만 변경 |
| DELETE | 삭제 | 보통 없음 | ✓ | ✗ | 계정 삭제, 게시글 삭제 |
| OPTIONS | 메서드 확인 | 보통 없음 | ✓ | ✓ | CORS 프리플라이트 |
| HEAD | 헤더만 조회 | 응답 본문 없음 | ✓ | ✓ | 리소스 존재 확인, 크기 확인 |
GET /search?q=network&page=1 HTTP/1.1
* 데이터를 URL 쿼리스트링에 포함
* 브라우저 히스토리에 남음
* 북마크 가능
* 실무상 브라우저/서버/프록시별 URL 길이 제한에 영향
* 캐시 가능
* 브라우저 뒤로 가기 시 재요청 없음
POST /api/users HTTP/1.1
Content-Type: application/json
{"name": "홍길동"}
* 데이터를 본문(Body)에 포함
* 히스토리에 안 남음
* 북마크 불가
* 크기 제한 없음 (서버 설정에 따라)
* 기본적으로 캐시 활용이 어렵지만, 명시적 캐시 조건을 둘 수 있음
* 뒤로 가기 시 "다시 제출?" 확인GET 요청에도 메시지 본문 자체를 붙이는 것이 문법적으로 완전히 불가능한 것은 아니지만, 표준 의미가 일반적으로 정의되어 있지 않아 실무에서는 쓰지 않는 편이 안전합니다.
멱등성과 안전성
HTTP 메서드를 분류하는 두 가지 중요한 속성이 있습니다.
안전성(Safety): 클라이언트가 요청한 의미가 서버 상태 변경을 요구하지 않는 메서드입니다. GET, HEAD, OPTIONS가 안전합니다. 서버가 로그를 남기거나 통계를 갱신하는 부수 효과까지 모두 금지한다는 뜻은 아닙니다.
멱등성(Idempotency): 같은 요청을 한 번 보내든 여러 번 보내든 서버에 남는 의도한 효과가 동일한 메서드입니다. 응답 코드나 응답 본문이 매번 완전히 같아야 한다는 뜻은 아닙니다.
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")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 스트림 구조로 크게 완화합니다. 다만 같은 스트림 내부에서는 순서 있는 전달이 필요하므로, 손실의 영향이 완전히 사라지는 것은 아닙니다.
다음 절에서는 서버가 응답할 때 사용하는 상태 코드와 요청/응답에 포함되는 헤더를 살펴보겠습니다.