웹사이트 주소를 브라우저에 입력하면 페이지가 뜬다. 당연한 일이다.
하지만 그 사이에 뭔가 일어난다. 도메인 이름을 IP 주소로 바꾸는 과정. DNS 쿼리다.
이 과정이 생각보다 복잡하다. 그리고 느리다.
왜 느린지, 어떻게 동작하는지 알아보자.
브라우저에 google.com을 입력하면 무슨 일이 일어날까?
대부분은 이렇게 생각한다. “도메인을 IP로 바꾸고, 그 IP로 요청 보내면 되지?”
맞다. 하지만 그 과정이 단순하지 않다.
┌─────────┐
│ 브라우저 │
└────┬────┘
│ "google.com의 IP 주소가 뭐야?"
▼
┌─────────┐
│ DNS서버 │
└────┬────┘
│ "8.8.8.8이야"
▼
┌─────────┐
│ 브라우저 │
└────┬────┘
│ HTTP 요청
▼
┌─────────┐
│ 서버 │
└────┬────┘
│ HTML 응답
▼
이게 전부가 아니다. 실제로는 더 복잡하다.
DNS 쿼리는 두 가지 방식이 있다.
클라이언트가 DNS 서버에게 “알아서 찾아줘”라고 요청하는 방식이다.
┌──────────┐
│ 클라이언트 │
└─────┬─────┘
│ "google.com의 IP 주소 알려줘"
▼
┌──────────┐ ┌──────┐ ┌──────┐ ┌──────┐
│ DNS서버 │─────▶│루트서버│─▶│.com서버│─▶│google│
│(Resolver) │ └──────┘ └──────┘ └──────┘
└─────┬─────┘
│ "8.8.8.8이야"
▼
┌──────────┐
│ 클라이언트 │
└──────────┘
클라이언트는 한 번만 물어본다. DNS 서버가 알아서 찾아준다.
DNS 서버가 직접 루트 서버부터 차례로 물어보는 방식이다.
┌──────────┐
│ DNS서버 │
└─────┬─────┘
│ ① ".com 서버 어디 있어?"
▼
┌──────────┐
│ 루트서버 │
└─────┬─────┘
│ ② ".com 서버는 여기 있어"
▼
┌──────────┐
│ DNS서버 │
└─────┬─────┘
│ ③ "google.com 서버 어디 있어?"
▼
┌──────────┐
│ .com서버 │
└─────┬─────┘
│ ④ "google.com 서버는 여기 있어"
▼
┌──────────┐
│ DNS서버 │
└─────┬─────┘
│ ⑤ "IP 주소 뭐야?"
▼
┌──────────┐
│google서버 │
└─────┬─────┘
│ ⑥ "8.8.8.8이야"
▼
여러 번 왕복한다. 그래서 느리다.
브라우저에 www.google.com을 입력하면 무슨 일이 일어날까?
전체 과정을 그림으로 보면 이렇다.
┌──────────┐
│ 브라우저 │
└─────┬─────┘
│ ① 로컬 캐시 확인
│
├─[캐시 있음]─▶ 바로 사용 (0ms)
│
└─[캐시 없음]─▶ ② OS 캐시 확인
│
├─[캐시 있음]─▶ 바로 사용 (1-2ms)
│
└─[캐시 없음]─▶ ③ Resolver 서버
│
▼
┌─────────────┐
│ 루트 서버 │
└──────┬──────┘
│
▼
┌─────────────┐
│ .com 서버 │
└──────┬──────┘
│
▼
┌─────────────┐
│google.com서버│
└──────┬──────┘
│
▼
IP 주소 반환 (150ms)
브라우저는 먼저 자신의 캐시를 확인한다. 이전에 조회한 적이 있으면 캐시에서 바로 가져온다.
┌──────────┐
│ 브라우저 │
│ 캐시 │
└─────┬────┘
│
├─[있음]─▶ 바로 사용 (0ms)
│
└─[없음]─▶ 다음 단계
캐시가 있으면 DNS 쿼리 자체가 발생하지 않는다. 가장 빠르다.
브라우저 캐시에 없으면 OS의 DNS 캐시를 확인한다.
Windows는 ipconfig /displaydns로 확인할 수 있다.
OS 캐시 → 있으면? 바로 사용 (1-2ms)
→ 없으면? 다음 단계
OS 캐시에도 없으면 Resolver 서버에 요청한다. 보통 ISP가 제공하는 DNS 서버다.
클라이언트 → Resolver: "www.google.com의 IP 주소 알려줘"
Resolver 서버는 Recursive Query를 수행한다. 클라이언트는 기다린다.
Resolver는 루트 서버부터 시작한다.
Resolver → 루트 서버: ".com의 네임서버 어디 있어?"
루트 서버 → Resolver: ".com 네임서버는 a.gtld-servers.net이야"
루트 서버는 13개가 있다. 전 세계에 분산되어 있다.
.com 서버를 찾았다. 이제 .com 서버에게 물어본다.
Resolver → .com 서버: "google.com의 네임서버 어디 있어?"
.com 서버 → Resolver: "google.com 네임서버는 ns1.google.com이야"
TLD는 Top-Level Domain이다. .com, .net, .org 같은 것들.
마지막으로 google.com의 네임서버에게 물어본다.
Resolver → ns1.google.com: "www.google.com의 IP 주소 뭐야?"
ns1.google.com → Resolver: "172.217.31.196이야"
이제 IP 주소를 얻었다.
Resolver는 클라이언트에게 IP 주소를 알려준다.
Resolver → 클라이언트: "www.google.com은 172.217.31.196이야"
이제 클라이언트는 IP 주소를 알았으니 HTTP 요청을 보낼 수 있다.
왜 이렇게 여러 서버를 거쳐야 할까?
DNS는 분산 구조다. 하나의 서버에 모든 도메인 정보를 저장하지 않는다.
루트 서버: TLD 정보만 저장 (.com, .net 등)
TLD 서버: 도메인 네임서버 정보만 저장 (google.com의 네임서버)
Authoritative 서버: 실제 IP 주소 저장
이렇게 나누는 이유는 부하 분산과 확장성 때문이다.
모든 도메인을 하나의 서버에 저장하면? 서버가 터진다. 전 세계의 모든 도메인 조회를 처리해야 한다.
계층 구조를 쓰면 각 서버가 자신의 영역만 관리하면 된다.
루트 서버: TLD 정보만 관리
.com 서버: .com 도메인들의 네임서버 정보만 관리
google.com 서버: google.com의 IP 주소만 관리
각 서버의 부담이 줄어든다.
DNS 쿼리가 느린 이유는 여러 단계를 거치기 때문이다.
각 단계마다 네트워크 왕복 시간이 필요하다.
루트 서버 조회: 50ms (한국 → 미국)
.com 서버 조회: 50ms (한국 → 미국)
google.com 서버 조회: 50ms (한국 → 미국)
총: 150ms
각 단계가 순차적으로 진행되면 시간이 누적된다.
캐시가 없으면 매번 이 과정을 거쳐야 한다.
첫 번째 요청: 150ms (캐시 없음)
두 번째 요청: 0ms (캐시 있음)
첫 요청이 느린 이유다.
DNS 캐싱이 얼마나 중요한지 보자.
DNS 레코드에는 TTL이 있다. 이 시간 동안 캐시에 저장된다.
google.com A 172.217.31.196 TTL 300
TTL이 300초면, 300초 동안 캐시에 저장된다.
DNS 캐시는 여러 계층에 있다.
1. 브라우저 캐시 (가장 빠름)
2. OS 캐시
3. Resolver 캐시
4. 각 DNS 서버의 캐시
각 계층에서 캐시를 확인한다. 상위 계층에 캐시가 있으면 하위 계층까지 가지 않는다.
DNS 쿼리 시간을 실제로 측정해보자.
$ dig google.com
; <<>> DiG 9.16.1 <<>> google.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 12345
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; QUESTION SECTION:
;google.com. IN A
;; ANSWER SECTION:
google.com. 300 IN A 172.217.31.196
;; Query time: 45 msec
;; SERVER: 8.8.8.8#53(8.8.8.8)
;; WHEN: Mon Dec 22 10:00:00 KST 2025
;; MSG SIZE rcvd: 55
Query time이 45ms다. 캐시가 있을 때는 더 빠르다.
$ nslookup google.com
Server: 8.8.8.8
Address: 8.8.8.8#53
Non-authoritative answer:
Name: google.com
Address: 172.217.31.196
Non-authoritative answer는 캐시에서 가져온 답변이다.
DNS 쿼리를 빠르게 만드는 방법들.
기본 DNS 서버보다 빠른 서버를 쓰면 빨라진다.
기본 DNS (ISP): 50ms
Google DNS (8.8.8.8): 20ms
Cloudflare DNS (1.1.1.1): 15ms
가까운 DNS 서버를 쓰면 빨라진다.
HTML에 DNS Prefetch를 추가하면 미리 조회한다.
<link rel="dns-prefetch" href="//fonts.googleapis.com">
페이지 로드 전에 DNS를 미리 조회한다.
DNS 쿼리를 HTTPS로 암호화해서 보낸다. 보안뿐만 아니라 성능도 개선될 수 있다.
일반 DNS: UDP로 전송
DoH: HTTPS로 전송 (캐싱 최적화 가능)
CDN을 쓰면 DNS 조회도 최적화된다.
일반 서버: 한국 → 미국 서버 (150ms)
CDN: 한국 → 한국 CDN 노드 (10ms)
가까운 CDN 노드를 쓰면 DNS 조회도 빨라진다.
실제 DNS 쿼리가 어떻게 진행되는지 로그로 확인해보자.
$ sudo tcpdump -i any -n port 53
10:00:00.123456 IP 192.168.1.100.54321 > 8.8.8.8.53:
UDP, length 32: 12345+ A? google.com. (28)
10:00:00.145678 IP 8.8.8.8.53 > 192.168.1.100.54321:
UDP, length 48: 12345 1/0/0 A 172.217.31.196 (44)
첫 번째 패킷은 질의, 두 번째 패킷은 응답이다.
Wireshark로 DNS 패킷을 자세히 볼 수 있다.
DNS Query:
Transaction ID: 0x1234
Questions: 1
Question: google.com, type A, class IN
DNS Response:
Transaction ID: 0x1234
Answers: 1
Answer: google.com, type A, class IN, TTL 300,
Address: 172.217.31.196
패킷 구조를 직접 볼 수 있다.
DNS 쿼리가 실패하는 경우들.
도메인의 네임서버가 다운되면 조회가 실패한다.
Resolver → ns1.example.com: "www.example.com의 IP 주소 뭐야?"
ns1.example.com: (응답 없음 - 서버 다운)
TTL이 만료되어 다시 조회했는데 서버가 다운된 경우.
캐시: (TTL 만료)
Resolver → ns1.example.com: (서버 다운)
존재하지 않는 도메인을 조회하면 NXDOMAIN을 받는다.
Resolver → 루트 서버: "nonexistent.xyz 어디 있어?"
루트 서버 → Resolver: "없어요 (NXDOMAIN)"
DNS 쿼리는 생각보다 복잡하다. 여러 서버를 거치고, 여러 단계를 거친다.
그래서 느리다. 하지만 캐싱 덕분에 대부분의 경우 빠르다.
DNS를 이해하면 웹 성능 최적화도 쉬워진다. DNS Prefetch, CDN 선택, DNS 서버 선택. 모두 DNS를 이해해야 한다.
다음에 웹사이트가 느리다고 느낄 때, DNS 쿼리 시간도 확인해보자. 생각보다 많은 시간을 차지할 수 있다.