Java IPv4 IPv6 변환 4가지 방법 — IPv4-Mapped, NAT64, 6to4 완전 정리

IPv4 주소 고갈이 현실이 된 지 오래지만 전 세계 서버 대부분은 여전히 IPv4로 통신한다. 한편 모바일 통신사(SK텔레콤, T-Mobile 등)는 IPv6 전용 네트워크를 빠르게 확대하고 있다. 두 프로토콜이 공존하는 기간은 앞으로도 꽤 길 수밖에 없고, Java 백엔드 개발자라면 듀얼스택 소켓을 열거나 로그를 정규화하다가 IPv4 주소를 IPv6 형식으로 바꿔야 하는 상황을 한 번쯤 만나게 된다. 이 글에서는 Java IPv4 IPv6 변환에 쓸 수 있는 네 가지 방식 — IPv4-Mapped, IPv4-Compatible, NAT64, 6to4 터널링 — 을 RFC 근거와 코드 예제로 정리한다. 결론부터 말하면 대부분의 경우 IPv4-Mapped 하나면 충분하지만, 나머지 세 가지도 레거시 로그나 특수 네트워크 환경에서 마주칠 수 있으므로 구조는 알아 두는 편이 낫다.

IPv4 32비트와 IPv6 128비트, 왜 “변환”이 아니라 “매핑”인가

IPv4는 32비트, IPv6는 128비트다. 주소 길이만 다른 게 아니라 헤더 구조와 라우팅 방식 자체가 다르다. 그래서 IPv4 주소를 IPv6로 1:1 “변환”한다는 표현은 좀 부정확하다. 실제로는 128비트 공간 안에 32비트 IPv4 주소를 끼워 넣는 “매핑”이나 “임베딩”에 가깝다.

RFC 4291 Section 2.5.5는 이 임베딩 규칙을 두 가지로 정의한다. 상위 96비트에 특정 프리픽스를 배치하고 하위 32비트에 IPv4 주소를 넣는 구조다. Java의 InetAddress 계층은 이 구조를 Inet4AddressInet6Address로 구분하지만, 내부적으로 IPv4-Mapped 주소를 파싱하면 Inet4Address 인스턴스를 반환하는 특이한 동작을 보인다. 이 점을 모르면 instanceof Inet6Address 검사에서 예상치 못한 결과를 만날 수 있다.

IPv4-Mapped IPv6 — 가장 실용적이고 권장되는 선택

IPv4-Mapped IPv6 주소는 ::ffff: 프리픽스 뒤에 IPv4 주소를 붙인 형태(::ffff:192.168.1.1)로, 듀얼스택 환경에서 IPv6 소켓이 IPv4 트래픽을 수신할 때 운영체제가 자동으로 사용하는 표준 방식이다. RFC 4291 Section 2.5.5.2에 정의되어 있으며, 2026년 현재 네 가지 방식 중 가장 널리 쓰이고 권장되는 접근이다.

import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.UnknownHostException;

public class IPv4MappedConverter {

    public static Inet6Address toIPv4Mapped(Inet4Address ipv4) throws UnknownHostException {
        byte[] ipv4Bytes = ipv4.getAddress(); // 4바이트
        byte[] ipv6Bytes = new byte[16];

        // 상위 10바이트는 0x00 (기본값)
        ipv6Bytes[10] = (byte) 0xff;
        ipv6Bytes[11] = (byte) 0xff;
        // 하위 4바이트에 IPv4 주소 복사
        System.arraycopy(ipv4Bytes, 0, ipv6Bytes, 12, 4);

        return Inet6Address.getByAddress(null, ipv6Bytes, null);
    }

    public static void main(String[] args) throws UnknownHostException {
        Inet4Address ipv4 = (Inet4Address) InetAddress.getByName("192.168.1.1");
        Inet6Address mapped = toIPv4Mapped(ipv4);
        System.out.println(mapped.getHostAddress());
        // 출력: 0:0:0:0:0:ffff:c0a8:101
    }
}
Java

16바이트 배열을 직접 구성하는 방식이다. 인덱스 10, 11번에 0xff를 채우고 12~15번에 IPv4 바이트를 복사하면 된다. Inet6Address.getByAddress가 이 바이트 배열로 IPv6 주소 인스턴스를 만들어 준다. 여기서 한 가지 함정이 있는데, InetAddress.getByName("::ffff:192.168.1.1")을 호출하면 Java가 Inet6Address가 아니라 Inet4Address를 반환한다. Inet6Address 타입이 필요하면 반드시 바이트 배열 방식을 써야 한다.

IPv4-Compatible IPv6 — 왜 더 이상 쓰면 안 되는가

IPv4-Compatible IPv6 주소(::w.x.y.z)는 프리픽스 96비트가 모두 0인 형태로, 초기 IPv6 전환 메커니즘의 일부였다. 그러나 RFC 4291 Section 2.5.5.1에서 공식적으로 deprecated 처리되었다. 실무에서 새로 작성하는 코드에 이 방식을 적용할 이유는 전혀 없지만, 레거시 시스템 마이그레이션이나 로그 파싱 과정에서 마주칠 수 있으므로 구조를 이해해 둘 필요가 있다.

public class IPv4CompatibleConverter {

    public static Inet6Address toIPv4Compatible(Inet4Address ipv4) throws UnknownHostException {
        byte[] ipv4Bytes = ipv4.getAddress();
        byte[] ipv6Bytes = new byte[16];

        // 상위 12바이트 모두 0x00 — IPv4-Compatible의 특징
        System.arraycopy(ipv4Bytes, 0, ipv6Bytes, 12, 4);

        return Inet6Address.getByAddress(null, ipv6Bytes, null);
    }

    public static void main(String[] args) throws UnknownHostException {
        Inet4Address ipv4 = (Inet4Address) InetAddress.getByName("192.0.2.1");
        Inet6Address compatible = toIPv4Compatible(ipv4);

        System.out.println(compatible.getHostAddress());
        // 출력: 0:0:0:0:0:0:c000:201
        System.out.println(compatible.isIPv4CompatibleAddress());
        // 출력: true
    }
}
Java

IPv4-Mapped 코드와 거의 똑같다. 차이는 딱 하나, 인덱스 10, 11번에 0xff를 채우지 않는다는 것이다. 상위 96비트가 전부 0이면 Inet6Address.isIPv4CompatibleAddress()true를 반환한다. 레거시 로그에서 이 형식을 만났을 때 식별하는 용도로는 쓸 수 있지만, 새 코드에서 이 형식을 생성할 이유는 없다. 라우팅 인프라가 이 주소를 더 이상 특별하게 처리하지 않기 때문이다.

NAT64 — IPv6 전용 네트워크에서 IPv4 서버에 닿는 법

NAT64는 IPv6 전용 클라이언트가 IPv4 서버에 접근할 수 있게 해 주는 번역 메커니즘이다. Well-Known Prefix 64:ff9b::/96(RFC 6052)을 쓰며, NAT64 게이트웨이가 이 프리픽스를 보고 IPv4 패킷으로 바꿔 준다. SK텔레콤이나 T-Mobile 같은 모바일 통신사의 IPv6 전용 네트워크에서 이미 돌아가고 있는 방식이라, 모바일 트래픽을 받는 백엔드라면 한 번쯤 들여다볼 필요가 있다.

public class NAT64Converter {

    // RFC 6052 Well-Known Prefix: 64:ff9b::/96
    private static final byte[] NAT64_PREFIX = {
        0x00, 0x64, (byte) 0xff, (byte) 0x9b,  // 64:ff9b
        0x00, 0x00, 0x00, 0x00,                  // ::
        0x00, 0x00, 0x00, 0x00                   // ::
    };

    public static Inet6Address toNAT64(Inet4Address ipv4) throws UnknownHostException {
        byte[] ipv4Bytes = ipv4.getAddress();
        byte[] ipv6Bytes = new byte[16];

        // 상위 96비트: NAT64 Well-Known Prefix
        System.arraycopy(NAT64_PREFIX, 0, ipv6Bytes, 0, 12);
        // 하위 32비트: IPv4 주소
        System.arraycopy(ipv4Bytes, 0, ipv6Bytes, 12, 4);

        return Inet6Address.getByAddress(null, ipv6Bytes, null);
    }

    public static void main(String[] args) throws UnknownHostException {
        Inet4Address ipv4 = (Inet4Address) InetAddress.getByName("203.0.113.50");
        Inet6Address nat64 = toNAT64(ipv4);
        System.out.println(nat64.getHostAddress());
        // 출력: 64:ff9b:0:0:0:0:cb00:7132
    }
}
Java

NAT64_PREFIX 배열이 RFC 6052의 Well-Known Prefix 64:ff9b::를 12바이트로 표현한 것이다. 뒤에 IPv4 4바이트를 붙이면 NAT64 주소가 된다. 이 주소로 패킷을 보내면 NAT64 게이트웨이가 프리픽스를 떼어내고 IPv4 패킷으로 바꿔서 목적지에 전달하는 구조다. 한 가지 알아 둘 점은, 기업 네트워크에서는 Well-Known Prefix 대신 자체 프리픽스를 쓰는 경우도 있다는 것이다. DNS64 설정을 함께 확인해야 실제 주소를 올바르게 합성할 수 있다.

6to4 터널링 — IPv4 인프라 위에 IPv6 패킷을 얹는 기술

6to4는 IPv4 네트워크 위로 IPv6 패킷을 전달하는 자동 터널링 메커니즘이다(RFC 3056). 원리는 간단하다. IPv4 주소를 16진수로 바꿔서 2002::/16 프리픽스 뒤에 붙이면 /48 IPv6 프리픽스가 된다. 192.0.2.4를 예로 들면 16진수 C000:0204가 되고, 6to4 주소는 2002:C000:0204::/48이다. 다만 6to4 릴레이 라우터의 anycast 프리픽스(192.88.99.0/24)가 RFC 7526(2015년 5월)에 의해 Historic으로 격하되었기 때문에, 사실상 운영 환경에서는 사용 불가 상태로 봐야 한다.

public class SixToFourConverter {

    // RFC 3056 6to4 prefix: 2002::/16
    private static final byte[] SIXTO4_PREFIX = { 0x20, 0x02 };

    public static Inet6Address toSixToFour(Inet4Address ipv4) throws UnknownHostException {
        byte[] ipv4Bytes = ipv4.getAddress();
        byte[] ipv6Bytes = new byte[16];

        // 바이트 0-1: 6to4 프리픽스 (2002)
        System.arraycopy(SIXTO4_PREFIX, 0, ipv6Bytes, 0, 2);
        // 바이트 2-5: IPv4 주소를 16진수로 임베딩
        System.arraycopy(ipv4Bytes, 0, ipv6Bytes, 2, 4);
        // 바이트 6-15: SLA ID + 인터페이스 ID (여기서는 0으로 초기화)

        return Inet6Address.getByAddress(null, ipv6Bytes, null);
    }

    public static void main(String[] args) throws UnknownHostException {
        Inet4Address ipv4 = (Inet4Address) InetAddress.getByName("192.0.2.4");
        Inet6Address sixToFour = toSixToFour(ipv4);
        System.out.println(sixToFour.getHostAddress());
        // 출력: 2002:c000:204:0:0:0:0:0
    }
}
Java

2002 프리픽스 2바이트 + IPv4 주소 4바이트로 6to4 주소를 구성한다. 나머지 10바이트(SLA ID와 인터페이스 ID)는 예제에서 0으로 두었지만, 실제 네트워크에서는 서브넷 구분을 위해 SLA ID 필드(바이트 6-7)에 값을 넣는다. 다시 강조하지만 RFC 7526로 anycast 릴레이가 사라지면서 6to4는 사실상 사용 불가 상태가 되었다. 신규 프로젝트에서 쓸 일은 없고, 기존 인프라에서 이 형태의 주소를 마주쳤을 때 구조를 읽어내는 용도 정도로만 알아 두면 된다.

네 가지 방식, 어떻게 다른가 — 한눈에 보는 비교 테이블

Java IPv4 IPv6 변환 방식 네 가지를 나란히 놓으면 프리픽스, 용도, 현재 상태가 확연히 갈린다. 결론부터 보면 실무에서 쓸 만한 것은 IPv4-Mapped와 NAT64 둘뿐이다.

구분프리픽스RFC현재 상태용도Java 지원
IPv4-Mapped::ffff:0:0/96RFC 4291 Sec 2.5.5.2활성 (권장)듀얼스택 소켓, 로그 정규화Inet6Address 바이트 배열 생성
IPv4-Compatible::/96RFC 4291 Sec 2.5.5.1Deprecated레거시 호환isIPv4CompatibleAddress()
NAT6464:ff9b::/96RFC 6052활성IPv6-only에서 IPv4 접근바이트 배열 직접 구성
6to42002::/16RFC 3056사용 불가 (anycast 릴레이가 RFC 7526로 Historic)IPv4 위 IPv6 터널링바이트 배열 직접 구성

이 테이블에서 주목할 점은 네 가지 중 두 가지(IPv4-Compatible, 6to4)가 사실상 퇴역 상태라는 것이다. IETF는 2015년 RFC 7526을 통해 6to4 anycast 릴레이를 Historic로 격하해 6to4를 운영 환경에서 사용할 수 없게 만들었고, IPv4-Compatible 역시 RFC 4291에서 명시적으로 deprecated 처리되었다. 신규 개발에서 실질적으로 선택지가 되는 것은 IPv4-Mapped와 NAT64 두 가지뿐이다.

Java IPv4 IPv6 변환, 실무에서 어떤 방식을 골라야 하나

Java IPv4 IPv6 변환 코드를 짜야 하는 상황이라면, 열에 아홉은 IPv4-Mapped IPv6로 끝난다. 듀얼스택 소켓을 열면 운영체제가 IPv4 트래픽에 ::ffff: 프리픽스를 자동으로 붙여 전달하기 때문에, 변환 코드를 따로 작성하지 않아도 IPv6 소켓 하나로 양쪽 트래픽을 처리할 수 있다.

NAT64가 필요한 경우

NAT64 주소를 코드에서 직접 만들어야 하는 경우는 많지 않다. DNS64/NAT64 환경에서 IPv4 전용 외부 서비스의 주소를 합성해야 할 때 정도다. iOS 앱을 만든다면 Apple이 2016년 6월부터 모든 App Store 앱에 IPv6 전용 네트워크 동작을 의무화했으므로(WWDC 2015 발표, 2016-06 시행), NAT64 관련 코드를 만질 일이 생길 수 있다. 서버 사이드에서는 거의 쓸 일이 없다.

쓰지 말아야 할 방식

IPv4-Compatible와 6to4는 새 코드에서 쓰지 않는다. 둘 다 IETF 표준에서 퇴역했고, ISP나 클라우드 사업자의 라우팅 인프라에서도 별도로 처리하지 않는다. 레거시 로그에서 이 형식을 만났을 때 구조를 파악하고 IPv4-Mapped로 변환하는 코드 정도만 갖추고 있으면 된다.

public class IPv6FormatDetector {

    public static String detectFormat(Inet6Address ipv6) {
        byte[] bytes = ipv6.getAddress();

        if (ipv6.isIPv4CompatibleAddress()) {
            return "IPv4-Compatible (deprecated)";
        }

        // IPv4-Mapped 검사: 바이트 10, 11이 0xff
        if (bytes[10] == (byte) 0xff && bytes[11] == (byte) 0xff) {
            boolean upperZero = true;
            for (int i = 0; i < 10; i++) {
                if (bytes[i] != 0) { upperZero = false; break; }
            }
            if (upperZero) return "IPv4-Mapped";
        }

        // NAT64 검사: 64:ff9b:: 프리픽스
        if (bytes[0] == 0x00 && bytes[1] == 0x64
            && bytes[2] == (byte) 0xff && bytes[3] == (byte) 0x9b) {
            return "NAT64";
        }

        // 6to4 검사: 2002:: 프리픽스
        if (bytes[0] == 0x20 && bytes[1] == 0x02) {
            return "6to4 (historic)";
        }

        return "Native IPv6";
    }
}
Java

isIPv4CompatibleAddress()는 Java 기본 제공 메서드고, 나머지 세 가지는 프리픽스 바이트를 직접 비교한다. 한 가지 주의점은 isIPv4CompatibleAddress():: (unspecified)과 ::1 (loopback)에도 true를 반환한다는 것이다. 프로덕션 코드에서는 이 두 특수 주소를 먼저 걸러내는 가드 절을 추가하는 게 안전하다. 필요하다면 Teredo(2001:0000::/32) 같은 다른 전환 메커니즘까지 검사 조건을 추가하면 된다.

Java IPv4 IPv6 변환에 관한 자주 묻는 질문 (FAQ)

마치며 — 주소 하나에도 역사가 담겨 있다

몇 년 전 모바일 결제 시스템 로그를 분석하다가 ::ffff:10.0.0.1이라는 주소를 처음 봤다. IPv6인지 IPv4인지 헷갈려서 한참을 검색했는데, 알고 보니 듀얼스택 로드밸런서가 IPv4 트래픽을 IPv6 소켓으로 넘기면서 자동으로 붙인 IPv4-Mapped 주소였다. 그때 느낀 건, 이 구조를 모르면 로그 한 줄도 제대로 못 읽는다는 것이었다.

Java IPv4 IPv6 변환의 네 가지 방식을 다뤘지만 실무에서 직접 코드를 짤 일이 있는 건 IPv4-Mapped 정도다. 나머지는 네트워크 인프라가 알아서 처리하거나 이미 퇴역한 방식이다. 그래도 전체 그림을 한 번 훑어 두면 레거시 마이그레이션이나 네트워크 트러블슈팅에서 쓸데없이 삽질하는 시간이 줄어든다. 네트워크 관련해서 더 깊이 들어가고 싶다면 Java 26 HTTP/3 Client API 가이드를, Java 버전 관리가 막막하다면 jenv Java 버전 관리 가이드를 함께 보면 도움이 된다. 로드밸런서 뒤에서 찍힌 주소가 왜 이상하게 생겼는지, 그 답이 여기 있다.