DNS Lookup: 도메인 이름을 IP 주소로 바꾸는 계층적 분산 조회 과정
DNS Lookup은 `example.com` 같은 도메인 이름을 IP 주소로 변환하는 과정이다. 스텁 리졸버(stub resolver), 재귀 리졸버(recursive resolver), 루트·TLD·권한 네임서버(authoritative name server)가 계층적으로 협력하며, 캐시와 TTL로 조회 비용을 줄인다. 이 글에서는 조회 흐름, 레코드
By 송민성
1. 개념
DNS(Domain Name System)는 도메인 이름과 IP 주소 등의 자원 정보를 대응시키는 계층적 분산 데이터베이스다. DNS Lookup(또는 name resolution)은 클라이언트가 특정 이름에 대한 레코드(record)를 질의하고 응답받는 과정이다.
주요 구성 요소는 다음과 같다.
| 구성 요소 | 역할 |
|---|---|
| 스텁 리졸버(stub resolver) | OS나 라이브러리에 내장된 클라이언트 측 리졸버. 재귀 리졸버에 질의를 위임한다 |
| 재귀 리졸버(recursive resolver) | ISP, 회사, 공용 DNS(예: 1.1.1.1, 8.8.8.8)가 운영. 대신 전체 조회를 수행하고 캐시한다 |
| 루트 네임서버(root server) | 최상위 영역. TLD 네임서버 위치를 알려준다 |
| TLD 네임서버 | .com, .kr 같은 최상위 도메인 담당. 해당 도메인의 권한 네임서버 위치를 알려준다 |
| 권한 네임서버(authoritative server) | 해당 도메인의 실제 레코드를 보유한 최종 응답자 |
자주 쓰는 레코드 타입은 다음과 같다.
| 타입 | 의미 |
|---|---|
| A | 이름 → IPv4 주소 |
| AAAA | 이름 → IPv6 주소 |
| CNAME | 이름 → 다른 이름(별칭, canonical name) |
| MX | 메일 서버 |
| TXT | 임의 텍스트(SPF, 도메인 소유 확인 등) |
| NS | 해당 영역의 네임서버 |
2. 왜 사용하는가
- 사람이 기억하기 쉬운 이름과 네트워크가 사용하는 IP 주소를 분리한다.
- 서버 IP가 바뀌어도 레코드만 수정하면 사용자는 같은 이름으로 접속한다.
- 하나의 중앙 서버가 아니라 영역(zone) 단위로 관리 권한을 위임하므로 확장성이 높다.
- 캐시를 통해 반복 조회 비용을 줄인다.
- 하나의 이름에 여러 IP를 반환하거나 지역별로 다른 응답을 주는 방식으로 부하 분산과 트래픽 제어에 활용할 수 있다.
3. 동작 원리
브라우저가 www.example.com에 접속하는 흐름은 다음과 같다.
- 로컬 캐시 확인: 브라우저 캐시, OS 캐시,
hosts파일 순으로 확인한다. 있으면 여기서 끝난다. - 스텁 리졸버 → 재귀 리졸버: 캐시에 없으면 설정된 재귀 리졸버에 재귀 질의(recursive query)를 보낸다.
- 재귀 리졸버 캐시 확인: 이미 캐시된 응답이 있으면 즉시 반환한다.
- 반복 질의(iterative query): 캐시가 없으면 재귀 리졸버가 계층을 따라 내려간다.
- 루트 서버에 질의 → .com TLD 서버 목록(NS 레퍼럴, referral)을 받는다. - TLD 서버에 질의 → example.com의 권한 네임서버 목록을 받는다. - 권한 네임서버에 질의 → www.example.com의 A/AAAA 레코드를 받는다.
- 응답 캐시 및 반환: 재귀 리졸버는 레코드의 TTL(Time To Live, 초 단위) 동안 결과를 캐시하고 클라이언트에 돌려준다.
실제로는 재귀 리졸버가 루트·TLD의 NS 정보도 캐시하므로, 대부분의 조회에서 루트부터 다시 내려가지 않는다.
전송은 전통적으로 UDP 53번 포트를 쓰며, 응답이 커서 잘리거나(truncation) 영역 전송(zone transfer) 같은 경우에는 TCP를 사용한다. 이 외에 DNS over TLS(DoT), DNS over HTTPS(DoH) 같은 암호화 전송도 있다.
CNAME이 응답에 포함되면 리졸버는 별칭이 가리키는 이름을 이어서 조회한다.
4. 코드 예제
Shell: dig로 조회 과정 확인
# 기본 조회 (A 레코드)
dig example.com A
# 답변 부분만 간단히 출력
dig +short example.com A
# 특정 재귀 리졸버를 지정해서 조회
dig @1.1.1.1 example.com AAAA +short
# 루트부터 위임(delegation)을 따라가는 과정을 출력
dig +trace example.com
# 네임서버와 메일 서버 레코드 조회
dig example.com NS +short
dig example.com MX +shortdig 출력의 답변 행에서 두 번째 숫자 필드가 남은 TTL(초)이다. 같은 질의를 반복하면 재귀 리졸버 캐시에 의해 이 값이 줄어드는 것을 볼 수 있다.
Python: OS 리졸버로 조회
import socket
def lookup(host: str, port: int = 443) -> list[str]:
"""OS의 리졸버(getaddrinfo)를 통해 IP 주소 목록을 반환한다."""
infos = socket.getaddrinfo(host, port, proto=socket.IPPROTO_TCP)
# 중복 제거, 순서 유지
seen: dict[str, None] = {}
for family, _type, _proto, _canon, sockaddr in infos:
seen[sockaddr[0]] = None
return list(seen)
if __name__ == "__main__":
for ip in lookup("example.com"):
print(ip)getaddrinfo는 hosts 파일, OS 캐시, 설정된 재귀 리졸버 등 시스템 설정을 따른다. IPv4와 IPv6 결과가 함께 나올 수 있다.
Python: TTL을 고려한 간단한 캐시 구현
import socket
import time
class DnsCache:
def __init__(self, ttl_seconds: float = 30.0):
self.ttl = ttl_seconds
self._store: dict[str, tuple[float, list[str]]] = {}
def resolve(self, host: str) -> list[str]:
now = time.monotonic()
entry = self._store.get(host)
if entry and entry[0] > now:
return entry[1] # 캐시 적중(hit)
infos = socket.getaddrinfo(host, 443, proto=socket.IPPROTO_TCP)
ips = list(dict.fromkeys(i[4][0] for i in infos))
self._store[host] = (now + self.ttl, ips)
return ips
if __name__ == "__main__":
cache = DnsCache(ttl_seconds=10)
t0 = time.perf_counter()
cache.resolve("example.com")
first = time.perf_counter() - t0
t0 = time.perf_counter()
cache.resolve("example.com")
second = time.perf_counter() - t0
print(f"첫 조회: {first * 1000:.2f} ms")
print(f"캐시 조회: {second * 1000:.4f} ms")참고로 getaddrinfo는 실제 DNS 레코드의 TTL을 알려주지 않으므로, 위 예제는 고정 TTL을 사용한 학습용 구현이다. 실제 TTL을 존중하려면 DNS 응답을 직접 파싱하는 라이브러리가 필요하다.
TypeScript: Node.js에서 조회
import { promises as dns } from "node:dns";
async function main(): Promise<void> {
const host = "example.com";
// A 레코드와 TTL 함께 조회 (ttl: true)
const a = await dns.resolve4(host, { ttl: true });
console.log("A:", a);
const mx = await dns.resolveMx(host);
console.log("MX:", mx);
// OS의 getaddrinfo를 사용 (hosts 파일 등 시스템 설정 반영)
const sys = await dns.lookup(host, { all: true });
console.log("lookup:", sys);
}
main().catch((err) => {
console.error("DNS 조회 실패:", err);
process.exit(1);
});Node.js에서 dns.lookup()은 getaddrinfo를 호출하고, dns.resolve*()는 네트워크로 DNS 서버에 직접 질의한다는 차이가 있다. dns.lookup()은 libuv 스레드 풀에서 실행되므로 동시 호출이 많으면 스레드 풀(기본 4개)이 병목이 될 수 있다.
5. 시간 복잡도 또는 성능 특성
DNS Lookup은 알고리즘 복잡도보다 네트워크 왕복 지연(RTT)이 성능을 좌우한다.
| 상황 | 특성 |
|---|---|
| 로컬(브라우저/OS) 캐시 적중 | 네트워크 요청 없음. 사실상 메모리 조회 수준 |
| 재귀 리졸버 캐시 적중 | 클라이언트 ↔ 리졸버 1회 왕복 |
| 캐시 미스(cache miss) | 리졸버가 루트·TLD·권한 서버를 차례로 질의하므로 여러 번 왕복이 추가됨 |
- 캐시 미스 시 지연은 최소 몇 번의 RTT 합이므로 권한 서버와의 거리, 서버 응답 속도에 따라 크게 달라진다. 정해진 고정 수치는 없고 환경마다 측정해야 한다.
- TTL이 길수록 캐시 적중률이 높아져 평균 지연은 줄지만, 레코드 변경이 전파되는 속도는 느려진다.
- 부정 응답(NXDOMAIN, 존재하지 않는 이름)도 캐시된다(negative caching). 기간은 영역의 SOA 레코드 값을 따른다.
6. 실무 사용 사례
- 배포·이전 시 TTL 조정: 서버 IP를 교체하기 전에 TTL을 미리 낮춰 두면 전환 후 전파 시간을 줄일 수 있다. 이전 TTL이 지나야 낮춘 값이 적용된다는 점에 유의한다.
- CDN 연동: 서비스 도메인을 CNAME으로 CDN 호스트명에 연결하고, CDN의 DNS가 사용자 위치에 맞는 엣지 IP를 응답한다.
- DNS 라운드 로빈(round robin): 하나의 이름에 여러 A 레코드를 등록해 간단히 부하를 분산한다. 서버 상태를 반영하지 않으므로 헬스 체크가 필요하면 별도 기능이 필요하다.
- 이메일 인증: MX로 메일 서버를 지정하고, TXT로 SPF·DKIM·DMARC 정책을 게시한다.
- 도메인 소유권 확인: 서비스 가입 시 요구하는 TXT 레코드를 등록한다.
- 성능 최적화: HTML에
<link rel="dns-prefetch" href="//cdn.example.com">을 넣어 외부 도메인 조회를 미리 수행하게 한다. - 장애 분석:
dig +trace,dig @특정리졸버로 어느 단계에서 응답이 틀어지는지 구분한다.
7. 주의할 점
- 전파 지연: "DNS 전파"는 실제로는 각 리졸버 캐시의 TTL 만료를 기다리는 것이다. 일부 리졸버는 TTL을 그대로 지키지 않을 수 있다.
- Apex 도메인의 CNAME 제한: 영역 최상위(
example.com)에는 NS, SOA 등 다른 레코드가 공존하므로 CNAME을 둘 수 없다. 일부 DNS 제공업체의 ALIAS/ANAME 같은 독자 기능으로 대체한다. - 캐시 때문에 보이는 불일치: 로컬 PC, 회사 리졸버, 공용 리졸버가 서로 다른 값을 줄 수 있다. 확인 시
dig @리졸버로 지정해 비교한다. - Node.js의 lookup/resolve 혼동:
dns.lookup은hosts파일을 반영하지만dns.resolve*는 반영하지 않는다. - 보안: 평문 DNS는 도청·변조 가능성이 있다. DNSSEC은 응답의 무결성을 서명으로 검증하지만 기밀성은 제공하지 않는다. 기밀성은 DoT/DoH 같은 암호화 전송이 담당한다.
- 지나치게 짧은 TTL: 질의량이 늘어 리졸버·권한 서버 부하와 지연이 증가한다.
- DNS 장애의 파급: 권한 네임서버가 하나뿐이거나 같은 장애 영역에 몰려 있으면 도메인 전체가 접근 불가가 된다. 서로 다른 네트워크의 네임서버 여러 대를 두는 것이 일반적이다.
8. 핵심 정리
- DNS Lookup은 이름을 레코드(주로 IP)로 바꾸는 계층적·분산 조회 과정이며, 스텁 리졸버 → 재귀 리졸버 → 루트 → TLD → 권한 서버 순으로 진행된다.
- 재귀 리졸버가 반복 질의를 대신 수행하고 TTL 동안 결과를 캐시한다.
- 성능은 캐시 적중 여부와 RTT에 의해 결정되며, TTL은 속도와 변경 반영 속도 사이의 트레이드오프다.
dig,dig +trace로 단계별 응답을 직접 확인할 수 있고, 코드에서는getaddrinfo계열(OS 설정 반영)과 직접 질의 방식의 차이를 구분해야 한다.- 실무에서는 TTL 사전 조정, CNAME·Apex 제한, 암호화 전송(DoT/DoH)과 DNSSEC의 역할 차이를 이해하는 것이 중요하다.