우리는 HTTP 타이밍을 어떻게 측정하는가: DNS, TCP, TLS, TTFB 분석
Source: Dev.to

웹사이트의 응답 시간을 확인할 때 “847 ms”라고만 봐서는 별다른 정보를 얻을 수 없습니다. DNS 조회가 느린 건가요? 네트워크 지연인가요? 아니면 서버 자체 때문인가요? 세부적인 분석이 필요합니다.
**upsonar.io**에서는 시간이 정확히 어디에 소요되는지 보여줍니다. Go의 net/http/httptrace 패키지를 사용해 이를 구현하는 방법은 다음과 같습니다.
문제
start := time.Now()
resp, _ := http.Get("https://example.com")
fmt.Println(time.Since(start)) // 847ms
847 ms – 하지만 왜일까요? 요청 라이프사이클의 어느 부분일 수도 있습니다.
해결책
httptrace는 HTTP 요청의 모든 단계에 훅을 연결합니다. 콜백을 연결하면 각 단계가 완료될 때마다 실행됩니다:
trace := &httptrace.ClientTrace{
DNSStart: func(_ httptrace.DNSStartInfo) {
dnsStart = time.Now()
},
DNSDone: func(_ httptrace.DNSDoneInfo) {
fmt.Printf("DNS: %v\n", time.Since(dnsStart))
},
// same pattern for TCP, TLS, etc.
}
req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
각 지표가 의미하는 바
- DNS Lookup – 도메인을 IP 주소로 해석합니다. > 100 ms는 DNS 서버 문제나 캐시가 비어 있는 경우를 나타냅니다.
- TCP Connect – 3‑way 핸드셰이크. 주로 서버와의 물리적 거리 영향을 받습니다.
- TLS Handshake – 암호화를 협상합니다. 일반적인 값은 50–150 ms이며, > 300 ms는 서버 또는 인증서 체인 문제를 시사합니다.
- TTFB (Time To First Byte) – DNS, TCP, TLS 및 서버 처리 시간을 포함합니다. 응답 속도를 측정하는 표준 지표입니다.
- Transfer – 응답 본문을 다운로드합니다.
작동 예제
package main
import (
"crypto/tls"
"fmt"
"io"
"net/http"
"net/http/httptrace"
"os"
"time"
)
func main() {
url := "https://httpbin.org/delay/2"
if len(os.Args) > 1 {
url = os.Args[1]
}
var dnsStart, tcpStart, tlsStart time.Time
totalStart := time.Now()
trace := &httptrace.ClientTrace{
DNSStart: func(_ httptrace.DNSStartInfo) { dnsStart = time.Now() },
DNSDone: func(_ httptrace.DNSDoneInfo) { fmt.Printf("DNS: %v\n", time.Since(dnsStart)) },
ConnectStart: func(_, _ string) { tcpStart = time.Now() },
ConnectDone: func(_, _ string, _ error) { fmt.Printf("TCP: %v\n", time.Since(tcpStart)) },
TLSHandshakeStart: func() { tlsStart = time.Now() },
TLSHandshakeDone: func(_ tls.ConnectionState, _ error) { fmt.Printf("TLS: %v\n", time.Since(tlsStart)) },
GotFirstResponseByte: func() { fmt.Printf("TTFB: %v\n", time.Since(totalStart)) },
}
req, _ := http.NewRequest("GET", url, nil)
req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
client := &http.Client{Transport: &http.Transport{DisableKeepAlives: true}}
resp, err := client.Do(req)
if err != nil {
fmt.Println("Error:", err)
return
}
defer resp.Body.Close()
transferStart := time.Now()
io.ReadAll(resp.Body)
fmt.Printf("Transfer: %v\n", time.Since(transferStart))
fmt.Printf("Total: %v\n", time.Since(totalStart))
}
프로그램 실행:
go run main.go https://httpbin.org/delay/2
샘플 출력
DNS: 148.00525ms
TCP: 142.56925ms
TLS: 289.685ms
TTFB: 2.956339583s
Transfer: 72.208µs
Total: 2.95670575s
TTFB는 첫 번째 바이트가 도착하기까지 걸린 전체 시간이며, Transfer는 본문을 다운로드하는 데 걸린 시간입니다—여기서는 본문이 작아 전체 시간은 거의 TTFB와 같습니다.
더 큰 응답 예시
go run main.go https://proof.ovh.net/files/10Mb.dat
샘플 출력
DNS: 3.621333ms
TCP: 54.364208ms
TLS: 116.879041ms
TTFB: 286.073291ms
Transfer: 14.221007833s
Total: 14.507351083s
10 MB 파일의 경우 Transfer가 전체 지연 시간의 대부분을 차지합니다.
연결 재사용에 주의하세요
Go의 http.Client는 기본적으로 연결을 재사용합니다. 동일한 호스트에 대한 이후 요청은 DNS, TCP 및 TLS에 대해 0 ms가 표시되는데, 이는 연결이 이미 설정되어 있기 때문입니다.
정확한 측정을 위해 keep‑alives를 비활성화하세요:
client := &http.Client{
Transport: &http.Transport{
DisableKeepAlives: true,
},
}
느린 수치가 알려주는 것
- DNS > 100 ms → 더 빠른 DNS 제공업체를 사용해 보세요(예: Cloudflare, Google).
- TCP > 100 ms → 서버가 사용자와 거리가 멉니다; CDN 도입을 고려하세요.
- TLS > 200 ms → 인증서 체인을 확인하고 세션 재개를 활성화하세요.
- TTFB > 500 ms → 백엔드 병목 현상: 느린 DB, 콜드 스타트, 무거운 처리.
- Transfer high → 큰 페이로드; gzip/Brotli 압축을 활성화하세요.
200 ms 규칙
사용자는 약 200 ms를 초과하는 지연을 감지합니다. TTFB만으로도 이 임계값을 초과하면 프런트엔드 최적화와 관계없이 페이지가 느리게 느껴집니다.
무료이며 회원가입이 필요 없는 도구로 직접 사이트에서 시도해 보세요: upsonar.io/tools/diagnose.