9.15. 이름과 DNS

지금까지의 모든 페이지는 192.168.1.20 같은 숫자 IP 주소를 사용했습니다. 실제 애플리케이션은 거의 그렇게 하지 않습니다. 서버는 example.com 이나 api.example.com 으로 이름이 붙고, 애플리케이션은 패킷을 보낼 IP를 찾기 위해 런타임에 그 이름을 조회합니다. 그 조회가 바로 Domain Name System, 즉 DNS입니다.

9.15.1. 이름이 무엇으로 해석되는가

이름은 그저 레이블일 뿐입니다. example.com 자체는 어떤 IP 정보도 담고 있지 않으며, 전화번호부에서 전화번호를 찾는 것과 같은 방식으로 조회되어야 합니다. DNS 인프라는 인터넷의 분산된 전화번호부이며, 조회 결과는 카메라가 연결할 수 있는 하나 이상의 IP 주소입니다.

example.com  ->  93.184.216.34

하나의 이름은 흔히 여러 주소로 해석됩니다(부하 분산, 지리적 이중화, 동일 서비스의 IPv4 IPv6 버전을 위해). 그 중 어느 것이든 동작합니다. 애플리케이션은 하나를 골라 시도하고, 실패하면 다음 것으로 넘어갑니다.

9.15.2. 조회는 어떻게 일어나는가

카메라가 example.com 을 요청하면:

  1. 카메라는 작은 UDP 데이터그램(그렇습니다, UDP입니다 – UDP – 패킷을 보내고 잘 되기를 바라기 참조)을 자신이 설정한 DNS 서버로 보냅니다. DNS 서버의 주소는 카메라에 자신의 IP를 건네준 바로 그 DHCP 교환에서 받은 것입니다.

  2. DNS 서버는 이미 답을 캐시에 가지고 있을 수 있습니다(최근에 같은 질의를 받은 적이 있는 경우). 그렇다면 즉시 응답합니다.

  3. 그렇지 않다면 DNS 서버는 전역 DNS 계층 구조를 따라갑니다. 즉 root 서버에 .com 에 대해 묻고, 그 서버들에 example.com 에 대해 묻고, 서버들에 해당 이름에 대해 묻습니다. 이 전체 트리 탐색은 카메라에게는 숨겨집니다. 카메라는 하나의 질의와 하나의 응답만 봅니다.

  4. DNS 서버는 다음을 위해 결과를 캐시하고, 또 다른 UDP 데이터그램으로 응답을 카메라에 돌려보냅니다.

전체 교환은 일반적으로 캐시가 따뜻할 때는 몇 밀리초, 차가울 때는 최대 백 밀리초 안팎이 걸립니다.

9.15.3. Python 인터페이스

getaddrinfo() 함수는 조회를 수행하고 소켓 생성자에 바로 건넬 수 있는 주소를 반환합니다:

import socket

addr = socket.getaddrinfo("example.com", 80)[0][-1]
print(addr)
# ('93.184.216.34', 80)

시그니처는 getaddrinfo(host, port) 입니다. 반환값은 5-튜플의 리스트 입니다(해석된 주소당 하나씩). [0] 은 첫 번째 것을 고르고, [-1] 은 마지막 필드를 고르는데, 이는 소켓에 직접 건넬 수 있는 (ip, port) 튜플입니다:

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.connect(socket.getaddrinfo("example.com", 80)[0][-1])
    s.send(b"GET / HTTP/1.0\r\nHost: example.com\r\n\r\n")
    ...

원격 서버와 통신하는 대부분의 카메라 코드는 그 한 줄로 시작합니다. asyncio 헬퍼(asyncio.open_connection())는 숫자 IP 대신 이름이 전달되면 내부적으로 조회를 수행하므로, 비동기 코드는 보통 getaddrinfo() 를 직접 호출하지 않습니다.

9.15.4. 무엇이 잘못될 수 있는가

  • 도달 가능한 DNS 서버 없음. Wi-Fi가 막 올라왔고 링크가 불안정하면 첫 getaddrinfo() 호출이 타임아웃될 수 있습니다. OSError 가 발생합니다. 링크가 안정되면 다시 시도하세요.

  • 이름이 존재하지 않음. 오타나 오래된 이름은 DNS 서버가 “그런 이름 없음”을 반환한 뒤 OSError 를 발생시킵니다. 오류 코드는 이를 “DNS 서버 도달 불가”와 구분해 주지만, 대부분의 카메라 애플리케이션에서는 재시도/포기 정책이 동일합니다.

  • 반환된 주소가 동작하지 않음. DNS는 더 이상 해당 서비스를 호스팅하지 않는 주소를 반환할 수 있습니다. 해결책은 getaddrinfo() 의 결과 리스트에서 다음 항목으로 넘어가거나, 나중에 이름을 다시 조회하는 것입니다(그때쯤이면 DNS 캐시가 갱신되어 있을 가능성이 큽니다).

  • 캡티브 포털(captive portal). 일부 Wi-Fi 네트워크는 DNS를 가로채 모든 것 에 대해 캡티브 포털 페이지의 IP를 반환합니다. 카메라는 연결된 것처럼 보이지만, 돌려받는 데이터는 실제 서비스가 보냈을 것과 일치하지 않습니다. 배포 환경에서는 흔하지 않지만, 컨퍼런스 Wi-Fi나 유사한 네트워크에서 일어나는 일입니다.

9.15.5. 카메라 자신의 이름

지금까지의 페이지들은 다른 기기의 이름을 조회했습니다. 카메라 역시 주소를 요청할 때 로컬 네트워크에 알리는 자신만의 이름을 가지고 있습니다. 기본값은 보드마다 다른 일반적인 식별자입니다. network.hostname() 으로 네트워크의 나머지 부분이 인식할 만한 이름으로 재정의할 수 있습니다:

import network
network.hostname("kitchen-cam")
# ... then bring the link up as usual ...

호스트네임은 인터페이스를 올리기 전에 설정하세요. 그래야 이름이 나중이 아니라 카메라의 초기 주소 요청의 일부로 나가게 됩니다.

카메라가 네트워크에 합류하면 이제 두 가지 일이 일어납니다. 첫째, 대부분의 가정용 라우터는 자신이 주소를 건네준 호스트네임을 자체 로컬 조회에 등록하므로, 다른 기기들이 라우터가 우연히 할당한 숫자 주소를 알 필요 없이 카메라에 kitchen-cam 으로 접근할 수 있습니다. (기업 네트워크는 이를 따를 수도 안 따를 수도 있습니다. 동작은 라우터에 달려 있습니다.) 둘째, 카메라 자체 가 별도 설정 없이 mDNS 응답기를 실행하므로, 클라이언트가 mDNS를 이해하는 모든 네트워크에서 같은 이름이 kitchen-cam.local 로도 접근 가능합니다. 대부분의 최신 데스크톱 운영체제가 이를 이해합니다.

참고

network.hostname() 에는 순수한 호스트네임을 전달하세요. 즉 .local 접미사 없이 "kitchen-cam" 만 전달합니다. .local 형식은 mDNS가 조회 시점에 덧붙이는 것입니다. 그것을 호스트네임에 박아 넣으면 카메라가 kitchen-cam.local 을 일반 호스트네임으로 알리게 되는데, 이는 조회의 양쪽 어느 쪽도 기대하지 않는 것입니다.

9.15.6. 이름만으로 충분하지 않을 때

DNS가 도움이 되지 않는 몇 가지 상황이 있습니다:

  • 로컬 네트워크에서의 검색. 표준 DNS는 전역 디렉터리에 등록된 이름에 대한 질의에 응답하며, 로컬 세그먼트에 있는 장치에 대해서는 아무것도 알지 못합니다. Multicast DNS (mDNS)는 그 공백을 메우는 시스템입니다. 참여하는 모든 장치는 로컬 네트워크의 특수한 멀티캐스트 그룹에 가입하여 질의를 수신하며, 어떤 장치가 .local 로 끝나는 이름을 요청하면 그 이름을 소유한 장치가 직접 응답합니다. 중앙 서버도, DNS 구성도 필요 없습니다. Apple 장치의 Bonjour, Linux의 Avahi, 그리고 Windows 10 이상은 모두 동일한 프로토콜을 사용하는데, 바로 이 때문에 이전 섹션에서 설정한 kitchen-cam.local 이름이 추가 구성 없이 가정용 네트워크에서 해석됩니다.

    그 교환에서 카메라 쪽은 응답기(responder) 이며, 이미 실행 중입니다. 카메라에 없는 것은 리졸버(resolver), 즉 스크립트가 네트워크에 “printer.local 은 어디 있나?”라고 묻고 답을 받게 해주는 나머지 절반입니다. 함께 제공되는 mDNS 코드는 응답기 전용입니다(임베디드 기기는 일반적으로 찾아지는 대상 이지 찾는 주체가 아닙니다). 디스커버리가 반대 방향으로 흘러야 할 때, 로컬 세그먼트의 경우에는 UDP 브로드캐스트(UDP – 패킷을 보내고 잘 되기를 바라기 참조)가 더 간단한 답이며, 또는 cbrand/micropython-mdns 같은 순수 Python 모듈이 완전한 리졸버를 추가해 줍니다.

  • IPv6 이름. getaddrinfo() 는 IPv4와 IPv6가 모두 사용 가능하면 두 결과를 모두 반환합니다. 애플리케이션의 소켓이 사용할 수 있는 패밀리를 고르세요.

대부분의 카메라 측 코드에서 getaddrinfo 는 네트워크 연결을 여는 모든 함수의 맨 위에 오는 한 줄입니다. 이 섹션의 다른 곳에서 "192.168.1.20" 에 대해 실행했던 예제들은 "api.example.com" 같은 공개 이름에 대해서도 모두 동일하게 동작합니다. 다만 먼저 해석하면 됩니다.