들어가며
HTTP(HyperText Transfer Protocol)는 웹의 근간을 이루는 프로토콜입니다. 백엔드 개발자에게 HTTP에 대한 깊은 이해는 성능 최적화, 디버깅, API 설계 등 모든 영역에서 필수적입니다. 이 글에서는 HTTP/1.1부터 HTTP/3까지의 발전 과정과, TCP 핸드셰이크, TLS, Keep-Alive, 상태 코드 등 실무에서 반드시 알아야 할 내용을 정리합니다.
1. HTTP 프로토콜의 발전
HTTP/1.1 (1997)
현재까지도 널리 사용되는 버전입니다. 주요 특징은 다음과 같습니다.
- Keep-Alive: 기본적으로 연결을 유지하여 매 요청마다 TCP 연결을 새로 맺지 않습니다.
- 파이프라이닝: 이론적으로 여러 요청을 연속 전송할 수 있지만, Head-of-Line Blocking 문제로 실질적으로 거의 사용되지 않습니다.
- 청크 전송 인코딩:
Transfer-Encoding: chunked를 통해 응답 크기를 미리 모르더라도 스트리밍 전송이 가능합니다.
HTTP/2 (2015)
Google의 SPDY 프로토콜을 기반으로 표준화되었습니다.
- 멀티플렉싱: 하나의 TCP 연결에서 여러 요청/응답을 동시에 처리합니다. HTTP/1.1의 HOL Blocking 문제를 해결합니다.
- 헤더 압축 (HPACK): 중복되는 헤더를 정적/동적 테이블로 관리하여 전송량을 줄입니다.
- 서버 푸시: 클라이언트가 요청하기 전에 서버가 필요한 리소스를 미리 전송할 수 있습니다.
- 바이너리 프레이밍: 텍스트가 아닌 바이너리 형태로 데이터를 전송하여 파싱 효율을 높입니다.
HTTP/3 (2022)
TCP 대신 QUIC(UDP 기반) 프로토콜을 사용합니다.
- TCP HOL Blocking 해결: HTTP/2는 멀티플렉싱을 지원하지만, TCP 계층에서 패킷 손실이 발생하면 모든 스트림이 차단됩니다. QUIC은 스트림 단위로 독립적이므로 이 문제가 없습니다.
- 0-RTT 연결: 이전에 접속한 서버라면 핸드셰이크 없이 즉시 데이터를 전송할 수 있습니다.
- 연결 마이그레이션: Wi-Fi에서 셀룰러로 전환되어도 연결이 끊어지지 않습니다. Connection ID 기반으로 연결을 식별하기 때문입니다.
2. TCP 3-Way Handshake
HTTP/1.1과 HTTP/2는 TCP 위에서 동작합니다. 클라이언트와 서버 간의 연결 수립 과정을 이해하는 것이 중요합니다.
클라이언트 서버
| |
|--- SYN (seq=x) --------->| ① 클라이언트가 연결 요청
| |
|<-- SYN-ACK (seq=y, ------|
| ack=x+1) | ② 서버가 요청 수락 + 자신의 SYN
| |
|--- ACK (ack=y+1) ------->| ③ 클라이언트가 확인 응답
| |
|===== 연결 수립 완료 =======|
이 과정은 최소 1 RTT(Round Trip Time)가 소요됩니다. 서울에서 미국 서부 서버까지의 RTT가 약 150ms라면, 연결 수립에만 150ms가 걸립니다. 여기에 TLS 핸드셰이크까지 더하면 지연이 상당해집니다.
연결 종료: 4-Way Handshake
클라이언트 서버
|--- FIN --->| ① 연결 종료 요청
|<-- ACK ----| ② 확인
|<-- FIN ----| ③ 서버도 종료 준비 완료
|--- ACK --->| ④ 최종 확인
연결 종료 시에는 양쪽 모두 종료 의사를 밝혀야 하므로 4단계가 필요합니다. 이 과정에서 TIME_WAIT 상태가 발생하며, 이는 서버에서 대량의 단기 연결을 처리할 때 포트 고갈 문제를 일으킬 수 있습니다.
3. TLS(Transport Layer Security) 핸드셰이크
HTTPS 통신을 위해서는 TCP 연결 수립 후 TLS 핸드셰이크가 추가로 필요합니다.
TLS 1.2 핸드셰이크 (2-RTT)
클라이언트 서버
|--- ClientHello ---------------->| 지원하는 암호화 스위트 목록 전송
|<-- ServerHello, Certificate, ----|
| ServerKeyExchange, Done | 서버 인증서 + 키 교환 파라미터
|--- ClientKeyExchange, ---------->|
| ChangeCipherSpec, Finished | Pre-master secret 전송
|<-- ChangeCipherSpec, Finished ---|
|===== 암호화 통신 시작 ===========|
TLS 1.3 핸드셰이크 (1-RTT)
TLS 1.3에서는 핸드셰이크가 1-RTT로 줄었습니다. ClientHello에 키 교환 파라미터를 함께 전송하여 왕복 횟수를 줄였습니다. 또한 안전하지 않은 암호화 알고리즘(RC4, 3DES, SHA-1 등)이 완전히 제거되었습니다.
# Nginx에서 TLS 1.3 설정 예시
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;
4. Keep-Alive와 커넥션 관리
HTTP/1.0에서는 매 요청마다 TCP 연결을 새로 맺었습니다. HTTP/1.1부터는 Connection: keep-alive가 기본값으로, 하나의 TCP 연결을 재사용합니다.
# 요청 헤더
GET /api/users HTTP/1.1
Host: api.example.com
Connection: keep-alive
# 응답 헤더
HTTP/1.1 200 OK
Connection: keep-alive
Keep-Alive: timeout=60, max=100
timeout=60은 유휴 상태에서 60초간 연결을 유지하겠다는 의미이고, max=100은 이 연결로 최대 100개의 요청을 처리하겠다는 의미입니다.
Spring Boot에서의 설정
# application.yml - 내장 Tomcat 설정
server:
tomcat:
keep-alive-timeout: 60000 # Keep-Alive 타임아웃 (ms)
max-keep-alive-requests: 100 # 최대 Keep-Alive 요청 수
connection-timeout: 20000 # 연결 타임아웃 (ms)
max-connections: 8192 # 최대 동시 연결 수
threads:
max: 200 # 최대 스레드 수
min-spare: 10 # 최소 유휴 스레드 수
5. HTTP 상태 코드 완벽 정리
상태 코드를 정확히 사용하는 것은 좋은 API 설계의 기본입니다.
2xx - 성공
| 코드 | 의미 | 사용 시점 |
|---|---|---|
| 200 OK | 요청 성공 | 일반적인 GET, PUT 요청의 성공 응답 |
| 201 Created | 리소스 생성 | POST로 새 리소스가 생성되었을 때. Location 헤더에 새 리소스의 URI를 포함해야 합니다. |
| 204 No Content | 본문 없음 | DELETE 성공 시 또는 PUT으로 업데이트 후 응답 본문이 불필요할 때 |
3xx - 리다이렉션
| 코드 | 의미 | 사용 시점 |
|---|---|---|
| 301 Moved Permanently | 영구 이동 | URI가 영구적으로 변경. 검색 엔진이 새 URI로 인덱스를 갱신합니다. |
| 302 Found | 임시 이동 | 일시적으로 다른 URI를 사용해야 할 때 |
| 304 Not Modified | 변경 없음 | 캐시된 리소스가 여전히 유효할 때. 본문 없이 헤더만 응답합니다. |
4xx - 클라이언트 오류
| 코드 | 의미 | 사용 시점 |
|---|---|---|
| 400 Bad Request | 잘못된 요청 | 요청 본문의 유효성 검증 실패, 잘못된 파라미터 |
| 401 Unauthorized | 인증 필요 | 인증되지 않은 요청. 로그인이 필요합니다. |
| 403 Forbidden | 접근 금지 | 인증은 되었지만 권한이 없는 경우 |
| 404 Not Found | 찾을 수 없음 | 요청한 리소스가 존재하지 않을 때 |
| 409 Conflict | 충돌 | 리소스 상태와 요청이 충돌할 때 (예: 중복 가입) |
| 429 Too Many Requests | 요청 과다 | Rate Limit 초과 시. Retry-After 헤더와 함께 응답합니다. |
5xx - 서버 오류
| 코드 | 의미 | 사용 시점 |
|---|---|---|
| 500 Internal Server Error | 서버 내부 오류 | 예상치 못한 서버 오류. 클라이언트에게 상세 정보를 노출하면 안 됩니다. |
| 502 Bad Gateway | 게이트웨이 오류 | 리버스 프록시(Nginx)가 업스트림 서버로부터 유효하지 않은 응답을 받았을 때 |
| 503 Service Unavailable | 서비스 불가 | 서버 점검 또는 과부하 상태. Retry-After 헤더를 포함해야 합니다. |
| 504 Gateway Timeout | 게이트웨이 타임아웃 | 업스트림 서버가 응답 시간 내에 응답하지 않았을 때 |
6. HTTP 캐싱
효율적인 캐싱은 서버 부하를 줄이고 응답 속도를 높이는 핵심 전략입니다.
# Spring Boot에서 캐시 헤더 설정
@GetMapping("/api/products/{id}")
public ResponseEntity<Product> getProduct(@PathVariable Long id) {
Product product = productService.findById(id);
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(Duration.ofHours(1))
.mustRevalidate())
.eTag(String.valueOf(product.getVersion()))
.body(product);
}
Cache-Control: max-age=3600, must-revalidate는 1시간 동안 캐시를 사용하되, 만료 후에는 반드시 서버에 재검증하라는 의미입니다. ETag는 리소스의 버전을 식별하는 값으로, 조건부 요청(If-None-Match)에 활용됩니다.
마치며
HTTP는 단순해 보이지만, 그 내부에는 수십 년간 축적된 설계 철학과 최적화 기법이 담겨 있습니다. TCP 핸드셰이크부터 TLS, Keep-Alive, 캐싱까지 각 계층의 동작 원리를 이해하면 성능 병목을 진단하고 최적화하는 능력이 크게 향상됩니다. 특히 HTTP/3와 QUIC의 등장으로 네트워크 프로토콜은 계속 진화하고 있으므로, 지속적으로 관심을 가지고 학습하는 것을 추천합니다.
'CS' 카테고리의 다른 글
| 백엔드 개발자 기술 면접 완벽 대비 - Spring/JPA/DB/인프라 핵심 질문 50선 (0) | 2026.04.14 |
|---|---|
| DNS와 CDN 동작 원리 - 웹 성능 최적화의 기초 (0) | 2026.04.08 |
| 실시간 통신 완벽 가이드 - WebSocket, SSE, gRPC 비교 분석 (0) | 2026.04.08 |
| OAuth 2.0 / OIDC 완벽 가이드 - 인증 프로토콜의 모든 것 (1) | 2026.04.08 |
| REST API 설계 원칙 - 실무에서 바로 쓰는 베스트 프랙티스 (0) | 2026.03.31 |