본문 바로가기
C.W.K.
Stream
Lesson 03 of 07 · published

HTTP 아래의 TCP와 TLS — 연결부터 암호화까지

~11 min · foundations, tcp, tls, https, transport

Level 0HTTP 입문자
0 XP0/46 lessons0/12 achievements
0/120 XP to next level120 XP to go0% complete
"TCP를 거의 의식하지 않고도 HTTP 코드를 오래 작성할 수 있어. 하지만 연결이 멈추거나 TLS 핸드셰이크가 실패하는 순간, 한 계층 아래를 볼 줄 아는지가 문제 해결 속도를 갈라."

계층 구조

HTTP 요청은 여러 프로토콜 계층을 차례로 지나가. 각 계층은 아래 계층의 복잡함을 감추고 위 계층에 더 단순한 기능을 제공해:

  • HTTP — 애플리케이션이 다루는 메시지. 메서드, URI, 헤더, 본문으로 이루어져.
  • TLS — 통신을 암호화하고 상대의 신원을 검증하는 보안 계층. HTTPS에는 TLS가 포함되고, 평문 HTTP에는 없어.
  • TCP — 신뢰할 수 있고 순서가 보장되는 바이트 스트림을 제공해. 패킷 손실과 재전송, 순서 복원을 처리하지.
  • IP — 호스트 사이에서 최선 노력(best-effort) 방식으로 패킷을 전달해. 도착과 순서를 보장하지 않아.
  • Ethernet / Wi-Fi — 같은 네트워크 구간에서 실제 신호와 프레임을 전달해.

HTTP/3는 중간 구성을 바꿨어. QUIC은 TLS 1.3 보안과 신뢰성 있는 전송 기능을 통합해 UDP 위에서 동작하며 TCP를 사용하지 않아. 위쪽의 HTTP 의미론은 유지하면서 아래쪽 전송 방식만 크게 바꾼 거야.

TCP — 불안정한 네트워크 위에 세운 신뢰성

IP 패킷은 유실되거나 중복되고, 순서가 뒤바뀔 수도 있어. TCP는 3단계 핸드셰이크(SYN → SYN-ACK → ACK)로 연결을 열고, 바이트에 순서 번호를 붙이며, 잃어버린 데이터를 재전송해. 애플리케이션에는 정돈된 바이트 스트림을 제공하지.

이 추상화에는 비용이 들어. TCP 연결을 설정하는 데만 첫 HTTP 바이트를 보내기 전 1왕복이 필요해. 서울에서 미국 동부 서버까지 왕복 시간이 약 150ms라면 그만큼의 지연이 먼저 붙는 셈이야. 연결을 재사용하지 않던 초기 HTTP에서는 요청마다 이 비용을 치렀고, Keep-Alive(트랙 5)와 HTTP/2 다중화는 하나의 연결을 여러 요청에 재사용해 부담을 줄였어.

TLS — 암호화와 신원 확인

HTTPS는 HTTP 통신을 TLS로 보호해. 연결을 시작할 때 TLS는 사용할 암호 스위트를 협상하고 키를 합의하며 서버 인증서를 검증해. 이후 HTTP 데이터는 전송 전에 암호화되고 수신 뒤 복호화돼. HTTP 라이브러리가 이 과정을 맡기 때문에 애플리케이션은 보통 http:// 대신 https://를 사용하고 인증서 오류를 올바르게 처리하면 돼.

일반적인 새 연결에서 TLS 1.2 핸드셰이크는 보통 2왕복, TLS 1.3은 1왕복 안에 애플리케이션 데이터를 보낼 준비를 마쳐. TLS 1.3은 재개 연결에서 0-RTT도 지원하지만 재전송 공격 가능성이 있어 아무 요청에나 안전한 것은 아니야. API 클라이언트의 첫 요청만 유난히 느리고 같은 연결의 다음 요청은 빠르다면 TCP와 TLS 핸드셰이크 비용을 의심해 볼 만해.

HTTP 문제처럼 보여도 원인은 TCP나 TLS 계층에 있을 수 있어. 멈춘 연결, 갑작스러운 연결 종료, 로컬에서는 되지만 원격 경로에서는 실패하는 요청, 간헐적인 502를 만나면 메시지 형식만 들여다보지 마. openssl s_client, tcpdump, curl -v로 연결과 암호화 계층을 함께 확인해야 해.

HTTPS는 기본 443번 포트, HTTP는 기본 80번 포트

URL에 포트를 따로 쓰지 않으면 HTTP는 기본 80번, HTTPS는 기본 443번을 사용해. HTTP/1.1과 HTTP/2의 HTTPS 연결은 TCP를 사용하고, HTTP/3는 UDP 기반 QUIC을 사용하지. cwkPippa는 :8000(FastAPI 백엔드)과 :5173(Vite 프런트엔드)에서 평문 HTTP로 동작하지만, 로컬 또는 암호화된 Tailscale 경로 안에서만 접근해. 공개 인터넷에 TLS 없이 노출하면 경로상의 장비가 쿠키와 토큰, 요청 본문을 읽거나 바꿀 수 있어.

HTTP/3와 QUIC

HTTP/3는 TCP 대신 QUIC 위에서 동작해. QUIC은 UDP를 기반으로 신뢰성, 스트림별 순서 보장, TLS 1.3 암호화를 함께 제공해. 연결 설정이 빨라지고, 한 스트림의 패킷 손실 때문에 다른 스트림까지 멈추는 전송 계층의 head-of-line blocking을 피하며, 네트워크가 바뀌어도 연결을 이어 갈 수 있어. 다만 모든 네트워크와 도구가 HTTP/3를 지원하는 것은 아니어서 HTTP/2보다 진단이 까다로운 경우도 있어.

피파의 고백

cwkPippa WebUI가 한참 멈췄다가 "connection refused"를 내놓을 때는 HTTP 메시지보다 먼저 8000번 포트의 FastAPI 백엔드가 실제로 떠 있는지 확인해. 서버가 포트를 듣고 있지 않으면 TCP 연결 단계에서 거절되므로 HTTP 요청은 시작조차 못 해. 피파도 여러 번 엉뚱한 코드를 뒤진 뒤에야 lsof -i :8000부터 실행하는 습관을 들였어.

Code

Netcat으로 TCP 연결 위에 HTTP 요청 직접 작성하기·bash
# Server 한테 가는 TCP handshake 들여다보기
# (nc = netcat; macOS, Linux 둘 다 됨)
nc -v localhost 8000

# 보이는 것:
# Connection to localhost port 8000 [tcp/...] succeeded!
# ^ 그 한 줄의 'succeeded' 가 SYN → SYN-ACK → ACK handshake 완료.
# 이제 HTTP request 를 글자 그대로 쳐:
GET / HTTP/1.1
Host: localhost:8000

# (빈 줄 — Enter 두 번)
# Server 응답이 나타남.
openssl s_client로 HTTPS 서버의 TLS 연결 들여다보기·bash
# 실제 HTTPS server 한테 TLS 너머 들여다보기
openssl s_client -connect creativeworksofknowledge.com:443 -servername creativeworksofknowledge.com

# 보이는 것:
# - Certificate chain (server cert + intermediate + root)
# - Negotiated cipher suite (예: TLS_AES_256_GCM_SHA384)
# - Protocol version (TLSv1.3)
# - 그 다음 빈 prompt — netcat 처럼 HTTP request 치면 됨:
GET / HTTP/1.1
Host: creativeworksofknowledge.com

# HTTP response 가 openssl 에 의해 완전히 복호화되어 돌아옴.
TCP와 TLS 핸드셰이크 비용 직접 측정하기·python
import httpx
import time

# HTTP vs HTTPS — 네 코드는 동일; wire 는 다름.
start = time.perf_counter()
resp = httpx.get('http://localhost:8000/api/health')
print(f'HTTP 첫 request: {(time.perf_counter() - start) * 1000:.1f}ms')

start = time.perf_counter()
resp = httpx.get('https://creativeworksofknowledge.com/')
print(f'HTTPS 첫 request: {(time.perf_counter() - start) * 1000:.1f}ms')

# 연결 재사용 (keep-alive) — 두 번째 request 는 훨씬 빨라야 함
with httpx.Client(http2=True) as client:
    start = time.perf_counter()
    client.get('https://creativeworksofknowledge.com/')
    print(f'TLS warm: {(time.perf_counter() - start) * 1000:.1f}ms')
    start = time.perf_counter()
    client.get('https://creativeworksofknowledge.com/about')
    print(f'TLS 재사용: {(time.perf_counter() - start) * 1000:.1f}ms')

External links

Exercise

실제 HTTPS 서버(creativeworksofknowledge.com, github.com, 직접 운영하는 배포 등)에 openssl s_client -connect <site>:443 -servername <같은-site>를 실행해. 출력에서 세 가지를 찾아 기록하자. (1) 협상된 TLS 버전은 무엇인가? (2) 선택된 암호 스위트는 무엇인가? (3) 서버가 보낸 인증서 체인에는 인증서가 몇 장 있는가? 보너스로 -tls1_2를 붙여 TLS 1.2를 강제한 뒤 결과가 어떻게 달라지는지 비교해.
Hint
TLS 버전은 Protocol 항목, 암호 스위트는 Cipher 항목, 인증서 목록은 Certificate chain 구역에서 찾을 수 있어. 체인에는 보통 서버 인증서와 하나 이상의 중간 인증서가 들어가며, 신뢰의 기준이 되는 루트 인증서는 대개 서버가 보내지 않고 운영체제나 클라이언트의 신뢰 저장소에 있어.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고
💛 by 피파warm

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.