안동민 개발노트 아이콘

안동민 개발노트

8장 : DNS

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 같은 다른 신호가 항상 사라지는 것은 아닙니다.

프로토콜일반 포트전송 방식보호하는 구간운영상 특징
전통 DNS53UDP/TCP기본적으로 암호화 없음단순하고 널리 지원되지만 관찰 쉬움
DoT853DNS over TLS클라이언트 ↔ 리졸버 DNS 메시지DNS 전용 포트라 정책 적용이 비교적 쉬움
DoH443DNS over HTTPS클라이언트 ↔ DoH 서버 HTTP 교환HTTPS와 같은 포트를 써서 앱별 설정이 쉬움
dns_privacy_check.sh
# 현재 시스템의 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_check.sh
# 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 +short

DNSSEC은 무결성출처 인증을 제공하지만 기밀성은 제공하지 않습니다. 즉, 응답이 변조되지 않았는지 검증하는 기술이지, 어떤 도메인을 질의했는지 숨기는 기술은 아닙니다. 질의 프라이버시까지 고려하려면 DoT나 DoH 같은 암호화 DNS 전송과 함께 봐야 합니다.


DNS 실무 문제 해결

DNS 장애는 “사이트가 안 열린다”로 보이지만 원인은 다양합니다. 로컬 캐시, 리졸버 장애, 권한 DNS 설정 오류, DNSSEC 검증 실패, 도메인 만료, 방화벽, 실제 서버 장애를 단계별로 분리해야 합니다.

문제흔한 증상확인할 것대응 방향
캐시 오래됨서버 이전 후 옛 IP로 접속TTL, OS/브라우저/리졸버 캐시TTL 사전 조정, 캐시 플러시
NXDOMAIN도메인 없음등록 만료, 오타, 위임 누락도메인/존/NS 설정 확인
SERVFAIL해석 실패권한 서버 장애, DNSSEC 검증 실패권한 서버와 DS/DNSKEY 체인 확인
느린 DNS첫 접속 지연리졸버 응답 시간, 네트워크 경로다른 리졸버 비교, 로컬 캐시 확인
잘못된 IP엉뚱한 서버로 접속레코드 오타, 오래된 캐시, 하이재킹권한 서버 직접 조회, 보안 점검
dns_health_check.py
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를 자세히 살펴보겠습니다.