DNS 심화와 실무
DNS의 기본 동작과 레코드를 이해했으니, 이제 실무에서 자주 마주치는 라우팅, 프라이버시, 보안, 장애 진단을 살펴보겠습니다. DNS는 단순한 “도메인 → IP 변환기”가 아니라 분산 데이터베이스, 캐시, 정책 기반 응답, 보안 검증이 함께 움직이는 인프라입니다.
DNS 라운드 로빈
하나의 이름에 여러 개의 A/AAAA 레코드를 두면 응답에 여러 IP가 포함될 수 있고, 권한 DNS 서버나 리졸버는 응답 순서를 바꾸어 줄 수 있습니다. 이를 흔히 DNS 라운드 로빈이라고 부릅니다.
다만 DNS 라운드 로빈은 L4/L7 로드밸런서처럼 연결 단위로 정교하게 분산하지 않습니다. 리졸버, 운영체제, 브라우저가 응답을 캐시할 수 있고, 클라이언트가 응답 목록 중 어떤 주소를 선택하는지도 구현에 따라 달라집니다. 또한 단순 라운드 로빈만으로는 서버 장애를 자동 감지하지 못하므로, 실제 운영에서는 헬스 체크, 짧은 TTL, 로드밸런서, Anycast, CDN과 함께 설계합니다.
GeoDNS와 글로벌 서비스 라우팅
GeoDNS는 질의 위치나 네트워크 정보를 기준으로 다른 응답을 돌려주는 DNS 운영 방식입니다. 예를 들어 한국 사용자는 서울 리전, 미국 사용자는 버지니아 리전의 IP를 받도록 할 수 있습니다.
주의할 점은 권한 DNS 서버가 보통 최종 사용자의 IP가 아니라 재귀 리졸버의 IP를 본다는 것입니다. EDNS Client Subnet(ECS)을 쓰면 일부 클라이언트 네트워크 정보가 권한 서버에 전달될 수 있지만, 성능과 프라이버시의 절충이 생기며 모든 리졸버가 같은 방식으로 지원하는 것도 아닙니다.
DNS 라우팅 정책과 장애 조치
클라우드 DNS와 CDN은 단순 응답 외에도 가중치, 지연 시간, 지리 위치, 헬스 체크 기반 장애 조치 같은 정책을 제공합니다. 이 정책들은 DNS 응답을 바꾸는 방식이므로 TTL과 캐시의 영향을 항상 받습니다.
| DNS 라우팅 정책 | 판단 기준 | 적합한 사용 사례 | 주의할 점 |
|---|---|---|---|
| 단순(Simple) | 고정 레코드 | 단일 서버, 내부 도메인 | 장애 감지 없음 |
| 라운드 로빈 | 여러 값의 순서나 목록 | 가벼운 분산, 실습 환경 | 정확한 비율 보장 아님 |
| 가중치(Weighted) | 설정한 비율 | 점진적 이전, A/B 테스트 | 캐시 때문에 순간 비율은 흔들림 |
| 지리적(Geolocation) | 리졸버/ECS 기반 위치 | 지역별 콘텐츠, 규제 분리 | 최종 사용자 위치와 다를 수 있음 |
| 지연 시간(Latency) | 측정된 리전 지연 시간 | 글로벌 API, CDN | 실시간 네트워크 상태와 차이 가능 |
| 장애 조치(Failover) | 헬스 체크 결과 | 주/백업 리전 | TTL 동안 이전 응답이 남을 수 있음 |
| 다중 값(Multi-value) | 헬스 체크를 통과한 여러 값 | DNS 수준의 단순 고가용성 | 로드밸런서 대체재로 과신 금지 |
DNS over HTTPS와 DNS over TLS
전통적인 DNS는 보통 53번 포트의 UDP 또는 TCP로 질의와 응답을 주고받습니다. 이 구간이 평문이면 같은 네트워크의 관찰자는 어떤 이름을 질의했는지 볼 수 있고, 공격자는 응답을 위조하려고 시도할 수 있습니다.
DNS over TLS(DoT)는 DNS 메시지를 TLS 연결 위에 실어 보내며 기본 포트는 853입니다. DNS over HTTPS(DoH)는 DNS 질의를 HTTPS 요청으로 보냅니다. 둘 다 클라이언트와 선택한 리졸버 사이의 DNS 질의를 암호화하지만, 리졸버 운영자는 여전히 질의를 볼 수 있고, 목적지 IP나 TLS SNI 같은 다른 신호가 항상 사라지는 것은 아닙니다.
| 프로토콜 | 일반 포트 | 전송 방식 | 보호하는 구간 | 운영상 특징 |
|---|---|---|---|---|
| 전통 DNS | 53 | UDP/TCP | 기본적으로 암호화 없음 | 단순하고 널리 지원되지만 관찰 쉬움 |
| DoT | 853 | DNS over TLS | 클라이언트 ↔ 리졸버 DNS 메시지 | DNS 전용 포트라 정책 적용이 비교적 쉬움 |
| DoH | 443 | DNS over HTTPS | 클라이언트 ↔ DoH 서버 HTTP 교환 | HTTPS와 같은 포트를 써서 앱별 설정이 쉬움 |
# 현재 시스템의 DNS 서버 확인
# Windows
ipconfig /all | findstr "DNS Servers"
# Linux/macOS
cat /etc/resolv.conf
# Cloudflare의 JSON API 예시입니다. RFC 8484의 application/dns-message 형식과는 다릅니다.
curl -s "https://cloudflare-dns.com/dns-query?name=example.com&type=A" \
-H "Accept: application/dns-json"DNS 캐시 포이즈닝과 DNSSEC
DNS 캐시 포이즈닝은 재귀 리졸버가 권한 서버의 진짜 응답보다 먼저 도착한 위조 응답을 믿고 캐시에 저장하게 만드는 공격입니다. 현대 리졸버는 트랜잭션 ID, 질의 이름, 질의 타입, 출발지 포트 무작위화, 응답 출처 확인 같은 조건으로 위조 난도를 높입니다.
DNSSEC(DNS Security Extensions)는 DNS 응답에 서명을 붙여 데이터 출처와 무결성을 검증하는 확장입니다. 리졸버는 루트부터 TLD, 도메인 영역까지 이어지는 신뢰 체인을 따라 DS, DNSKEY, RRSIG 같은 레코드를 확인합니다.
# DNSSEC 관련 레코드와 응답 확인
dig +dnssec example.com
# 검증 리졸버가 Authenticated Data 플래그를 세웠는지 확인
dig +dnssec +adflag example.com | grep "flags"
# DNSKEY와 DS 확인
dig DNSKEY example.com +short
dig DS example.com +shortDNSSEC은 무결성과 출처 인증을 제공하지만 기밀성은 제공하지 않습니다. 즉, 응답이 변조되지 않았는지 검증하는 기술이지, 어떤 도메인을 질의했는지 숨기는 기술은 아닙니다. 질의 프라이버시까지 고려하려면 DoT나 DoH 같은 암호화 DNS 전송과 함께 봐야 합니다.
DNS 실무 문제 해결
DNS 장애는 “사이트가 안 열린다”로 보이지만 원인은 다양합니다. 로컬 캐시, 리졸버 장애, 권한 DNS 설정 오류, DNSSEC 검증 실패, 도메인 만료, 방화벽, 실제 서버 장애를 단계별로 분리해야 합니다.
| 문제 | 흔한 증상 | 확인할 것 | 대응 방향 |
|---|---|---|---|
| 캐시 오래됨 | 서버 이전 후 옛 IP로 접속 | TTL, OS/브라우저/리졸버 캐시 | TTL 사전 조정, 캐시 플러시 |
| NXDOMAIN | 도메인 없음 | 등록 만료, 오타, 위임 누락 | 도메인/존/NS 설정 확인 |
| SERVFAIL | 해석 실패 | 권한 서버 장애, DNSSEC 검증 실패 | 권한 서버와 DS/DNSKEY 체인 확인 |
| 느린 DNS | 첫 접속 지연 | 리졸버 응답 시간, 네트워크 경로 | 다른 리졸버 비교, 로컬 캐시 확인 |
| 잘못된 IP | 엉뚱한 서버로 접속 | 레코드 오타, 오래된 캐시, 하이재킹 | 권한 서버 직접 조회, 보안 점검 |
import socket
import time
def dns_health_check(domains):
"""여러 도메인의 기본 DNS 해석 상태 확인"""
results = []
for domain in domains:
start = time.time()
try:
ip = socket.gethostbyname(domain)
elapsed = (time.time() - start) * 1000
results.append((domain, ip, f"{elapsed:.1f}ms", "OK"))
except socket.gaierror as e:
elapsed = (time.time() - start) * 1000
results.append((domain, "-", f"{elapsed:.1f}ms", str(e)))
print(f"{'도메인':<25} {'IP':<18} {'시간':<10} {'상태'}")
print("-" * 70)
for domain, ip, time_str, status in results:
print(f"{domain:<25} {ip:<18} {time_str:<10} {status}")
domains = [
"google.com",
"github.com",
"naver.com",
"example.com",
"nonexistent-domain-test.xyz",
]
dns_health_check(domains)다음 장에서는 웹 개발자가 매일 사용하지만 깊이 이해하는 경우가 드문 프로토콜, HTTP를 자세히 살펴보겠습니다.