한입 CS

네트워크: 주소창에서 첫 바이트까지

DNS, TCP, TLS, 연결 재사용(connection reuse), preconnect를 왕복 횟수로 세기

6장 · 멀리 다녀오기 · 읽는 시간 약 18분

목차 · 진행 0%

    쇼핑몰 첫 화면이 느리다는 신고가 들어왔다. 서버 로그를 보면 응답을 만드는 데 30ms면 끝난다. 그런데 해외 사용자의 브라우저 개발자 도구에서는 첫 바이트가 오기까지 1초 가까이 걸린다. 서버가 일을 시작하기도 전에 시간이 흘러가고 있다.

    웹 성능 지표에서는 이 시간을 TTFB(Time to First Byte)라고 부른다. 페이지 이동을 시작한 때부터 응답의 첫 바이트가 도착할 때까지의 시간이다. 그 안에는 리다이렉트, 서비스 워커(service worker) 시작, DNS 조회, 연결과 TLS 협상, 요청 전송과 첫 바이트 수신이 들어 있다(web.dev: TTFB). 서버의 30ms는 그중 마지막 조각일 뿐이다. 이 장은 나머지 조각을 왕복 횟수로 센다.

    왕복 시간(RTT, round-trip time)은 패킷 하나가 상대에게 갔다가 답이 돌아오기까지의 시간이다. 거리와 경로가 정하는 값이라 코드로 줄일 수 없다. 코드로 줄일 수 있는 것은 몇 번 왕복하느냐다.

    DNS: 이름을 주소로 바꾸기

    브라우저는 shop.example.com이라는 이름으로는 연결할 수 없다. 먼저 IP 주소를 알아야 한다. 이름을 주소로 바꿔 주는 체계가 DNS(Domain Name System)다. 브라우저는 자기 캐시, 운영체제의 캐시를 차례로 보고, 없으면 DNS 리졸버(대개 통신사나 공용 DNS 서버)에 묻는다. 리졸버도 모르면 루트 서버(DNS 체계의 맨 위 서버)부터 차례로 물어 답을 찾는다. 받은 답은 정해진 유효 시간(TTL, time to live) 동안 각 층에 캐시된다.

    그래서 DNS 비용은 한결같지 않다. 캐시에 있으면 0, 리졸버가 알고 있으면 리졸버까지 한 번 왕복, 리졸버도 모르면 여러 번 왕복한다. 이 장의 모델은 가운데 경우, 리졸버까지 한 번 왕복으로 친다. DNS의 층과 레코드 종류는 DNS 노트에, 운영체제가 DNS보다 먼저 보는 파일은 hosts 파일 노트에 있다.

    TCP와 TLS: 연결에 드는 왕복

    주소를 알았으면 연결을 맺는다. TCP는 3방향 핸드셰이크(three-way handshake)로 연결을 연다(RFC 9293 §3.5). 클라이언트가 SYN을 보내고, 서버가 SYN과 ACK를 함께 돌려주고, 클라이언트가 ACK를 보낸다(SYN = 연결 요청, ACK = 받았다는 확인). 마지막 ACK는 보내 놓고 기다리지 않으니, 요청을 보낼 수 있을 때까지 한 번 왕복이 든다.

    방향메시지왕복으로 세면
    클라이언트 → 서버SYN(연결 요청)가는 길
    서버 → 클라이언트SYN + ACK(요청 받았다, 서버도 연결 요청)오는 길. 여기까지 왕복 1번
    클라이언트 → 서버ACK(받았다)첫 요청과 함께 나간다. 기다리지 않는다

    HTTPS라면 그 위에서 TLS 핸드셰이크로 암호화 키를 맞춘다. 결론부터 적으면 TLS 1.2는 두 번 왕복, TLS 1.3은 한 번 왕복이다. 메시지 이름은 외울 필요 없고, 왕복이 어디서 끝나는지만 보자.

    버전왕복무엇을 주고받나
    TLS 1.22번1번째: 클라이언트의 ClientHello → 서버의 ServerHelloDone. 2번째: 키 교환(key exchange) 값과 Finished → 서버의 Finished. 그 뒤에야 애플리케이션 데이터가 오간다(RFC 5246 §7.3)
    TLS 1.31번첫 메시지에 키 교환 값(key_share)을 미리 실어 보내서, 서버의 Finished가 첫 응답에 함께 온다. 클라이언트는 한 번 왕복 뒤에 바로 요청을 보낸다(RFC 8446 §2)

    연결이 됐어도 끝이 아니다. 요청을 보내고 첫 바이트를 받는 데 왕복 한 번이 더 든다. TLS 1.2면 TCP 1 + TLS 2 + 요청 1, 서버까지 네 번 왕복한다.

    예측먼저 답해 보세요

    서버까지 RTT 80ms, DNS 리졸버까지 20ms, TLS 1.2다. DNS 캐시도 열린 연결도 없을 때 /의 첫 바이트는 몇 ms에 올까? (서버 처리 시간은 0으로 친다)

    선택지와 해설 보기

    TLS 1.2에서 /의 첫 바이트는 340ms다. 서버까지 네 번 왕복했고, 그중 세 번이 연결을 맺는 데 들었다. TLS 1.3으로 바꾸면 한 번이 줄어 260ms다.

    TLS 1.3에는 한 번 연결했던 서버라면 첫 메시지에 데이터를 실어 보내는 0-RTT 모드도 있다(RFC 8446 §2.3). 대신 명세는 0-RTT 데이터에는 같은 요청을 다시 보내는 공격(replay)을 막는 보장이 없다고 경고한다. 공격자가 보낸 두 번째 요청을 서버가 구별하지 못할 수 있다는 뜻이다. 그래서 다시 실행돼도 결과가 같은 요청, 곧 멱등(idempotent) 요청(조회 같은 것)에만 쓰는 것이 안전하다. HTTPS가 무엇을 지키는지는 HTTPS 노트에 있다.

    연결 재사용과 CDN: 왕복을 없애거나 줄이기

    왕복을 가장 많이 줄이는 방법은 연결을 다시 쓰는 것이다. HTTP/1.1은 기본으로 지속 연결(persistent connection)을 써서, 연결 하나로 요청과 응답을 여러 번 주고받는다(RFC 9112 §9.3). 열린 연결이 있으면 DNS·TCP·TLS를 모두 건너뛰고 요청 한 번 왕복만 남는다.

    실험 RTT로 그리는 요청 두 번의 워터폴

    아래 모델은 같은 서버에 /와 /app.js를 차례로 요청한다. RTT와 설정을 바꿔 보자.

    실험RTT로 그리는 요청 두 번의 워터폴

    같은 서버에 /와 /app.js를 차례로 요청합니다. DNS 리졸버까지는 20ms 왕복으로 고정하고, 서버까지의 RTT를 바꿉니다. 서버 처리 시간은 0으로 칩니다. '연결 재사용'은 첫 응답 뒤 연결을 열어 두고 둘째 요청에 다시 쓰는지입니다. 실제로 잰 값이 아니라 왕복 횟수만 세는 모델입니다.

    TLS
    DNS 캐시
    연결 재사용
    DNS 조회TCP 연결TLS 1.2 핸드셰이크요청 → 첫 바이트
    /420ms
    DNS 20 + TCP 100 + TLS 200 + 요청 100
    /app.js100ms
    요청 100 (건너뜀: DNS 기억함, 연결 재사용)

    첫 요청 420ms(서버까지 4번 왕복, 그중 3번이 연결), 둘째 요청 100ms입니다.

    • 해볼 것
    • RTT 300ms, TLS 1.2, DNS 캐시 없이 첫 요청이 1초를 넘는지 본다
    • TLS 1.3으로 바꿔 몇 ms 줄어드는지 본다
    • 연결 재사용을 끄고 둘째 요청이 얼마나 느려지는지 본다

    RTT를 80ms로 두면 2절의 숫자가 나온다.

    RTT 80ms, TLS 1.2에서 /는 340ms, 같은 서버에 이어 보낸 /app.js는 같은 연결을 다시 써서 80ms다. 연결 재사용을 끄면(응답마다 연결을 닫으면) /app.js도 TCP와 TLS를 다시 해서 320ms가 된다(TLS 1.3이면 240ms). DNS는 첫 조회 결과를 기억해 두니 둘째 요청에서는 다시 묻지 않는다.

    RTT 300ms, TLS 1.2, DNS 캐시가 없으면 첫 요청의 첫 바이트까지 1,220ms다. 같은 조건에서 TLS 1.3이면 920ms다. 연결을 재사용한 둘째 요청은 300ms다. RTT가 클수록 왕복 한 번을 줄이는 효과가 커진다. 해외 사용자의 첫 화면이 유독 느린 이유가 여기 있다. 같은 횟수를 왕복해도 한 번 왕복에 드는 시간이 크다.

    왕복 횟수를 못 줄인다면 한 번 왕복에 드는 시간을 줄인다. CDN(content delivery network)은 사용자와 가까운 곳에 서버(엣지)를 두고 그곳에서 연결을 받는다. TCP와 TLS 핸드셰이크가 가까운 엣지와 오가니 RTT가 작아진다. 엣지가 응답을 캐시해 두었다면 멀리 있는 원래 서버까지 가지도 않는다. 자세한 구조는 CDN 노트에 있다.

    preconnect와 dns-prefetch: 연결을 미리 열기

    페이지는 대개 여러 출처(origin)에서 리소스를 받는다. 웹 폰트, 이미지 CDN, API 서버가 다른 도메인에 있으면 출처마다 DNS·TCP·TLS를 따로 해야 한다. 문제는 그 연결이 리소스가 필요해진 순간에야 시작된다는 것이다. CSS를 다 받고 나서야 폰트 주소를 알게 되고, 그제야 폰트 서버와 연결을 맺는다.

    <link rel="preconnect">는 이 출처에 곧 요청할 테니 연결을 미리 열어 두라는 힌트다. HTTPS 출처라면 DNS·TCP·TLS까지 미리 해 둔다. 아끼는 시간은 그 출처의 연결 준비 시간과 "힌트를 읽은 뒤 리소스가 필요해지기까지의 여유" 중 작은 쪽이다. 여유가 넉넉하면 연결 준비 전체(TLS 1.3이면 DNS를 빼고도 왕복 2번)를 아끼고, 리소스가 힌트 직후에 필요해지면 미리 시작한 몇십 ms만 아낀다. 같은 출처에는 이미 연결이 열려 있으니 효과가 없다. 그리고 여러 외부 출처를 모두 미리 연결하면 오히려 손해일 수 있어서, MDN은 가장 중요한 몇 곳에만 쓰라고 권한다(MDN: rel=preconnect). 나머지 출처에는 DNS 조회만 미리 하는 <link rel="dns-prefetch">를 쓴다(MDN: dns-prefetch). 브라우저가 미리 받는 힌트 전체는 리소스 힌트 노트에 있다.

    함정 찾기 AI가 짠 레이아웃과 첫 화면 데이터 로딩, 어디서 왕복이 새고 있을까?
    함정 찾기AI가 짠 레이아웃과 첫 화면 데이터 로딩, 어디서 왕복이 새고 있을까?
    내가 AI에게 한 요청
    “shop.example.com 쇼핑몰의 공통 레이아웃에 연결 최적화를 넣고, 홈 화면에 필요한 사용자·공지·배너 데이터를 불러오는 함수, 세일 페이지 링크를 만들어 줘.”

    의심스러운 줄을 모두 눌러 표시한 뒤 “검토 끝”을 누르세요. 함정은 4개입니다.

    확인 문제 장을 덮기 전에 떠올려 보기
    확인 문제장을 덮기 전에 떠올려 보기

    1. 새 HTTPS 연결에서 요청을 보내기 전에 드는 왕복은? (DNS는 이미 캐시되어 있다)

    2. 페이지와 같은 출처에 <link rel="preconnect">를 거는 것은?

    3. 해외 사용자의 첫 바이트가 유독 늦을 때, 코드를 고치지 않고 RTT 자체를 줄이는 방법은?

    더 풀어 보기(선택) · preconnect로 몇 ms를 아낄까
    보너스 문제preconnect로 몇 ms를 아낄까

    HTML의 <head>를 100ms에 읽는다. 다른 출처 세 곳의 리소스가 페이지를 그리는 도중에 각각 필요해진다. 출처마다 RTT와 리소스가 필요해지는 시각이 다르다. 연결 준비(DNS + TCP + TLS 1.3)는 3번 왕복, 곧 RTT의 3배로 친다(계산을 줄이려고 DNS도 리졸버 20ms가 아니라 그 출처의 RTT 한 번으로 셈한다). 요청부터 첫 바이트까지는 1번 왕복으로 친다. 세 출처 모두에 preconnect를 걸면 출처마다 첫 바이트가 얼마나 앞당겨질지, 합해서 몇 ms를 아끼는지 계산하는 코드를 짜 보자.

    출처  RTT  필요해지는 시각
    fonts  60ms  400ms
    cdn    40ms  250ms
    api   120ms  600ms
    → 아낀 시간 합 660ms
    풀 때 쓰는 것: 『개발 수학』 Unit 3. 증가 속도: n, n log n, n², 2ⁿ (preconnect 계산이 O(출처 수))

    이 장을 언급한 노트

    다음7장. HTTP/1.1에서 HTTP/3까지: 무엇을 고쳤나연결 하나 위에서 요청 여러 개가 어떻게 오가는지, HTTP/1.1에서 HTTP/3까지 무엇을 고쳤는지 봅니다.

    『한입 CS』 6장. 네트워크: 주소창에서 첫 바이트까지 · younggeun0. Claude와 함께 썼고, 저자가 아직 전체 내용을 검수하지 않은 초안입니다. 사실 주장은 공식 문서로, 예제 코드는 실제로 실행해 확인했습니다.

    보기 옵션