본문 바로가기

DNS 레코드와 실습

리소스 레코드를 이름, TTL, 클래스, 타입, 값의 다섯 칸으로 읽는 법과 A, AAAA, CNAME, MX, NS, TXT, SOA, PTR 같은 주요 레코드의 쓰임과 제약을 정리합니다. CNAME의 사슬을 따라가는 코드와 역방향 이름을 만드는 코드를 보고, dig의 출력에서 상태와 플래그, 빈 응답의 세 가지 뜻을 구별합니다.

앞 글에서 DNS 서버들이 이름의 계층을 나누어 맡는다는 것을 보았습니다. 이 글에서는 그 서버들이 저장하고 돌려주는 정보, 곧 리소스 레코드(resource record)를 살펴봅니다. 레코드의 종류마다 담는 값과 지켜야 할 규칙이 다르고, 이것을 모르면 "DNS를 고쳤는데 메일이 안 온다" 같은 문제의 원인을 찾기 어렵습니다. 끝으로 dig와 nslookup으로 실제 응답을 읽는 방법을 정리합니다.

레코드는 다섯 칸으로 읽는다

레코드 하나는 이름(owner name), TTL, 클래스(class), 타입(type), 값(RDATA)의 다섯 칸으로 이루어집니다(RFC 1035 §3.2.1). dig의 응답도 레코드 하나를 다음처럼 한 줄에 이 순서대로 적어 보여 줍니다. 이름 끝에 붙은 점은 루트까지 모두 적은 완전한 이름이라는 표시입니다.

www.example.com.  300  IN  A  192.0.2.44

이 줄은 www.example.com.이라는 이름에 대한 A 타입 레코드이고, 값은 IPv4 주소 192.0.2.44이며, 캐시는 300초 동안 이 답을 다시 쓸 수 있다는 뜻입니다. 클래스 IN은 인터넷을 뜻하며 지금은 사실상 이것만 씁니다. 같은 이름에 같은 타입의 레코드가 여러 개 있으면 이를 묶어 레코드 집합(RRset)이라고 하고, 응답은 집합 전체를 함께 돌려줍니다. 레코드를 읽을 때는 어느 이름의 어느 타입인지를 먼저 확인하고 값을 봐야 합니다. 같은 응답에 여러 이름의 레코드가 섞여 오기 때문입니다.

DNS · 레코드 읽기

이름과 TYPE을 먼저 정하고 값과 제약을 읽는다

A·AAAA의 주소, CNAME의 별칭, MX·NS의 서버 이름은 서로 다른 데이터입니다. 아래 이름·주소는 형식 예이며 TTL·클래스 등은 생략했습니다. SOA는 긴 필드 중 일부만 표시합니다.

DNS 레코드의 값과 읽는 기준
타입역할과 형식 예제약·확인
A이름 → IPv4 주소 — example.com. A 93.184.216.34하나의 이름에 여러 A가 있을 수 있음
AAAA이름 → IPv6 주소 — example.com. AAAA 2606:2800:220:1::IPv6 주소용 레코드
CNAME별칭 이름 → canonical name — www CNAME example.com.같은 이름에 일반 레코드와 공존 불가
MX메일 교환 서버 지정 — example.com. MX 10 mail.example.com.숫자가 낮을수록 우선순위가 높음
NS영역의 권한 네임서버 지정 — example.com. NS ns1.example.com.위임과 권한 서버 확인에 사용
TXT텍스트 기반 정책/검증 정보 — v=spf1 include:_spf.example.net -allSPF, DKIM, DMARC 등 형식별 규칙 확인 필요
SRV서비스 위치와 포트 지정 — _sip._tcp.example.com. SRV 10 60 5060 host.example.com.지원하는 프로토콜에서만 의미가 있음
SOA영역 관리 메타데이터 (일부) — ns1.example.com. hostmaster.example.com.serial, refresh, retry, expire 포함
PTR역방향 이름 → canonical name — 34.216.184.93.in-addr.arpa. PTR ...IP 소유자가 관리하는 역방향 영역 필요
CAA인증서 발급 가능한 CA 제한 — example.com. CAA 0 issue "letsencrypt.org"CA의 오발급 위험을 줄이는 정책
A
역할과 형식 예: 이름 → IPv4 주소 — example.com. A 93.184.216.34
제약·확인: 하나의 이름에 여러 A가 있을 수 있음
AAAA
역할과 형식 예: 이름 → IPv6 주소 — example.com. AAAA 2606:2800:220:1::
제약·확인: IPv6 주소용 레코드
CNAME
역할과 형식 예: 별칭 이름 → canonical name — www CNAME example.com.
제약·확인: 같은 이름에 일반 레코드와 공존 불가
MX
역할과 형식 예: 메일 교환 서버 지정 — example.com. MX 10 mail.example.com.
제약·확인: 숫자가 낮을수록 우선순위가 높음
NS
역할과 형식 예: 영역의 권한 네임서버 지정 — example.com. NS ns1.example.com.
제약·확인: 위임과 권한 서버 확인에 사용
TXT
역할과 형식 예: 텍스트 기반 정책/검증 정보 — v=spf1 include:_spf.example.net -all
제약·확인: SPF, DKIM, DMARC 등 형식별 규칙 확인 필요
SRV
역할과 형식 예: 서비스 위치와 포트 지정 — _sip._tcp.example.com. SRV 10 60 5060 host.example.com.
제약·확인: 지원하는 프로토콜에서만 의미가 있음
SOA
역할과 형식 예: 영역 관리 메타데이터 (일부) — ns1.example.com. hostmaster.example.com.
제약·확인: serial, refresh, retry, expire 포함
PTR
역할과 형식 예: 역방향 이름 → canonical name — 34.216.184.93.in-addr.arpa. PTR ...
제약·확인: IP 소유자가 관리하는 역방향 영역 필요
CAA
역할과 형식 예: 인증서 발급 가능한 CA 제한 — example.com. CAA 0 issue "letsencrypt.org"
제약·확인: CA의 오발급 위험을 줄이는 정책

주소와 이름은 형식 예시입니다. 실제 질의에서는 owner name과 TYPE을 먼저 고정하고 응답의 TTL·권한 플래그를 함께 읽습니다.

자주 보는 레코드

A와 AAAA는 이름을 각각 IPv4와 IPv6 주소로 연결합니다. 한 이름에 여러 개를 둘 수 있고, 클라이언트는 그 가운데 하나를 골라 접속합니다. CNAME은 이름을 다른 이름의 별칭으로 만듭니다. www.example.com이 app.example.com의 CNAME이라면, 리졸버는 www를 물었을 때 app의 레코드를 다시 찾아 함께 돌려줍니다.

NS는 그 영역을 맡은 권한 서버의 이름을, SOA(start of authority)는 영역의 관리 정보를 담습니다. SOA에는 주 서버의 이름과 관리자의 메일 주소, 영역이 바뀔 때마다 올리는 일련번호(serial), 보조 서버가 변경을 확인하는 주기 같은 값이 들어 있고, 마지막 값은 지금은 이름이 없다는 응답을 캐시할 시간으로 쓰입니다.

MX는 그 도메인으로 오는 메일을 받을 서버를, TXT는 자유로운 문자열을 담습니다. TXT는 그 자체로는 의미가 없는 그릇이어서, 아래에서 볼 메일 보안 정책이나 도메인 소유 확인 문자열처럼 각 서비스가 정한 형식으로 씁니다. SRV는 특정 서비스의 서버와 포트를 알려 주고, CAA는 그 도메인의 인증서를 발급할 수 있는 인증 기관을 제한합니다.

PTR은 거꾸로 주소에서 이름을 찾는 역방향 조회에 씁니다. 주소를 이름처럼 다루기 위해 IPv4는 옥텟을, IPv6는 16진수 한 자리씩을 뒤집어 in-addr.arpa나 ip6.arpa 아래의 이름으로 만듭니다. Python의 ipaddress 모듈은 이 이름을 바로 만들어 줍니다.

from ipaddress import ip_address

for addr in ["192.0.2.44", "2001:db8::1"]:
    print(ip_address(addr).reverse_pointer)
44.2.0.192.in-addr.arpa
1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa

뒤집는 이유는 DNS가 오른쪽을 상위 계층으로 읽기 때문입니다. 주소는 왼쪽이 큰 단위이므로, 뒤집어야 192.0.2.0/24 블록을 2.0.192.in-addr.arpa라는 영역 하나로 위임할 수 있습니다. 그래서 PTR 레코드는 도메인의 주인이 아니라 그 주소 블록을 받은 통신사나 클라우드 사업자가 관리합니다.

CNAME의 사슬을 따라가기

CNAME은 다른 CNAME을 가리킬 수도 있어서, 리졸버는 원하는 타입의 레코드가 나올 때까지 사슬을 따라갑니다. 다음 코드는 이 과정을 흉내 냅니다. 원하는 타입이 없으면 빈 목록을, 사슬이 다시 앞의 이름으로 돌아오면 오류를 냅니다.

records = {
    ("www.example.com.", "CNAME"): ["app.example.com."],
    ("app.example.com.", "CNAME"): ["lb.example.net."],
    ("lb.example.net.", "A"): ["192.0.2.44", "192.0.2.45"],
    ("loop-a.example.com.", "CNAME"): ["loop-b.example.com."],
    ("loop-b.example.com.", "CNAME"): ["loop-a.example.com."],
}

def lookup(name, rtype):
    chain = [name]
    while (name, rtype) not in records:
        if (name, "CNAME") not in records:
            return chain, []                                 # 이 이름에 그 타입의 레코드가 없다
        name = records[(name, "CNAME")][0]                   # 별칭이면 대상 이름으로 다시 찾는다
        if name in chain:
            raise RuntimeError(f"CNAME 고리: {' -> '.join(chain + [name])}")
        chain.append(name)
    return chain, records[(name, rtype)]

print(lookup("www.example.com.", "A"))
print(lookup("www.example.com.", "AAAA"))
try:
    lookup("loop-a.example.com.", "A")
except RuntimeError as e:
    print(e)
(['www.example.com.', 'app.example.com.', 'lb.example.net.'], ['192.0.2.44', '192.0.2.45'])
(['www.example.com.', 'app.example.com.', 'lb.example.net.'], [])
CNAME 고리: loop-a.example.com. -> loop-b.example.com. -> loop-a.example.com.

www의 A를 찾으려면 별칭을 두 번 따라가 다른 도메인의 lb.example.net.까지 가야 합니다. 사슬의 이름마다 다른 서버에 물어야 할 수 있으므로, CNAME이 길어질수록 첫 조회가 느려집니다. AAAA는 사슬의 끝에 그 타입의 레코드가 없어서 빈 목록이 나왔는데, 이는 이름이 없다는 뜻이 아니라 이름은 있지만 그 타입의 데이터가 없다는 뜻입니다. 이 차이는 아래의 dig 응답에서 다시 봅니다. 실제 리졸버도 돌고 있거나 지나치게 긴 사슬을 발견하면 따라가기를 멈추고 오류를 돌려줍니다.

CNAME이 놓일 수 없는 곳

CNAME에는 중요한 제약이 있습니다. 어떤 이름에 CNAME이 있으면 그 이름에는 다른 데이터를 둘 수 없습니다(RFC 1034 §3.6.2, RFC 2181 §10.1). 별칭은 "이 이름의 모든 것은 저 이름에서 찾으라"는 뜻이므로, 같은 이름에 A나 MX가 함께 있으면 어느 쪽을 따라야 할지 정할 수 없기 때문입니다. DNSSEC 서명처럼 이 규칙을 위해 정해 둔 몇몇 예외만 있습니다.

이 제약 때문에 example.com처럼 영역의 꼭대기(zone apex)에는 CNAME을 둘 수 없습니다. 영역의 꼭대기에는 SOA와 NS가 반드시 있어야 하기 때문입니다. 그런데 example.com 자체를 CDN이나 클라우드 로드 밸런서의 이름으로 연결하고 싶은 경우가 많아서, DNS 사업자들은 ALIAS, ANAME, CNAME flattening 같은 이름의 기능을 제공합니다. 이 기능들은 표준 레코드가 아니라, 권한 서버가 대상 이름을 대신 조회해 그 결과를 A와 AAAA처럼 돌려주는 사업자별 방식입니다. 표준으로는 RFC 9460의 HTTPS 레코드가 꼭대기에서도 다른 이름을 가리키는 별칭 모드를 지원합니다.

MX와 NS의 값에도 CNAME인 이름을 쓰면 안 됩니다(RFC 2181 §10.3). 메일 서버의 이름을 바꿀 때 습관처럼 CNAME으로 연결해 두면, 일부 메일 서버가 메일을 거부하는 원인이 됩니다.

MX와 메일 보안 레코드

MX 레코드의 숫자는 선호도(preference)이며, 숫자가 작은 서버부터 먼저 시도합니다(RFC 5321 §5.1). 10 mail-a.example.com.과 20 mail-b.example.com.이 있다면 보내는 쪽은 mail-a에 먼저 연결하고, 실패하면 mail-b를 시도합니다. 선호도가 같은 서버가 여러 개라면 그 가운데 아무 순서로나 시도해 부하를 나눕니다.

메일 보안에서는 TXT 레코드에 정책을 적어 둡니다. 세 가지가 함께 쓰이며, 각각 따로 정한 형식과 규칙이 있습니다.

정책레코드를 두는 이름의 예하는 일자주 하는 실수
SPFexample.com.의 v=spf1 ...이 도메인으로 메일을 보내도 되는 서버를 밝힌다같은 이름에 SPF 레코드를 둘 이상 둔다
DKIMselector._domainkey.example.com.메일의 서명을 검증할 공개 키를 둔다selector 이름이나 긴 키 문자열을 잘못 적는다
DMARC_dmarc.example.com.의 v=DMARC1; ...SPF와 DKIM 검사의 결과를 어떻게 처리할지 정한다보낸 사람 주소의 도메인과 검사한 도메인이 일치해야 한다는 조건을 놓친다

SPF는 RFC 7208, DKIM은 RFC 6376, DMARC는 2026년에 RFC 7489를 대체한 RFC 9989에 정의되어 있습니다. 세 정책은 서로 다른 이름에 놓이므로, dig example.com TXT로는 SPF만 보입니다. DKIM과 DMARC는 위의 표에 적힌 이름을 따로 조회해야 합니다.

dig로 응답 읽기

DNS를 확인할 때 가장 많이 쓰는 도구는 dig입니다. nslookup은 Windows에도 기본으로 있어 간단히 확인하기 편하고, dig는 응답의 상태와 플래그, 각 구역을 모두 보여 주어 문제를 진단하기에 좋습니다.

dig www.example.com A                     # A 레코드를 묻는다
dig +short www.example.com A              # 값만 짧게
dig @1.1.1.1 www.example.com A            # 특정 리졸버에 묻는다
dig @ns1.example.com www.example.com A    # 권한 서버에 직접 묻는다
dig +trace www.example.com                # 루트부터 위임을 따라간다
dig -x 192.0.2.44                         # 역방향 조회
nslookup -type=MX example.com             # nslookup으로 MX를 묻는다

dig의 출력은 다음과 같은 구역으로 나뉩니다. 아래는 앞의 CNAME 사슬을 가진 영역을 재귀 리졸버에 물었을 때의 모양을, 필요한 부분만 남겨 옮긴 예입니다.

;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 4242
;; flags: qr rd ra; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1

;; QUESTION SECTION:
;www.example.com.               IN      A

;; ANSWER SECTION:
www.example.com.        300     IN      CNAME   app.example.com.
app.example.com.        300     IN      CNAME   lb.example.net.
lb.example.net.         42      IN      A       192.0.2.44

;; SERVER: 192.168.1.1#53(192.168.1.1) (UDP)

status는 응답의 결과입니다. NOERROR는 정상, NXDOMAIN은 이름이 없음, SERVFAIL은 리졸버가 답을 얻지 못했음, REFUSED는 서버가 질문을 거절했음을 뜻합니다. flags의 qr은 응답이라는 표시, rd는 재귀를 요청했다는 표시, ra는 서버가 재귀를 해 준다는 표시입니다. 권한 서버에 직접 물으면 권한 있는 답이라는 aa가 붙고, DNSSEC 검증을 마친 리졸버의 답에는 ad가 붙습니다. nslookup이 찍는 Non-authoritative answer도 aa가 없다는 뜻일 뿐, 틀렸거나 오래된 답이라는 뜻은 아닙니다.

ANSWER의 TTL도 함께 읽어야 합니다. 재귀 리졸버가 캐시에서 돌려준 답은 남은 시간을 보여 주므로, 위의 42는 이 리졸버가 그 A 레코드를 앞으로 42초 더 쓴다는 뜻입니다. 같은 명령을 몇 초 뒤에 다시 하면 숫자가 줄어든 것을 볼 수 있습니다. 권한 서버에 직접 물으면 영역에 설정된 원래 TTL이 나옵니다. 이름을 고친 뒤 결과가 바뀌지 않는다면, 권한 서버에 직접 물어 원본이 바뀌었는지와 리졸버의 캐시가 남은 시간을 나누어 확인합니다.

더 깊이: ADDITIONAL: 1의 정체, EDNS

앞의 출력에는 ADDITIONAL 구역이 보이지 않는데 머리글에는 ADDITIONAL: 1이 적혀 있습니다. 이 예처럼 덧붙일 레코드가 없는 응답이라면 그 하나는 대개 실제 데이터가 아니라 EDNS(0)의 OPT 의사 레코드(pseudo-RR)이며, dig는 이를 OPT PSEUDOSECTION이라는 구역에 따로 보여 줍니다. RFC 6891에 따르면 원래의 DNS는 UDP 메시지를 512바이트로 제한했고, EDNS는 이 제한을 넘으려고 질의와 응답의 추가 구역에 타입 41의 OPT 레코드를 넣어 서로의 기능을 알립니다.

OPT 레코드는 보통 레코드의 칸을 다른 뜻으로 빌려 씁니다. 클래스 칸에는 받을 수 있는 UDP 메시지의 크기를, TTL 칸에는 확장된 응답 코드와 버전, DNSSEC 레코드도 달라는 DO 비트를 담습니다. 연결 하나의 협상 정보일 뿐이므로 이 레코드는 캐시하거나 다른 서버로 전달하지 않습니다.

광고하는 크기는 단편화와 맞물립니다. RFC 6891은 4096바이트에서 시작하기를 권했지만, 큰 UDP 응답이 단편화되어 사라지는 문제 때문에 DNS 소프트웨어 개발사와 운영자들이 함께한 2020년 DNS 플래그 데이는 IPv6 최소 MTU 1280에서 헤더 48바이트를 뺀 1232바이트를 기본값으로 권했습니다. 2025년에 나온 정보 문서 RFC 9715는 최대 1400바이트를 권합니다. 이 크기를 넘는 응답에는 잘렸다는 TC 비트가 붙고, 리졸버는 같은 질문을 TCP로 다시 보내야 합니다.

빈 응답의 세 가지 뜻

ANSWER가 비어 있다고 모두 같은 뜻은 아닙니다(RFC 2308 §2).

응답의 모양뜻
status: NXDOMAIN, AUTHORITY에 SOA그 이름 자체가 없다
status: NOERROR, ANSWER가 비고 AUTHORITY에 SOA이름은 있지만 물어본 타입의 데이터가 없다(NODATA)
status: NOERROR, aa가 없고 ANSWER가 비고 AUTHORITY에 NS답이 아니라 다른 서버에 물으라는 위임이다(referral)

앞의 코드에서 AAAA가 빈 목록으로 나온 것이 두 번째 경우입니다. 이름이 없다고 판단해 버리면 IPv6 레코드만 추가하면 될 일을 엉뚱한 곳에서 찾게 됩니다. 세 번째는 dig +norecurse로 루트나 TLD 서버에 직접 물었을 때 보이는 모양으로, 재귀 리졸버를 거치면 리졸버가 위임을 끝까지 따라가므로 보통은 볼 일이 없습니다.

정리

  • 레코드는 이름, TTL, 클래스, 타입, 값으로 이루어지며, 어느 이름의 어느 타입인지를 먼저 확인하고 값을 읽습니다.
  • A와 AAAA는 주소를, CNAME은 별칭을, NS와 SOA는 영역의 권한 서버와 관리 정보를, MX는 메일 서버를, TXT는 서비스별 정책 문자열을, PTR은 역방향 이름을 담습니다.
  • 역방향 이름은 주소를 뒤집어 in-addr.arpa나 ip6.arpa 아래에 만들며, 주소 블록을 받은 쪽이 관리합니다.
  • CNAME이 있는 이름에는 다른 데이터를 둘 수 없으므로 영역의 꼭대기에는 쓸 수 없고, MX와 NS의 값도 CNAME이면 안 됩니다.
  • MX는 선호도가 작은 서버부터 시도하며, 메일 보안의 SPF, DKIM, DMARC는 서로 다른 이름의 TXT 레코드에 놓입니다.
  • dig의 status와 flags, TTL을 함께 읽고, 빈 ANSWER는 NXDOMAIN, NODATA, 위임 가운데 무엇인지 구별합니다.

연습 문제

1. 별칭 옆의 A와 MX의 순서

본문의 영역에 www.example.com. 300 IN A 192.0.2.45를 추가하면 규칙에 맞나요? 또 example.com의 MX가 10 mail-a.example.com.과 20 mail-b.example.com.이라면 어느 서버를 먼저 시도하나요? mail-a의 선호도만 30으로 바꾸면 어떻게 되나요?

해설 보기

www.example.com.에는 이미 CNAME이 있으므로 같은 이름에 A를 추가하면 CNAME과 다른 데이터가 함께 있을 수 없다는 규칙을 어깁니다. 사슬의 끝인 lb.example.net.에 A가 있는 것과는 다른 문제입니다. MX는 처음 조건에서 선호도 10인 mail-a를 먼저 시도하고, mail-a를 30으로 바꾸면 20인 mail-b가 먼저입니다. 선호도는 시도하는 순서일 뿐이어서, 먼저 시도한 서버에 연결할 수 없으면 다음 서버로 넘어갑니다.

2. 같은 빈 답, 다른 원인

dig api.example.com AAAA의 결과가 status: NOERROR, ANSWER: 0이고 AUTHORITY에 example.com의 SOA가 있었습니다. 동료는 "api라는 이름이 DNS에 없다"고 말합니다. 맞는 판단인가요? 이름이 정말 없었다면 응답은 어떻게 달랐을까요?

해설 보기

맞지 않습니다. 상태가 NOERROR이고 AUTHORITY에 SOA가 있으므로, api.example.com이라는 이름은 있지만 AAAA 레코드가 없는 NODATA 응답입니다. dig api.example.com A로 물으면 주소가 나올 가능성이 큽니다. IPv6로 접속하려면 AAAA를 추가해야 합니다. 이름이 정말 없었다면 상태가 NXDOMAIN으로 나왔을 것입니다. 두 경우 모두 SOA의 값에 따라 부정 응답이 캐시되므로, 레코드를 추가한 뒤에도 그 시간 동안은 빈 답이 보일 수 있습니다.

3. 메일 서버의 역방향 이름

메일 서버의 주소가 198.51.100.7입니다. 이 주소의 PTR 레코드를 담는 이름은 무엇인가요? example.com의 권한 서버에 이 PTR 레코드를 추가하면 역방향 조회가 될까요?

해설 보기

옥텟을 뒤집은 7.100.51.198.in-addr.arpa.가 PTR 레코드의 이름입니다. 이 이름은 example.com 영역이 아니라 198.51.100.0/24를 받은 쪽에 위임된 100.51.198.in-addr.arpa 영역에 속하므로, example.com의 권한 서버에 추가해도 아무도 그곳에 묻지 않습니다. 서버를 빌려준 통신사나 클라우드 사업자에게 PTR 설정을 요청하거나, 사업자가 제공하는 설정 화면에서 바꿔야 합니다. 받는 메일 서버들은 보내는 서버의 PTR이 가리키는 이름이 다시 같은 주소로 풀리는지를 확인하기도 하므로, A 레코드도 함께 맞춰 둡니다.

다음 글 DNS 심화와 실무에서는 DNS로 트래픽을 나누고 장애에 대비하는 방법과, DNS를 암호화하고 검증하는 DoH, DoT, DNSSEC을 살펴봅니다.