DNS 설정, 아직도 프로덕션을 망가뜨린다
DNS는 단순해 보이지만 고장 나면 답이 없습니다. OS 업데이트, resolv.conf, mDNS, .internal TLD가 만드는 장애 원인을 정리했습니다.

애플의 최신 macOS 릴리스 때문에 수많은 개발자의 커스텀 DNS 설정이 깨졌습니다. 구체적으로는 많은 기업이 사설 네트워크 리소스에 쓰는 .internal TLD 쿼리를 가로채서, 설정된 DNS 리졸버 대신 mDNS(멀티캐스트 DNS)로 보내기 시작했습니다. 몇 년 동안 잘 돌아가던 서비스들이 갑자기 접근이 안 되기 시작했죠. 해결 방법도 뻔하지 않았고, 에러 메시지도 도움이 안 됐습니다. 그리고 DNS 문제가 늘 그렇듯, 증상은 DNS만 빼고 다 원인처럼 보였습니다.
이 글은 DNS 설정에 관한 이야기입니다. 모든 개발자가 한 번은 마주치지만 깊이 아는 사람은 드물고, 새벽 2시에 프로덕션을 한 방에 날리기 전까지는 다들 얕보는 주제죠.
.internal TLD 사건
많은 조직이 내부 서비스에 .internal을 사설 TLD로 씁니다. 예를 들면 api.internal, db.staging.internal, grafana.internal 같은 식이죠. 공개 TLD가 아니고, '내부용'이라는 의미가 분명하며, 수십 년 동안 이렇게 써 왔으니 안전한 선택처럼 보였습니다.
그런데 애플의 새 OS가 .internal을 mDNS로 처리하기로 한 겁니다. 집 네트워크에서 myprinter.local을 풀어주는 바로 그 프로토콜이죠. 맥이 api.internal을 조회하면, 설정된 DNS 서버에 묻는 대신 로컬 네트워크에 mDNS 쿼리를 브로드캐스트합니다. DNS 서버는 이 쿼리를 아예 보지도 못합니다. 응답은 아무것도 없어서 타임아웃이 나거나, 로컬 네트워크의 누군가가 그 이름에 응답하면 엉뚱한 주소가 돌아옵니다.
증상은 이렇습니다. curl api.internal이 5초 동안 멈췄다가 실패합니다. 내부 호스트로의 SSH 연결이 타임아웃 나고, VPN 사용자는 내부 리소스에 접근하지 못합니다. Docker 컨테이너는 서비스 이름을 풀지 못하죠. 그리고 실패가 일반적인 연결 타임아웃으로 나타나기 때문에, 대부분의 개발자는 VPN, 방화벽, 애플리케이션을 붙잡고 디버깅합니다. DNS만 빼고요.
최신 시스템에서 DNS 조회가 실제로 동작하는 방식
'DNS는 단순하다'는 멘탈 모델, 즉 컴퓨터가 DNS 서버에 IP를 묻고 서버가 응답한다는 그림은 이제 몇 년째 정확하지 않습니다. 현대 시스템에는 여러 단계로 된 리졸루션 파이프라인이 있고, 각 단계가 쿼리를 가로채거나 수정하거나 우회시킬 수 있습니다.
DNS resolution pipeline on a typical macOS/Linux system:
Application calls getaddrinfo("api.internal")
↓
1. /etc/hosts — checked first. Static overrides.
↓
2. NSSwitch / resolver configuration
- Linux: /etc/nsswitch.conf controls lookup order
- macOS: /etc/resolver/* for per-domain DNS servers
↓
3. System resolver (systemd-resolved, mDNSResponder)
- May intercept .local, .internal, or other special TLDs
- May apply DNSSEC validation
- May cache responses
↓
4. DNS server (from DHCP, VPN, or manual config)
- Queries sent to configured upstream resolver
- May go through a corporate DNS proxy
↓
5. Recursive resolver (8.8.8.8, 1.1.1.1, corporate)
- Walks the DNS hierarchy: root → TLD → authoritative
↓
Response flows back up the chain
각 단계에서 무언가 잘못될 수 있습니다. 시스템 리졸버가 쿼리를 설정된 DNS 서버에 닿기 전에 가로챌 수 있고, VPN이 DNS 설정을 덮어쓸 수 있으며, DHCP가 내려준 DNS 서버가 수동 설정보다 우선할 수도 있습니다. 게다가 모든 계층의 캐싱 때문에 수정 사항이 바로 반영되지 않을 수도 있죠.
/etc/resolv.conf라는 거짓말
리눅스에서 /etc/resolv.conf는 전통적으로 DNS 설정의 유일한 진실의 원천이었습니다. 거기에 네임서버를 적으면 시스템이 그걸 쓰는 식이었죠. 간단했습니다.
요즘 리눅스 배포판에서는 이게 훨씬 복잡해졌습니다. systemd-resolved를 쓰는 환경(우분투, 페도라, 대부분의 systemd 기반 배포판이 기본값으로 사용)에서는 /etc/resolv.conf가 127.0.0.53의 스텁 리졸버를 가리키는 심볼릭 링크입니다. 실제 DNS 설정은 systemd-resolved의 상태로 존재하고, resolvectl로 관리합니다. /etc/resolv.conf를 직접 편집하면 다음 네트워크 변경 때 덮어써지거나, 스텁 리졸버를 망가뜨립니다.
# What you THINK your DNS config is:
$ cat /etc/resolv.conf
nameserver 127.0.0.53 # This is systemd-resolved's stub
# What your DNS config ACTUALLY is:
$ resolvectl status
Global:
DNS Servers: 8.8.8.8 8.8.4.4
DNS Domain: ~.
Link 2 (eth0):
DNS Servers: 10.0.0.1 # From DHCP — this takes precedence
DNS Domain: corp.internal
Link 5 (wg0): # WireGuard VPN
DNS Servers: 10.100.0.1
DNS Domain: ~internal # VPN claims the .internal domain
# Three different DNS servers, each handling different domains.
# /etc/resolv.conf shows none of this.
Docker는 한 겹을 더 얹습니다. Docker 컨테이너는 자체 /etc/resolv.conf를 갖는데, 보통 컨테이너가 시작될 때 호스트 설정을 복사해 옵니다. 호스트의 DNS가 바뀌면(VPN 연결, 네트워크 전환 등) 실행 중인 컨테이너는 재시작 전까지 예전 DNS 설정을 계속 씁니다. '재시작하면 되는데 한동안 지나면 또 깨진다'는 버그의 흔한 원인이죠.
Split DNS와 VPN 충돌
Split DNS, 즉 도메인마다 다른 DNS 서버를 쓰는 방식은 기업 환경에서 표준입니다. VPN이 *.corp.internal 쿼리는 회사 DNS 서버로 보내고, 공용 쿼리는 평소 쓰는 리졸버로 보내는 식이죠. 제대로 설정되어 있으면 잘 동작하지만, 잘못되어 있으면 아주 헷갈리는 방식으로 실패합니다.
자주 보이는 실패 패턴은 다음과 같습니다.
- VPN이 DNS 서버를 전역 기본값으로 밀어 넣습니다. 일부 VPN 클라이언트는 내부 쿼리만이 아니라 모든 쿼리의 기본 리졸버로 VPN의 DNS 서버를 설정합니다. 그러면 개인적인 웹 서핑을 포함한 모든 DNS 쿼리가 회사 DNS 서버를 거치게 됩니다. 회사 DNS가 다른 데이터센터에 있으니 느리고, 프라이버시 문제이기도 합니다.
- DNS 서버 순서 충돌. DNS 서버가 여러 개 설정되어 있으면 시스템은 순서대로 시도합니다. 첫 번째 서버가 느리거나 도달할 수 없으면(VPN은 끊겼는데 DNS 설정은 남아 있는 경우), 모든 DNS 쿼리가 두 번째 서버로 넘어가기 전에 타임아웃을 기다립니다. 그러면 모든 연결에 5초씩 지연이 붙습니다.
- 검색 도메인이 조용히 붙습니다. 검색 도메인이
corp.internal이면api쿼리는api.corp.internal도 함께 시도합니다.ssh api처럼 짧게 쓸 수 있어 편하지만, 짧은 호스트 이름이 내부 서비스 이름과 겹치면 쿼리가 엉뚱한 IP로 풀리게 됩니다. - mDNS가 내부 도메인을 가로챕니다.
.internal사건이 보여주듯, 시스템 리졸버는 설정된 어떤 DNS 서버에도 닿기 전에 특정 TLD 쿼리를 가로챌 수 있습니다. 가로채기가 조용히 일어나서 에러 메시지 없이 타임아웃이나 잘못된 응답만 돌아오니 특히 골치 아픕니다.
DNS 디버깅: 도구 모음
DNS가 깨지면 조회 파이프라인의 각 단계에서 실제로 무슨 일이 일어나는지 보여주는 도구가 필요합니다.
# 1. Check what your system actually resolves:
$ dig api.internal
# Shows: query sent to which server, response received
# The 'SERVER' line tells you which DNS server answered
# 2. Query a specific DNS server directly:
$ dig @10.100.0.1 api.internal
# Bypasses the system resolver — goes directly to the specified server
# If this works but 'dig api.internal' doesn't, the problem is
# in the system resolver, not the DNS server
# 3. Check systemd-resolved state (Linux):
$ resolvectl query api.internal
# Shows which interface/server handled the query
# 4. Check macOS DNS routing:
$ scutil --dns
# Shows per-domain DNS configuration on macOS
# Look for your .internal domain — is it being routed correctly?
# 5. Monitor DNS queries in real time:
$ sudo tcpdump -i any port 53
# See actual DNS packets. If no packets go out for your query,
# the system resolver is intercepting it locally.
# 6. Flush DNS cache (when fixing config):
$ sudo dscacheutil -flushcache && sudo killall -HUP mDNSResponder # macOS
$ resolvectl flush-caches # Linux with systemd-resolved
가장 중요한 진단 단계는 dig @server hostname으로 DNS 서버에 직접 질의하는 것입니다. 이것은 되는데 일반 조회가 안 된다면, 문제는 애플리케이션과 DNS 서버 사이, 즉 시스템 리졸버의 가로채기, 캐싱, 라우팅에 있습니다. 직접 질의도 실패한다면 문제는 DNS 서버 자체이거나 그곳까지의 네트워크 연결입니다.
사설 TLD 고르기
.internal 사건은 현실적인 질문을 던집니다. 내부 서비스에는 어떤 TLD를 써야 할까요? 어디에도 완벽하게 안전한 답은 없지만, 어떤 선택지는 다른 것보다 낫습니다.
- .internal — RFC 6762가 내부용으로 예약해 두었지만, 애플의 mDNS 처리 때문에 macOS에서는 문제가 됩니다. 리눅스에서는 잘 동작할 수도 있습니다.
- .local — mDNS용으로 예약되어 있습니다(RFC 6762). DNS로 풀리는 서비스에 쓰면 Bonjour/Avahi와 충돌합니다. 쓰지 마세요.
- .corp, .home, .mail — 예약되지 않은 이름입니다. ICANN이 언제든 공개 TLD로 배정할 수 있어서, 그러면 내부 조회가 깨지게 됩니다. 쓰지 마세요.
- 소유한 도메인의 서브도메인.
internal.yourcompany.com처럼 쓰는 방식이 가장 안전합니다. 상위 도메인을 직접 통제하니 충돌할 위험이 없습니다. 단점은 타이핑이 조금 길어진다는 것입니다. - .test, .example, .invalid, .localhost — RFC 6761에 따라 예약되어 있고, 공개 TLD로 배정되지 않는다고 보장됩니다. 프로덕션이 아닌 내부 서비스에는
.test가 가장 깔끔한 선택입니다.
지루하지만 정확한 답은, 직접 소유한 도메인의 서브도메인을 쓰는 것입니다. api.internal.yourcompany.com은 모호하지 않고, 아무것과도 충돌하지 않으며, 모든 OS와 DNS 리졸버에서 올바르게 동작합니다. 길긴 해도, 새벽 2시에 겪는 DNS 문제가 몇 글자 더 치는 것보다 훨씬 괴롭습니다.
프로덕션 시스템을 위한 교훈
DNS 장애는 매번 같은 교훈을 남기는데, 우리는 계속 다시 배우고 있습니다.
- DNS 서버만이 아니라 DNS 조회를 모니터링하세요. DNS 서버가 건강하다고 조회가 된다는 뜻은 아닙니다. 애플리케이션이 필요한 호스트 이름을 실제로 풀 수 있는지, 전체 경로를 모니터링하세요. 핵심 호스트 이름을 30초마다 조회하는 합성 체크가 서버 모니터링보다 문제를 빨리 잡아냅니다.
- 내부 DNS에는 짧은 TTL을 설정하세요. 내부 DNS 레코드를 바꿔야 할 때 긴 TTL은 오래된 캐시 데이터를 의미합니다. 내부 서비스에 60초 TTL을 주면 리졸버 부하는 적으면서도 빠른 페일오버가 가능합니다.
- OS 업데이트를 DNS 설정에 대해 테스트하세요.
.internal장애는 macOS 사용자에게만 영향을 줬습니다. 주요 OS 업데이트 후에는 개발 환경을 점검하세요. DNS 조회 동작 변경은 릴리스 노트에 거의 나오지 않습니다. - DNS 아키텍처를 문서화하세요. 어떤 DNS 서버가 어떤 도메인을 맡는지, 분할 지점은 어디인지, VPN이 끊기면 무슨 일이 일어나는지 적어두세요. 이 문서는 장애 때 가장 먼저 필요하고, 누군가는 가장 마지막에 쓰게 되는 문서입니다.
- 대체 수단을 마련하세요. 내부 서비스의 DNS 조회가 실패하면 애플리케이션이 하드코딩된 IP를 쓸 수 있나요? 우아한 방법은 아니지만, 동작하는 하드코딩 IP가 풀리지 않는 DNS 이름보다 낫습니다.
DNS는 잘 동작할 때는 보이지 않고, 동작하지 않을 때는 재앙이 되는 기반 서비스입니다. .internal 사건은 DNS 설정이 생각보다 훨씬 깨지기 쉽다는 것을 상기시켜 줍니다. OS 업데이트 하나가 수백만 사용자의 조회 동작을 바꿀 수 있죠. DNS는 언제든 깨질 수 있다고 가정하고 시스템을 만드세요. 그러면 실제로 깨졌을 때 덜 당황하게 될 겁니다.


