IPv6
IPv4 주소가 고갈된 과정과 128비트 IPv6 주소의 표기, 줄여 쓰는 규칙을 ipaddress 모듈로 확인합니다. 주소의 종류와 단순해진 헤더, 라우터 광고로 주소를 스스로 만드는 SLAAC와 MAC 주소로 만든 인터페이스 ID의 문제를 살펴보고, 듀얼 스택과 터널, NAT64 같은 전환 방법과 개발자가 확인할 것을 정리합니다.
앞 글의 NAT는 IPv4 주소의 부족을 임시로 덜어 주었을 뿐, 근본적인 한계는 그대로입니다. IPv4의 주소는 2³², 곧 약 43억 개인데 이미 거의 모두 나누어 주었고, 스마트폰과 IoT 기기, 클라우드의 인스턴스는 계속 늘어납니다. 이 문제에 대한 근본적인 답이 IPv6입니다.
IPv4 주소의 고갈
| 연도 | 일어난 일 |
|---|---|
| 2011 | IANA가 마지막 /8 블록들을 지역 인터넷 등록 기관(RIR)에 나누어 줌. 아시아·태평양의 APNIC가 일반 할당을 멈춤 |
| 2012 | 유럽의 RIPE NCC가 마지막 /8에 들어섬 |
| 2014 | 중남미의 LACNIC가 마지막 /10에 들어섬 |
| 2015 | 북미의 ARIN이 할당할 주소를 다 씀 |
| 2017 | 아프리카의 AFRINIC가 마지막 /8의 제한 할당 단계에 들어섬 |
지금은 이미 할당된 IPv4 주소를 사고파는 시장과, 여러 가입자가 주소 하나를 나누어 쓰는 CGNAT로 모자란 수요를 메우고 있습니다. 하지만 NAT 때문에 장치끼리 직접 연결하기 어렵고, 변환 표를 관리하는 비용이 들며, 어떤 프로토콜은 NAT 뒤에서 제대로 동작하지 않습니다.
IPv6 주소 읽기
IPv6 주소는 128비트로, 2¹²⁸ ≈ 3.4×10³⁸개가 있습니다. 16비트씩 여덟 그룹으로 나누어 각 그룹을 16진수 네 자리로 쓰고 콜론으로 잇습니다. 이렇게 쓰면 너무 길어서 두 가지로 줄여 씁니다. 그룹 앞의 0은 생략할 수 있고, 0으로만 된 그룹이 이어지는 곳은 주소 하나에서 한 번만 ::로 줄일 수 있습니다.
같은 IPv6 주소의 전체 표기와 압축 표기
16비트 그룹 여덟 개를 모두 씁니다. 보이는 그룹 수로 생략한 그룹 수를 계산합니다. 각 단계의 값과 전환 이유를 비교합니다.
IPv6 NOTATION · EIGHT GROUPS / ONE ::
압축해도 같은 128비트이며 복원 결과는 항상 여덟 그룹이다
각 그룹의 앞 0을 줄인 뒤 연속된 0 그룹 네 개를 ::로 한 번만 생략합니다. 2001:db8:1::1에는 보이는 네 그룹이 있어 8−4=4개의 0000을 채워 복원합니다.
| 표기 단계 | 주소 |
|---|---|
| 8개 그룹 전체 | 2001:0db8:0001:0000:0000:0000:0000:0001 |
| 그룹 앞의 0 생략 | 2001:db8:1:0:0:0:0:1 |
| 연속된 0 그룹 압축 | 2001:db8:1::1 |
| 복원 시 채우는 그룹 | 보이는 4개 그룹 + :: 위치의 0000 네 그룹 = 8개 |
- 8개 그룹 전체
- 주소: 2001:0db8:0001:0000:0000:0000:0000:0001
- 그룹 앞의 0 생략
- 주소: 2001:db8:1:0:0:0:0:1
- 연속된 0 그룹 압축
- 주소: 2001:db8:1::1
- 복원 시 채우는 그룹
- 주소: 보이는 4개 그룹 + :: 위치의 0000 네 그룹 = 8개
:: 한 번이어야 빠진 그룹 수와 위치가 정해진다
2001:db8:1::1 → 보이는 4개 + 생략된 4개 = 8개
주소 값은 같고 표기 길이만 달라집니다. ::를 두 번 쓰면 0 그룹을 두 위치에 몇 개씩 채울지 모호해집니다. 이 예시는 문서용 2001:db8::/32 범위입니다.
이 예의 조건
- 본문과 전체 도표의 예를 따라 전체 표기부터 복원까지 진행합니다.
관계도의 보충 설명
| 구분 | 전체 도표의 내용 |
|---|---|
| 표의 값 | 8개 그룹 전체 — 주소: 2001:0db8:0001:0000:0000:0000:0000:0001. 그룹 앞의 0 생략 — 주소: 2001:db8:1:0:0:0:0:1. 연속된 0 그룹 압축 — 주소: 2001:db8:1::1. 복원 시 채우는 그룹 — 주소: 보이는 4개 그룹 + :: 위치의 0000 네 그룹 = 8개. |
| 설명·조건 | ::를 한 번만 써야 빠진 그룹 수와 위치가 정해진다 |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 전체 표기 16비트 그룹 여덟 개를 모두 씁니다. |
|
|
| 앞 0 제거 각 그룹에서 불필요한 앞의 0만 제거합니다. |
|
|
| 연속 0 압축 연속된 0 그룹 네 개를 한 번 생략합니다. |
|
|
| 복원 보이는 그룹 수로 생략한 그룹 수를 계산합니다. |
|
|
같은 IPv6 주소의 전체 표기와 압축 표기
16비트 그룹 여덟 개를 모두 씁니다. 보이는 그룹 수로 생략한 그룹 수를 계산합니다. 각 단계의 값과 전환 이유를 비교합니다.
IPv6 NOTATION · EIGHT GROUPS / ONE ::
압축해도 같은 128비트이며 복원 결과는 항상 여덟 그룹이다
각 그룹의 앞 0을 줄인 뒤 연속된 0 그룹 네 개를 ::로 한 번만 생략합니다. 2001:db8:1::1에는 보이는 네 그룹이 있어 8−4=4개의 0000을 채워 복원합니다.
| 표기 단계 | 주소 |
|---|---|
| 8개 그룹 전체 | 2001:0db8:0001:0000:0000:0000:0000:0001 |
| 그룹 앞의 0 생략 | 2001:db8:1:0:0:0:0:1 |
| 연속된 0 그룹 압축 | 2001:db8:1::1 |
| 복원 시 채우는 그룹 | 보이는 4개 그룹 + :: 위치의 0000 네 그룹 = 8개 |
- 8개 그룹 전체
- 주소: 2001:0db8:0001:0000:0000:0000:0000:0001
- 그룹 앞의 0 생략
- 주소: 2001:db8:1:0:0:0:0:1
- 연속된 0 그룹 압축
- 주소: 2001:db8:1::1
- 복원 시 채우는 그룹
- 주소: 보이는 4개 그룹 + :: 위치의 0000 네 그룹 = 8개
:: 한 번이어야 빠진 그룹 수와 위치가 정해진다
2001:db8:1::1 → 보이는 4개 + 생략된 4개 = 8개
주소 값은 같고 표기 길이만 달라집니다. ::를 두 번 쓰면 0 그룹을 두 위치에 몇 개씩 채울지 모호해집니다. 이 예시는 문서용 2001:db8::/32 범위입니다.
이 예의 조건
- 본문과 전체 도표의 예를 따라 전체 표기부터 복원까지 진행합니다.
관계도의 보충 설명
| 구분 | 전체 도표의 내용 |
|---|---|
| 표의 값 | 8개 그룹 전체 — 주소: 2001:0db8:0001:0000:0000:0000:0000:0001. 그룹 앞의 0 생략 — 주소: 2001:db8:1:0:0:0:0:1. 연속된 0 그룹 압축 — 주소: 2001:db8:1::1. 복원 시 채우는 그룹 — 주소: 보이는 4개 그룹 + :: 위치의 0000 네 그룹 = 8개. |
| 설명·조건 | ::를 한 번만 써야 빠진 그룹 수와 위치가 정해진다 |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 전체 표기 16비트 그룹 여덟 개를 모두 씁니다. |
|
|
| 앞 0 제거 각 그룹에서 불필요한 앞의 0만 제거합니다. |
|
|
| 연속 0 압축 연속된 0 그룹 네 개를 한 번 생략합니다. |
|
|
| 복원 보이는 그룹 수로 생략한 그룹 수를 계산합니다. |
|
|
2001:db8::abcd:0:1에는 보이는 그룹이 다섯 개이므로, ::가 줄인 것은 8 - 5 = 3개의 0000입니다. 같은 주소를 여러 방식으로 쓸 수 있으므로, RFC 5952는 기록하고 출력할 때 쓰는 하나의 표기를 정했습니다. 그룹 앞의 0을 모두 없애고, 가장 긴 0의 구간을 ::로 줄이되 길이가 같으면 앞의 것을 고르며, 0 그룹 하나만 있는 곳은 줄이지 않고, 영문자는 소문자로 씁니다. 파이썬의 ipaddress 모듈이 이 규칙대로 줄인 표기(compressed)와 모두 풀어 쓴 표기(exploded)를 돌려줍니다.
import ipaddress
for text in ("2001:0db8:0000:0000:0000:abcd:0000:0001", "2001:db8:0:0:1:0:0:1", "fe80::1", "fd12:3456:789a::10"):
ip = ipaddress.IPv6Address(text)
print(f"{ip.compressed:<20} {ip.exploded}")
try:
ipaddress.IPv6Address("2001::db8::1")
except ValueError as error:
print("2001::db8::1 ->", type(error).__name__)2001:db8::abcd:0:1 2001:0db8:0000:0000:0000:abcd:0000:0001
2001:db8::1:0:0:1 2001:0db8:0000:0000:0001:0000:0000:0001
fe80::1 fe80:0000:0000:0000:0000:0000:0000:0001
fd12:3456:789a::10 fd12:3456:789a:0000:0000:0000:0000:0010
2001::db8::1 -> AddressValueError두 번째 주소에는 길이가 2인 0의 구간이 두 곳 있어, 앞의 것을 줄여 2001:db8::1:0:0:1이 되었습니다. 2001::db8::1처럼 ::가 두 번 나오면 빠진 그룹 다섯 개를 두 자리에 몇 개씩 넣을지 알 수 없으므로 올바른 주소가 아니고, 모듈은 예외를 일으킵니다. 그래서 IPv6 주소를 비교할 때는 문자열을 그대로 비교하지 말고 주소로 바꾸어 값을 비교해야 합니다.
IPv4와 무엇이 달라졌나
| 항목 | IPv4 | IPv6 |
|---|---|---|
| 주소 | 32비트, 점 십진 표기 | 128비트, 콜론으로 나눈 16진 표기 |
| 기본 헤더 | 20바이트에서 60바이트 | 40바이트로 고정. 선택 기능은 확장 헤더로 |
| 헤더 체크섬 | 있다 | 없다. 링크 계층과 위 계층의 검사에 맡긴다 |
| 단편화 | 라우터도 할 수 있다 | 보내는 호스트만 한다 |
| 브로드캐스트 | 있다 | 없다. 멀티캐스트로 대신한다 |
| 주소 자동 설정 | DHCP | SLAAC, DHCPv6 |
헤더가 고정된 길이이고 체크섬이 없으므로, 라우터는 IPv4처럼 TTL을 줄일 때마다 체크섬을 다시 계산할 필요가 없습니다. 기본 헤더에는 버전, 트래픽 종류, 흐름 표시, 데이터 길이, 다음 헤더(Next Header), 남은 홉 수(Hop Limit), 출발지와 목적지 주소가 들어 있으며, Next Header는 IPv4의 프로토콜 칸처럼 뒤에 오는 확장 헤더나 TCP, UDP를 가리켜 확장 헤더들을 사슬처럼 잇습니다. 흔히 IPv6가 IPv4보다 안전하다고 하지만, 암호화하는 IPsec은 둘 다에서 쓸 수 있으며 IPv6라고 저절로 암호화되는 것은 아닙니다.
주소의 종류
| 종류 | 범위 | 쓰임 |
|---|---|---|
| 전역 유니캐스트 | 2000::/3 | 인터넷에서 쓰는 주소 |
| 링크 로컬 | fe80::/10 | 같은 링크 안의 통신. 모든 인터페이스가 하나씩 가진다 |
| 고유 로컬(ULA) | fc00::/7, 실제로는 fd00::/8 | 조직 안에서 쓰는 주소. IPv4의 사설 주소와 비슷하다 |
| 멀티캐스트 | ff00::/8 | 그룹 통신. 브로드캐스트를 대신한다 |
| 루프백 | ::1 | 자기 자신 |
| 미지정 | :: | 아직 주소가 없다는 표시 |
IPv6에서는 인터페이스 하나가 링크 로컬 주소와 전역 주소처럼 여러 주소를 함께 갖는 것이 보통입니다. 링크 로컬 주소는 어느 링크의 것인지 함께 적어야 하므로, fe80::1%eth0처럼 뒤에 인터페이스의 이름을 붙이기도 합니다.
주소를 스스로 만드는 SLAAC
IPv6에서는 DHCP 서버가 없어도 장치가 스스로 주소를 만들 수 있는데, 이를 SLAAC(stateless address autoconfiguration)라고 합니다. 장치는 먼저 링크 로컬 주소를 만들고, 같은 주소를 쓰는 장치가 없는지 중복 주소 검사(DAD)로 확인합니다. 그다음 라우터에게 라우터 요청(router solicitation)을 보내면, 라우터는 이 링크에서 쓸 프리픽스, 예를 들어 2001:db8:1::/64를 담은 라우터 광고(router advertisement)로 답하고, 장치는 그 프리픽스 뒤에 자기가 만든 64비트 인터페이스 ID를 붙여 전역 주소를 만듭니다.
예전에는 인터페이스 ID를 MAC 주소로 만들었습니다. 수정된 EUI-64라는 이 방식은 48비트 MAC 주소의 가운데에 ff:fe를 끼우고, 첫 바이트의 U/L 비트를 뒤집습니다.
import ipaddress
def eui64_interface_id(mac):
b = bytearray(bytes.fromhex(mac.replace(":", "")))
b[0] ^= 0b10 # U/L 비트를 뒤집는다
return bytes(b[:3]) + b"\xff\xfe" + bytes(b[3:]) # 가운데에 ff:fe를 끼운다
iid = eui64_interface_id("00:1a:2b:3c:4d:5e")
link_local = ipaddress.IPv6Address(b"\xfe\x80" + bytes(6) + iid)
global_addr = ipaddress.IPv6Address(int(ipaddress.IPv6Address("2001:db8:1::")) | int.from_bytes(iid, "big"))
print("링크 로컬:", link_local)
print("전역(RA의 프리픽스 2001:db8:1::/64):", global_addr)링크 로컬: fe80::21a:2bff:fe3c:4d5e
전역(RA의 프리픽스 2001:db8:1::/64): 2001:db8:1:0:21a:2bff:fe3c:4d5e이렇게 만든 주소에는 MAC 주소가 그대로 드러나서, 장치가 어느 네트워크로 옮겨 가든 주소의 뒤 64비트가 같아 사용자를 추적할 수 있습니다. 그래서 지금은 RFC 7217처럼 네트워크마다 다르지만 같은 네트워크에서는 바뀌지 않는 인터페이스 ID를 쓰고, 바깥으로 연결할 때는 RFC 8981의 임시 주소를 주기적으로 바꾸어 쓰도록 권합니다. 주소를 관리자가 더 엄격하게 관리해야 하는 곳에서는 DHCPv6를 함께 씁니다.
IPv4에서 IPv6로 옮겨 가기
인터넷 전체가 한꺼번에 IPv6로 바꿀 수는 없으므로, 둘이 함께 쓰이는 시기를 위한 방법들이 있습니다.
| 방법 | 동작 | 따질 것 |
|---|---|---|
| 듀얼 스택 | 장치와 네트워크가 IPv4와 IPv6를 함께 쓴다 | 가장 직관적이지만 두 가지를 모두 운영해야 한다 |
| 터널 | IPv6 패킷을 IPv4 패킷 안에 담아 IPv4 망을 건넌다 | 헤더가 더해져 MTU가 줄어든다 |
| NAT64와 DNS64 | IPv6만 쓰는 장치가 IPv4 서버에 닿도록 주소를 바꾸어 준다 | 주소가 담긴 프로토콜과 DNSSEC에 제약이 있다 |
이미 많은 휴대전화망이 단말에는 IPv6만 주고, IPv4 서비스에는 NAT64로 닿게 합니다. 그래서 서비스가 IPv6를 지원하지 않으면, 그 사용자들은 변환을 한 번 더 거쳐서만 접속할 수 있습니다.
더 깊이: 듀얼 스택 클라이언트는 어느 주소로 먼저 연결하나
이름을 풀었더니 IPv4와 IPv6 주소가 모두 나왔다면, 클라이언트는 어느 것으로 먼저 연결할지 정해야 합니다. RFC 6724의 기본 주소 선택 규칙은 대개 IPv6를 앞에 둡니다. 그런데 서버의 IPv6 경로만 어딘가에서 끊겨 있다면, 주소를 차례로 시도하는 클라이언트는 IPv6 연결이 타임아웃될 때까지 기다린 뒤에야 IPv4로 넘어가므로 IPv4만 쓰는 클라이언트보다 훨씬 늦게 연결됩니다.
RFC 8305의 해피 아이볼스(Happy Eyeballs)는 이 문제를 경주로 풉니다. 클라이언트는 주소를 RFC 6724의 순서로 정렬하되 IPv6와 IPv4를 번갈아 섞고, 앞의 시도가 끝나기를 기다리지 않고 권장값 250ms가 지나면 다음 주소로 연결을 하나 더 시작해 먼저 맺어진 연결을 씁니다. A 응답이 AAAA 응답보다 먼저 오면 50ms쯤 더 기다려 IPv6가 우선권을 잃지 않게 합니다. 이 RFC는 처음 판인 RFC 6555를 대체한 두 번째 판이며, 2026년 현재 IETF에서 세 번째 판을 초안으로 다듬고 있습니다.
이 알고리즘이 저절로 쓰이는 것은 아닙니다. Python의 socket.create_connection()은 문서에 적힌 대로 IPv4와 IPv6 주소를 모두 구한 뒤 하나씩 차례로 시도하므로, IPv6가 끊긴 상황에서는 앞의 지연을 그대로 겪습니다. asyncio의 loop.create_connection()은 Python 3.8부터 happy_eyeballs_delay 인자를 주었을 때만 이 방식으로 연결합니다.
개발자가 확인할 것
| 확인할 곳 | 확인할 것 |
|---|---|
| 서버 소켓 | IPv6 주소 ::로 바인딩하거나 IPv4와 IPv6를 함께 받는다. 파이썬은 socket.create_server(addr, family=socket.AF_INET6, dualstack_ipv6=True)로 둘 다 받을 수 있다 |
| 데이터베이스 | 주소를 문자열로 저장한다면 IPv6는 최대 39자, IPv4를 섞은 표기는 최대 45자. 가능하면 주소 전용 형식이나 16바이트로 저장한다 |
| 입력 검증과 로그 | 콜론이 든 주소를 받아들이고 파싱하는가. URL에서는 http://[2001:db8::1]:8080/처럼 대괄호로 감싼다 |
| 로드 밸런서와 클라우드 | IPv6 리스너와 VPC의 IPv6 블록이 설정되어 있는가 |
ip -6 addr show # 인터페이스의 IPv6 주소들
ip -6 route show # IPv6 라우팅 표
ip -6 neigh show # 이웃 발견(NDP)으로 알아낸 이웃. ARP의 IPv6판
ping -6 ::1 # IPv6 루프백
dig AAAA example.com +short # 이 이름의 IPv6 주소
curl -6 -I https://example.com # IPv6로만 접속해 보기ip -6 addr show에는 전역 주소와 함께 fe80::로 시작하는 링크 로컬 주소가 나오는데, 앞에서 본 것처럼 IPv6를 쓰는 인터페이스는 링크 로컬 주소를 하나씩 갖기 때문입니다. 마지막 두 명령은 함께 보면 쓸모가 있습니다. dig AAAA는 주소를 돌려주는데 curl -6이 실패한다면, 이름에는 IPv6 주소가 있지만 내 네트워크에 IPv6 경로가 없거나 그 서버가 IPv6로 응답하지 않는다는 뜻입니다.
정리
- IPv4 주소는 2011년부터 지역마다 바닥나, 주소 거래와 CGNAT로 버티고 있으며 근본적인 해결책은 IPv6입니다.
- IPv6 주소는 128비트로, 16비트씩 여덟 그룹의 16진수로 쓰고, 앞의 0과 한 번의
::로 줄여 쓰며, RFC 5952가 기록용 표기를 정합니다. - IPv6의 기본 헤더는 40바이트로 고정되고 체크섬이 없으며, 단편화는 보내는 호스트만 하고, 브로드캐스트 대신 멀티캐스트를 씁니다.
- SLAAC는 라우터 광고의 프리픽스에 인터페이스 ID를 붙여 주소를 만들며, MAC 주소로 만든 ID는 추적에 쓰일 수 있어 지금은 다른 방식을 권합니다.
- 듀얼 스택, 터널, NAT64로 IPv4와 함께 쓰는 시기를 넘기며, 서비스는 소켓과 저장, 파싱이 IPv6를 다루는지 확인해야 합니다.
연습 문제
1. 같은 길이의 0 구간
2001:0db8:0000:0000:0001:0000:0000:0001을 RFC 5952의 방식으로 줄이세요. 0의 구간이 두 곳일 때 어느 쪽을 줄여야 하나요? 2001::db8::1이 주소가 될 수 없는 이유도 설명하세요.
해설 보기
먼저 각 그룹 앞의 0을 없애면 2001:db8:0:0:1:0:0:1이고, 0의 구간이 셋째와 넷째, 여섯째와 일곱째 그룹으로 길이가 같으므로 앞의 것을 줄여 2001:db8::1:0:0:1이 됩니다. 보이는 그룹 여섯 개에 0000 두 개를 채우면 원래의 여덟 그룹으로 돌아옵니다. 2001::db8::1은 ::가 두 번 나와, 빠진 그룹 다섯 개를 두 자리에 어떻게 나누어 넣을지 정해지지 않으므로 올바른 IPv6 주소가 아닙니다.
2. 주소에서 MAC이 보인다
어떤 노트북의 IPv6 주소가 2001:db8:1:0:21a:2bff:fe3c:4d5e입니다. 이 주소에서 노트북의 MAC 주소를 알아낼 수 있나요? 이것이 왜 문제가 되며, 요즘의 운영체제는 어떻게 하나요?
해설 보기
뒤의 64비트 021a:2bff:fe3c:4d5e의 가운데에 ff:fe가 있으므로 수정된 EUI-64로 만든 인터페이스 ID로 보입니다. ff:fe를 빼면 02:1a:2b:3c:4d:5e가 남고, 첫 바이트의 U/L 비트를 다시 뒤집으면 MAC 주소 00:1a:2b:3c:4d:5e가 나옵니다. 이 노트북이 카페, 회사, 집으로 옮겨 다녀도 주소의 뒤 64비트가 늘 같으므로, 접속 기록만으로 같은 기기를 추적할 수 있습니다. 그래서 요즘의 운영체제는 네트워크마다 다른 값으로 인터페이스 ID를 만들고, 바깥으로 연결할 때는 주기적으로 바뀌는 임시 주소를 씁니다.
3. 주소를 문자열로 비교했더니
로그인 기록에서 같은 사용자의 접속을 찾으려고 IPv6 주소를 문자열로 비교했는데, 2001:DB8::1과 2001:db8:0:0:0:0:0:1이 서로 다른 주소로 집계되었습니다. 무엇이 잘못되었고 어떻게 고쳐야 하나요?
해설 보기
두 문자열은 대소문자와 줄여 쓴 정도가 다를 뿐 같은 128비트 값을 가리키는 같은 주소입니다. 문자열을 그대로 비교하면 같은 주소의 여러 표기를 다른 주소로 셉니다. 기록할 때 주소를 파싱해 RFC 5952의 표기로 바꾸어 저장하거나, 비교할 때 ipaddress.IPv6Address로 바꾸어 값으로 비교해야 합니다. 데이터베이스라면 주소 전용 형식이나 16바이트 값으로 저장하는 것이 가장 확실합니다.
다음 장의 라우팅의 기본 개념에서는 IP 주소를 보고 패킷이 여러 네트워크를 건너 목적지까지 가는 길을 정하는 라우팅을 살펴봅니다.