본문으로 바로가기
Lab

DNS 작동 원리, 맥 터미널로 루트 서버부터 따라가 보니

DNS가 도메인을 IP 주소로 바꾸는 과정을 맥 터미널에서 따라간 기록입니다. 루트 188 ms, com 187 ms, Cloudflare 9 ms 응답과 결과를 읽는 법, HTTPS 조회, TCP 재시도까지 확인합니다.

·

나무 모양으로 이어진 서버들을 따라 내려가 이름표가 숫자 주소 카드로 바뀌는 개념 그림
DNS 작동 원리, 맥 터미널로 루트 서버부터 따라가 보니 표지 삽화

solodevnote.com 하나를 루트부터 찾아가 보면 루트 서버가 188 ms, com 서버가 187 ms, Cloudflare 이름서버가 9 ms 만에 답합니다. 맥 터미널의 dig를 쓰면 이렇게 DNS 조회의 각 단계를 실제 응답으로 볼 수 있어요. AI 코딩 도구로 배포한 서비스의 도메인을 점검할 때도 같은 명령을 쓸 수 있습니다.

DNS란 무엇이고 어떻게 바로 확인할까요?

DNS는 solodevnote.com 같은 이름을 104.21.68.112 같은 IP 주소로 바꿔 주는 인터넷의 이름 체계입니다. MDN의 DNS 설명은 이 변환 과정을 DNS 조회라고 부릅니다. 맥에서는 dig 이름 +short 한 줄이면 결과를 바로 확인할 수 있어요.

$ dig solodevnote.com +short && dig naver.com +short
104.21.68.112
172.67.195.4
223.130.192.248
223.130.200.236
223.130.192.247
223.130.200.219

위 두 줄이 solodevnote.com의 주소이고, 아래 네 줄이 naver.com의 주소입니다. 그런데 이 답은 누가 준 걸까요? 먼저 맥이 어느 서버에 묻는지부터 살펴보겠습니다.

함께 읽을 글

내 맥은 어느 DNS 서버에 먼저 물을까요?

이 맥은 Tailscale(VPN)이 켜져 있어 먼저 묻는 DNS 서버가 100.100.100.100이고, 공유기(192.168.0.1)가 그다음입니다. 확인용 명령은 scutil --dns인데, 이번 실행에서는 명령을 찾지 못해 서버 목록을 얻지 못했습니다.

$ scutil --dns | grep -E '^resolver #|nameserver\['  | head -5
zsh:2: command not found: scutil

기록만으로는 명령을 찾지 못한 원인을 알 수 없습니다. 다만 뒤에 나올 추적 출력의 첫 응답이 100.100.100.100에서 왔으니, 그 서버에 물었다는 점은 확인됩니다. 여러분의 터미널에서도 위 명령을 실행해 결과를 확인해 보세요.

조회 결과는 어떻게 읽을까요?

+short를 빼면 응답 전체가 나옵니다. 여기서는 서버를 @1.1.1.1로 지정해 물었습니다.

$ dig @1.1.1.1 solodevnote.com
; <<>> DiG 9.10.6 <<>> @1.1.1.1 solodevnote.com
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 19416
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;solodevnote.com.		IN	A

;; ANSWER SECTION:
solodevnote.com.	300	IN	A	104.21.68.112
solodevnote.com.	300	IN	A	172.67.195.4

;; Query time: 14 msec
;; SERVER: 1.1.1.1#53(1.1.1.1)
;; WHEN: Mon Nov 02 11:00:08 KST 2026
;; MSG SIZE  rcvd: 76

처음에는 아래 다섯 곳만 읽어도 충분합니다.

위치이 출력의 값읽는 법
statusNOERROR없는 이름이면 NXDOMAIN이 나옵니다
flagsqr rd rard는 재귀 요청, ra는 재귀 제공 가능 표시입니다
ANSWER의 숫자300캐시에 저장해 둘 수 있는 초(TTL)입니다
Query time14 msec이 조회에 걸린 시간입니다
SERVER1.1.1.1#53답을 준 서버와 포트입니다

루트 서버부터 따라가면 무엇이 보일까요?

dig +trace는 위에서부터 차례로 물어 내려가며 받은 응답을 모두 보여 줍니다. 전체 명령은 4.3초가 걸렸고, 아래 출력은 같은 형태가 반복되는 NS 줄을 일부 생략한 것입니다.

$ dig +trace +nodnssec solodevnote.com | tee trace.txt
.			515578	IN	NS	c.root-servers.net.
.			515578	IN	NS	m.root-servers.net.
;; Received 811 bytes from 100.100.100.100#53(100.100.100.100) in 7 ms

com.			172800	IN	NS	f.gtld-servers.net.
com.			172800	IN	NS	i.gtld-servers.net.
;; Received 840 bytes from 192.112.36.4#53(g.root-servers.net) in 188 ms

solodevnote.com.	172800	IN	NS	elliot.ns.cloudflare.com.
solodevnote.com.	172800	IN	NS	addyson.ns.cloudflare.com.
;; Received 365 bytes from 192.42.93.30#53(g.gtld-servers.net) in 187 ms

solodevnote.com.	300	IN	A	104.21.68.112
solodevnote.com.	300	IN	A	172.67.195.4
;; Received 76 bytes from 172.64.33.162#53(elliot.ns.cloudflare.com) in 9 ms

각 덩어리의 맨 아래 줄이 답한 서버입니다. 루트 서버는 com을 맡은 서버를, com 서버는 Cloudflare 이름서버를 알려 주죠. 마지막 Cloudflare 이름서버에 와서야 A 레코드, 즉 IP 주소가 나옵니다.

dig +trace로 본 DNS 조회 순서: 루트 서버, com 서버, Cloudflare 이름서버, A 레코드 순서를 단계별로 그린 도식
기본 DNS 서버에서 루트 목록을 받은 뒤 루트와 com 서버의 안내를 따라갑니다. 마지막 Cloudflare 권한 서버는 A 레코드로 답합니다. 4번 캐시는 TTL의 일반적인 의미이며 이번 실행에서 확인한 동작은 아닙니다.
도식출처: 직접 실행한 순서

출력의 루트 이름은 a부터 m까지입니다. IANA 문서에 따르면 루트 서버는 이름으로는 13개지만, 실제로는 여러 나라에 있는 수백 대의 서버로 이뤄진 네트워크입니다. 그러니 이름 개수와 서버 대수는 구분해서 봐야 합니다.

단계별 응답 시간은 아래와 같습니다. 이 네 값은 한 번 잰 결과라서 어느 구간이 늘 느리다고 말할 수는 없어요.

100.100.100.100 7 ms, 루트 서버 188 ms, com 서버 187 ms, Cloudflare 이름서버 9 ms를 비교한 막대 도식
이 맥에서 한 번 잰 값이며 네트워크와 시점에 따라 달라집니다.
도식출처: 근거를 바탕으로 코드가 그린 도식

또 하나 눈여겨볼 점은 첫 응답입니다. 7 ms 응답을 준 곳은 루트 서버가 아니라 100.100.100.100이고, 여기서 받은 것은 루트 서버 목록입니다. 네 응답 시간과 전체 4.3초의 차이는 이 기록만으로는 설명할 수 없습니다.

재귀 질의와 반복 질의는 어떻게 다를까요?

두 방식은 누가 답을 찾아다니느냐가 다릅니다. RFC 1034에 따르면 재귀 모드에서 서버는 오류나 답만 돌려주고, 다른 서버를 알려 주는 응답(referral)은 주지 않습니다. 앞서 @1.1.1.1 응답의 rd와 ra가 함께 켜져 있었죠. RFC는 이 둘을 보고 재귀 모드가 쓰였는지 확인할 수 있다고 설명합니다.

반면 비재귀(non-recursive) 모드의 서버는 자기가 가진 정보로 답이나 오류, 또는 더 가까운 서버를 돌려줍니다. 이 글에서는 이 방식으로 차례로 묻는 것을 반복 질의라고 부릅니다. 추적 출력에서는 dig +trace 자신이 리졸버 역할을 맡아 루트, com, 권한 서버를 차례로 물었습니다.

노트북이 리졸버 상자에 한 번 묻고, 리졸버가 세 단계 서버를 차례로 찾아가는 개념 그림
클라이언트와 리졸버 사이의 질의, 리졸버와 각 서버 사이의 질의를 나눠 나타낸 개념 그림입니다.

정리하면 클라이언트가 리졸버에 묻는 구간은 재귀 질의이고, 리졸버가 각 서버를 묻는 구간은 반복 질의입니다.

권한 서버와 공용 DNS의 답은 어떻게 다를까요?

권한 서버는 그 이름의 답을 직접 맡은 서버입니다. 같은 이름을 Cloudflare 이름서버와 1.1.1.1에 5초 간격으로 세 번씩 물었습니다. 기다린 시간이 들어가서 이 단계는 30.7초가 걸렸어요.

$ echo '권한 서버(elliot.ns.cloudflare.com)'; for i in 1 2 3; do dig @elliot.ns.cloudflare.com solodevnote.com +noall +answer | grep -m1 IN; sleep 5; done
dig @elliot.ns.cloudflare.com solodevnote.com | grep -E 'flags|WARNING'
echo '공용 DNS(1.1.1.1)'; for i in 1 2 3; do dig @1.1.1.1 solodevnote.com +noall +answer | grep -m1 IN; sleep 5; done
권한 서버(elliot.ns.cloudflare.com)
solodevnote.com.	300	IN	A	104.21.68.112
solodevnote.com.	300	IN	A	104.21.68.112
solodevnote.com.	300	IN	A	172.67.195.4
;; flags: qr aa rd; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available
; EDNS: version: 0, flags:; udp: 1232
공용 DNS(1.1.1.1)
solodevnote.com.	300	IN	A	172.67.195.4
solodevnote.com.	300	IN	A	172.67.195.4
solodevnote.com.	300	IN	A	104.21.68.112
비교권한 서버공용 DNS(1.1.1.1)
세 번의 TTL300, 300, 300300, 300, 300
flagsqr aa rdqr rd ra
재귀사용할 수 없다는 경고ra 표시 있음

공용 DNS의 TTL은 조회할 때마다 줄어들까요? 이번 기록에서는 세 번 모두 300이어서 줄어드는 모습을 확인하지 못했습니다. 이유도 이 측정만으로는 가를 수 없으니, 간격을 바꿔 가며 숫자가 달라지는지 직접 다시 재 보세요. 이번에 분명히 드러난 차이는 aa와 ra 표시입니다.

같은 도메인의 IP가 여러 개 나올 때

주소가 여러 개 나오거나 조회할 때마다 순서가 달라져도 바로 이상 신호로 볼 일은 아닙니다. naver.com을 연달아 물으면 같은 네 주소가 순서만 바뀌어 나왔습니다.

$ for i in 1 2 3; do dig @1.1.1.1 naver.com +short | tr '\n' ' '; echo; done
echo "8.8.8.8 정렬: $(dig @8.8.8.8 naver.com +short | sort | tr '\n' ' ')"
223.130.200.236 223.130.200.219 223.130.192.248 223.130.192.247
223.130.200.219 223.130.192.248 223.130.192.247 223.130.200.236
223.130.192.247 223.130.200.236 223.130.200.219 223.130.192.248
8.8.8.8 정렬: 223.130.192.247 223.130.192.248 223.130.200.219 223.130.200.236

8.8.8.8에 물어 정렬한 결과도 같은 네 주소였습니다. 다만 지역에 따라 답이 달라지는지는 이번에 재지 않았어요. 그래서 배포 점검 스크립트를 만든다면 첫 줄 하나만 비교하지 말고, 주소 묶음을 정렬해 비교하는 편이 안전해 보입니다.

레코드 종류별 조회 결과

주소 말고 다른 정보도 같은 명령으로 물을 수 있습니다. 이름 뒤에 종류를 붙이면 되고, IP로 이름을 찾는 역방향 조회는 dig -x로 합니다.

$ for t in A AAAA NS MX SOA; do echo "$t: $(dig solodevnote.com $t +short | tr '\n' ' ')"; done && echo "PTR(1.1.1.1): $(dig -x 1.1.1.1 +short)"
A: 172.67.195.4 104.21.68.112
AAAA: 2606:4700:3033::ac43:c304 2606:4700:3031::6815:4470
NS: elliot.ns.cloudflare.com. addyson.ns.cloudflare.com.
MX: 8 route2.mx.cloudflare.net. 5 route1.mx.cloudflare.net. 57 route3.mx.cloudflare.net.
SOA: addyson.ns.cloudflare.com. dns.cloudflare.com. 2416056046 10000 2400 604800 1800
PTR(1.1.1.1): one.one.one.one.

예를 들어 메일 설정을 확인한다면 MX 줄을, 이름서버 변경을 확인한다면 NS 줄을 보면 됩니다. 이 도메인은 NS와 MX 모두 Cloudflare 이름을 가리키고 있네요.

없는 이름을 물으면 어떤 답이 올까요?

없는 이름을 물으면 status가 NXDOMAIN으로 바뀌고, 도메인의 SOA 레코드가 함께 옵니다.

$ dig @1.1.1.1 nothing-here.solodevnote.com | grep -E 'status|SOA'
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 35553
solodevnote.com.	1800	IN	SOA	addyson.ns.cloudflare.com. dns.cloudflare.com. 2416056046 10000 2400 604800 1800

그래서 서브도메인이 안 열릴 때는 status부터 보세요. NXDOMAIN이라면 그 서버는 이름이 없다고 답한 것이니, 먼저 DNS 쪽 설정을 살펴볼 수 있습니다.

맥 시스템이 보는 주소 확인하기

dig 말고 맥 시스템 쪽 결과는 dscacheutil로 볼 수 있습니다. 이번에는 IPv6와 IPv4 주소가 함께 나왔고, 앞서 본 AAAA·A 값과 같았습니다.

$ dscacheutil -q host -a name solodevnote.com
name: solodevnote.com
ipv6_address: 2606:4700:3031::6815:4470
ipv6_address: 2606:4700:3033::ac43:c304

name: solodevnote.com
ip_address: 172.67.195.4
ip_address: 104.21.68.112

HTTPS로 물으면 응답이 어떻게 생겼을까요?

DoH(DNS over HTTPS)는 DNS 질의를 HTTPS로 보내는 방식입니다. Cloudflare의 JSON 형식 문서대로 accept 헤더를 붙이면 curl만으로 물을 수 있어요.

$ curl -s -H 'accept: application/dns-json' 'https://cloudflare-dns.com/dns-query?name=solodevnote.com&type=A' | jq -c '{Status, Answer: [.Answer[] | {data, TTL}]}'
curl -s -o /dev/null -w 'DoH 응답 시간 %{time_total}초\n' -H 'accept: application/dns-json' 'https://cloudflare-dns.com/dns-query?name=solodevnote.com&type=A'
dig @1.1.1.1 solodevnote.com | grep 'Query time'
{"Status":0,"Answer":[{"data":"172.67.195.4","TTL":300},{"data":"104.21.68.112","TTL":300}]}
DoH 응답 시간 0.041438초
;; Query time: 17 msec

답은 같은 두 주소와 TTL 300이고, 시간은 DoH가 0.041438초, dig가 17 msec였습니다. 다만 각 한 번 잰 값이고 측정 도구도 달라서, 이 차이의 원인까지는 가를 수 없습니다.

JSON이라 에이전트가 만든 스크립트에서 다루기 편해 보이지만 주의할 점이 있습니다. Cloudflare는 이 형식에 정식 RFC가 없어 제공자마다 동작이 다를 수 있다고 안내합니다. 중요한 용도에는 와이어 형식(wireformat)을 권하니, 이 점을 고려해 고르세요.

응답이 커서 잘리면 어떻게 될까요?

google.com의 TXT 레코드를 +bufsize=512로 물으면 잘리는 모습을 직접 볼 수 있습니다. 첫 명령은 TCP 재시도를 막았고, 두 번째 명령은 막지 않았습니다.

$ dig @8.8.8.8 +notcp +ignore +bufsize=512 google.com TXT | grep -E 'flags|MSG SIZE'
echo '---'
dig @8.8.8.8 +bufsize=512 google.com TXT | grep -E 'Truncated|flags|MSG SIZE'
;; flags: qr tc rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1
; EDNS: version: 0, flags:; udp: 512
;; MSG SIZE  rcvd: 39
---
;; Truncated, retrying in TCP mode.
;; flags: qr rd ra; QUERY: 1, ANSWER: 17, AUTHORITY: 0, ADDITIONAL: 1
; EDNS: version: 0, flags:; udp: 512
;; MSG SIZE  rcvd: 1187
조건tc 표시ANSWER받은 크기
TCP 재시도 안 함있음039바이트
TCP로 재시도없음171187바이트

즉 UDP 크기를 512바이트로 알린 이 조건에서는 답이 잘렸고, dig가 TCP로 다시 물어 전체를 받았습니다. 앞서 1.1.1.1 응답에는 udp: 1232가 표시되어 있었으니, 다른 조건에서도 같으리라고 넘겨짚지는 마세요.

Claude Code에게 출력 정리를 맡기려면?

추적 출력은 저장해 둔 trace.txt를 그대로 넘기면 표로 받을 수 있습니다. 이 단계는 7.1초가 걸렸습니다.

$ claude --model opus -p "아래는 맥 터미널에서 실행한 dig +trace +nodnssec solodevnote.com 출력이다. 단계마다 어느 서버(이름과 IP)가 답했고 다음 단계로 어떤 이름서버를 알려 줬는지, 걸린 시간과 함께 한국어 표로 정리하고, 마지막 줄에 최종 A 레코드를 적어. 출력에 없는 내용은 쓰지 마." < trace.txt
| 단계 | 응답 서버 (이름) | 응답 서버 (IP) | 알려 준 다음 단계 이름서버 | 걸린 시간 |
|---|---|---|---|---|
| 1 | 출력에 이름 없음 | 100.100.100.100 | 루트(`.`): `a`~`m.root-servers.net` 13개 | 7 ms |
| 2 | `g.root-servers.net` | 192.112.36.4 | `com.`: `a`~`m.gtld-servers.net` 13개 | 188 ms |
| 3 | `g.gtld-servers.net` | 192.42.93.30 | `solodevnote.com.`: `elliot.ns.cloudflare.com`, `addyson.ns.cloudflare.com` | 187 ms |
| 4 | `elliot.ns.cloudflare.com` | 172.64.33.162 | 없음 (A 레코드로 최종 응답) | 9 ms |

최종 A 레코드: `solodevnote.com` → `104.21.68.112`, `172.67.195.4` (TTL 300)

첫 줄의 서버 이름을 지어내지 않고 "출력에 이름 없음"으로 적은 점이 눈에 띕니다. 프롬프트에 출력에 없는 내용은 쓰지 말라고 적어 둔 요청과 맞는 결과죠. 도메인 문제를 에이전트에게 맡긴다면 이처럼 원본 출력을 함께 주고, 받은 표를 원본과 대조하는 습관을 권합니다.

측정 조건과 한계

모든 값은 2026-11-02 11:00 KST에 이 맥에서 명령을 한 번씩 실행해 얻은 것입니다. 시간과 TTL은 네트워크와 시점에 따라 달라지니 기준값으로 삼지 마세요.

  • 환경: darwin arm64, dig(BIND 9.10.6), Claude Code 2.1.309
  • 네트워크: Tailscale이 켜진 상태이며, 다른 네트워크에서는 첫 서버와 시간이 다를 수 있습니다
  • 시간 기준: dig의 Query time과 curl의 time_total
  • 재지 않은 것: 첫 조회와 반복 조회의 속도 차이, 지역별 응답 차이, nslookup 결과

특히 scutil --dns는 이번에 실패했고, 공용 DNS의 TTL 감소도 확인되지 않았습니다. 자기 도메인으로 확인하려면 solodevnote.com 자리에 이름만 바꿔 위 명령들을 실행하면 됩니다.

참고 자료