웹 애플리케이션 개발 과정에서 스테이징 서버 도메인의 IP를 로컬 개발 환경으로 전환하기 위해 /etc/hosts 파일을 수정하거나 DNS 레코드를 갱신한 직후, 브라우저와 터미널이 여전히 과거의 공인 IP를 바라보는 문제는 개발 환경에서 매우 빈번하게 발생합니다. 일반적인 리눅스 환경과 달리 macOS는 전통적인 /etc/resolv.conf 파일을 단순 참조하는 구조가 아니기 때문에, 터미널 명령 한 줄로 캐시를 비웠다고 생각해도 소켓 연결은 여전히 이전 목적지로 향하곤 합니다. 이 현상은 macOS 고유의 다중 계층 리졸버 구조와 백그라운드에서 상시 구동되는 mDNSResponder 데몬의 캐싱 정책이 맞물려 발생합니다.
DNS 캐시 불일치와 hosts 미반영 증상
/etc/hosts 파일에 도메인을 추가하고 저장을 마쳤음에도 curl이나 브라우저 요청이 외부 공인 IP로 전송되어 타임아웃이 발생하거나 엉뚱한 SSL 인증서 오류(ERR_CERT_COMMON_NAME_INVALID)가 반환되는 현상이 대표적입니다.
개발자를 가장 혼란스럽게 만드는 부분은 진단 도구마다 반환하는 IP 주소가 제각각이라는 점입니다. 네트워크 진단을 위해 터미널에서 dig나 nslookup을 실행하면 방금 변경된 새로운 DNS 레코드나 로컬 매핑이 정상적으로 출력됩니다. 그러나 실제로 애플리케이션 트래픽을 생성하는 ping, curl, 그리고 웹 브라우저는 과거의 캐시된 IP 주소를 고수합니다.
# 네임서버 직접 질의 (캐시를 우회하여 최신 외부 레코드 출력)
dig +short api.example.com
# 시스템 리졸버 질의 (mDNSResponder 캐시가 반환되어 구버전 IP 노출)
dscacheutil -q host -a name api.example.com
# 실제 네트워크 연결 시도 (캐시된 이전 IP로 연결 시도)
ping -c 1 api.example.com
이러한 불일치는 dig 명령어가 운영체제의 시스템 리졸버 API를 거치지 않고 네트워크 인터페이스를 통해 외부 DNS 서버와 직접 UDP 통신을 수행하기 때문에 발생합니다. 반면 일반 애플리케이션과 유틸리티는 POSIX 표준인 getaddrinfo() 또는 CoreFoundation 프레임워크의 네트워크 API를 호출하므로 시스템 전역 DNS 캐시 테이블에 먼저 접근하게 됩니다.
macOS 통합 로깅 시스템(Unified Logging System)을 통해 mDNSResponder의 내부 동작을 추적해 보면, 새로운 네트워크 질의를 외부로 보내지 않고 메모리 캐시에서 곧바로 이전 레코드를 반환하는 과정을 실시간으로 확인할 수 있습니다:
# mDNSResponder의 DNS 질의 및 캐시 응답 로그 실시간 필터링
log stream --predicate 'process == "mDNSResponder"' \
--info \
--style compact
로그 출력에서 CacheHit 이벤트가 연속적으로 기록되거나, 유효 수명(TTL)이 만료되지 않은 과거 레코드가 즉시 반환되는 패턴이 관측된다면 데몬 레벨의 캐시 테이블이 고착된 상태로 판단할 수 있습니다.
macOS 리졸버와 mDNSResponder 아키텍처
macOS의 도메인 이름 해석 체계는 유닉스 계열 운영체제 중에서도 가장 복잡한 동적 계층 구조를 갖추고 있습니다.
리눅스 배포판이 /etc/resolv.conf에 선언된 네임서버 목록을 순차 조회하는 단일 구조를 채택한 것과 달리, macOS는 SystemConfiguration 프레임워크와 configd 데몬이 네트워크 환경 변화를 감지하여 인터페이스별로 독립된 리졸버를 동적으로 생성합니다. 터미널에서 scutil --dns를 실행하면 현재 시스템에 활성화된 복수의 리졸버 구성을 상세히 확인할 수 있습니다.
# 현재 시스템의 동적 리졸버 및 인터페이스별 DNS 정책 확인
scutil --dns
출력 결과를 분석해 보면 전역 질의를 처리하는 기본 리졸버(resolver #1) 외에도, 특정 네트워크 인터페이스(Wi-Fi의 en0, 유선 랜의 en1, VPN 가상 터널의 utun0 등)에 바인딩된 스코프 리졸버(Scoped Resolver)들이 독립적으로 등록되어 있습니다. 사내망 VPN에 접속하거나 도커 가상 네트워크를 띄울 때 도메인 해석이 충돌하는 이유가 바로 이 스코프 리졸버들 간의 라우팅 우선순위 때문입니다.
이 복잡한 리졸버 체계의 정점에서 모든 질의를 통합 중계하고 캐싱하는 핵심 데몬이 바로 /usr/sbin/mDNSResponder입니다. 과거 이름에서 알 수 있듯 본래 로컬 네트워크의 제로콘프(Bonjour, .local 도메인)를 처리하기 위한 멀티캐스트 DNS 데몬으로 시작했으나, macOS 버전이 거듭되면서 시스템 전체의 유니캐스트 DNS 질의까지 통합 관리하는 전역 DNS 프록시로 역할이 확장되었습니다.
사용자 공간(User Space)의 애플리케이션이 호스트 이름을 요청하면, 커널 소켓을 통해 mDNSResponder 데몬에 질의가 전달됩니다. mDNSResponder는 자체 메모리에 보관 중인 DNS 캐시 테이블을 가장 먼저 탐색하며, 여기에 유효한 레코드가 남아 있다면 /etc/hosts 파일의 변경 사항이나 물리적인 네트워크 어댑터 설정 변경을 무시하고 즉시 응답을 돌려줍니다.
따라서 macOS의 DNS 캐시를 완벽히 갱신하려면 Directory Service 캐시 플러시와 mDNSResponder 데몬 시그널 전달이 동시에 이루어져야 합니다. 두 계층 중 어느 한쪽만 초기화할 경우, 메모리에 상주하는 인메모리 소켓 캐시가 잔존하여 갱신이 실패하게 됩니다.
mDNSResponder와 dscacheutil 단계별 초기화 절차
macOS에서 꼬여버린 DNS 캐시를 완전히 비우고 새로운 설정을 즉각 반영하기 위해서는 캐시 관리 유틸리티와 데몬 프로세스 신호를 순차적으로 조합해야 합니다.
첫 단계는 디렉터리 서비스 캐시를 초기화하는 dscacheutil 명령어의 실행입니다. 이 도구는 Open Directory 및 로컬 시스템 캐시 계층에 저장된 호스트 정보와 사용자 세션 캐시를 무효화합니다. 그러나 앞서 언급했듯이 이 명령은 mDNSResponder 데몬의 소켓 메모리 테이블까지 완전히 비우지는 못합니다.
따라서 두 번째 단계로 mDNSResponder 프로세스에 행업 시그널(SIGHUP)을 전송해야 합니다. 유닉스 환경에서 SIGHUP은 프로세스를 종료시키지 않고 설정 파일 재로드와 내부 캐시 초기화를 유도하는 표준 시그널입니다.
다음 복합 명령어를 터미널에 입력하여 두 초기화 작업을 단일 트랜잭션으로 일괄 실행합니다:
# macOS Sonoma, Sequoia 및 최신 버전 표준 DNS 캐시 완전 초기화
sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder
관리자 비밀번호를 입력하고 위 명령이 실행되면, dscacheutil이 상위 디렉터리 캐시를 제거한 직후 killall이 mDNSResponder에 시그널을 전달하여 소켓 테이블을 0으로 비웁니다.
만약 네트워크 인터페이스의 잦은 전환이나 비정상 종료로 인해 mDNSResponder 프로세스가 비정상 루프(Hang)에 빠져 SIGHUP 시그널에 응답하지 않는다면, macOS의 서비스 관리자인 launchd를 통해 서비스 자체를 즉시 강제 재시작해야 합니다:
# mDNSResponder 서비스 강제 중단 및 클린 재기동 (hang 상태 복구)
sudo launchctl kickstart -k system/com.apple.mDNSResponder
launchctl kickstart -k 옵션은 구버전의 unload/load 명령과 달리 launchd 내부 상태 트리를 깨뜨리지 않고 실행 중인 데몬 인스턴스만 안전하게 죽인 뒤 새 프로세스로 즉각 교체합니다. 데몬이 새로 기동되면서 /etc/hosts 파일과 네트워크 어댑터 설정을 디스크에서 완전히 새로 읽어 들이므로 모든 고착 상태가 해제됩니다.
DNS 캐시 플러시 후 정밀 검증 방법
초기화 명령을 실행한 후에는 감에 의존하지 않고, 터미널 도구를 통해 시스템 캐시가 실제로 완전히 소거되었는지 단계별로 검증해야 합니다.
가장 먼저 dscacheutil을 사용하여 시스템 디렉터리 서비스가 반환하는 호스트 정보를 직접 조회합니다:
# 시스템 리졸버가 캐시하고 있는 특정 도메인의 실제 IP 조회
dscacheutil -q host -a name api.example.com
출력 결과의 ip_address 항목에 방금 /etc/hosts에 지정한 로컬 IP나 변경된 네임서버의 최신 IP 주소가 정확히 표시되는지 확인합니다. 만약 이전 IP가 여전히 나타난다면 브라우저나 타 프로세스가 캐시를 즉시 재오염시켰을 가능성이 있습니다.
두 번째로 mDNSResponder 프로세스에 정보 시그널(SIGINFO)을 전송하여 현재 메모리에 적재된 캐시 테이블의 내부 통계를 시스템 콘솔 로그에 덤프시킬 수 있습니다:
# mDNSResponder 내부 캐시 상태 덤프 신호 전송
sudo killall -INFO mDNSResponder
# 시스템 로그에서 mDNSResponder 캐시 통계 및 요약 출력 확인
log show --predicate 'process == "mDNSResponder"' \
--info \
--last 1m | grep -i "cache"
출력된 로그 요약에서 Cache Size 항목이 최소치로 줄어들었거나 방금 전송된 도메인이 캐시 미스(Cache Miss)로 기록된 뒤 외부 네임서버로 신규 질의된 내역을 확인하면, 초기화 작업이 완벽하게 완료되었음을 기술적으로 증명할 수 있습니다.
브라우저와 개발 환경 사이드 이펙트 방지 FAQ
운영체제 레벨에서 DNS 캐시를 완전히 초기화했음에도 불구하고 개발 실무 현장에서는 브라우저, 가상 네트워크, 파일 권한 등 다양한 외부 요인으로 인해 이전 IP로 접속되는 사이드 이펙트가 빈번히 발생합니다.
터미널 캐시 플러시 후에도 Chrome 브라우저에서 이전 IP로 접속되는 원인
Google Chrome과 Microsoft Edge 등 Chromium 기반 브라우저는 운영체제의 DNS 리졸버와 별개로 브라우저 내부 메모리에 자체 DNS 캐시 테이블과 기존 TCP/QUIC 소켓 연결 풀(Socket Pool)을 유지합니다. 터미널에서 OS 캐시를 지워도 브라우저가 기존에 맺어둔 Keep-Alive 소켓을 재사용하면 과거 IP로 트래픽이 계속 흐릅니다. 브라우저 주소창에 chrome://net-internals/#sockets를 입력하고 Flush socket pools 버튼을 클릭한 뒤, chrome://net-internals/#dns에서 Clear host cache를 눌러 내부 소켓과 캐시를 완전히 비워주어야 합니다. 또한 브라우저 설정에서 DoH(DNS over HTTPS)가 활성화되어 있다면 OS의 /etc/hosts 설정을 완전히 무시하고 Cloudflare나 Google의 DoH 서버로 직접 질의하므로, 로컬 개발 시에는 DoH 설정을 일시적으로 해제해야 합니다.
VPN 연결 해제 후 인터넷 연결이 끊기거나 내부 도메인 질의가 실패하는 이유
사내 VPN 클라이언트(Cisco AnyConnect, GlobalProtect, OpenVPN 등)가 종료될 때 macOS의 SystemConfiguration 동적 데이터베이스에서 가상 터널 인터페이스(utun)의 리졸버 설정을 정상적으로 회수하지 못하는 경우가 있습니다. 이로 인해 scutil --dns 조회 시 이미 사라진 VPN 터널의 사내 DNS 서버가 여전히 우선순위 리졸버로 남아 모든 도메인 질의를 타임아웃시키는 현상이 발생합니다. 이때는 Wi-Fi 어댑터를 껐다 켜거나, networksetup -setdnsservers Wi-Fi empty 명령으로 네트워크 서비스의 DNS 목록을 초기화한 후 sudo killall -HUP mDNSResponder를 재실행하여 인터페이스 우선순위를 재정렬해야 합니다.
/etc/hosts 파일에 등록한 도메인이 시스템에서 완전히 무시되는 경우
/etc/hosts 파일을 편집할 때 흔히 발생하는 실수는 파일 권한 손상과 포맷 오류입니다. 파일의 소유권이 root:wheel이 아니거나 권한이 644(-rw-r--r--)가 아니면 mDNSResponder 데몬이 보안상의 이유로 파일 읽기를 거부합니다. 또한 파일의 가장 마지막 줄 끝에 개행 문자(New Line)가 없으면 마지막 행의 도메인 매핑이 무시될 수 있습니다. 아울러 호스트명과 IP 사이의 구분자로 특수 유니코드 공백이 들어갔는지 점검하고, 반드시 순수 ASCII 공백이나 탭 문자로 구분되어 있는지 cat -e /etc/hosts 명령어로 확인해야 합니다.
점(.)이 없는 단일 호스트명 질의 시 로컬 DNS 조회가 실패하는 이유
도메인에 점(.)이 포함되지 않은 단일 라벨 호스트명(예: localhost, mydevserver)은 DNS 해석 시 유니캐스트 DNS가 아닌 mDNS(Bonjour, 로컬 링크 멀티캐스트) 질의로 먼저 라우팅될 수 있습니다. macOS 리졸버는 점이 없는 이름을 기본적으로 로컬 서브넷 탐색 대상으로 간주하기 때문에 의도치 않은 딜레이가 발생하거나 조회가 실패할 수 있습니다. 로컬 개발 환경을 구성할 때는 api.local.test 또는 app.localhost처럼 IETF 표준 예약 도메인(.test, .localhost)을 접미사로 붙여 최소 2단계 이상의 완전한 도메인 이름(FQDN) 형태를 유지하는 것이 안정적입니다.
