핵심 요약
- InetAddress는 자바 표준 라이브러리에서 IP 주소(IPv4/IPv6)와 호스트 이름(DNS)을 표현하고 해석(리졸브)하는 핵심 클래스입니다.
InetAddress.getByName()/getAllByName()는 내부적으로 OS의 이름 해석기(Resolver)를 통해 DNS/hosts를 조회하며, 결과는 JVM 레벨 캐시 정책에 영향을 받습니다.- 로컬 개발/운영 환경에서의 차이(/etc/hosts, DNS, 컨테이너, VPC, 프록시, split-horizon DNS) 때문에, “같은 코드”라도 주소 해석 결과가 달라질 수 있습니다.
- 주의 포인트: DNS 역조회(
getCanonicalHostName()), 블로킹 호출로 인한 응답 지연, 캐시 TTL, IPv6 우선순위, 그리고 “입력값을 신뢰하지 않는” 보안 설계가 중요합니다.
목차
- 1. InetAddress는 무엇이고, 왜 백엔드에서 중요할까?
- 2. InetAddress가 다루는 것: 호스트 이름, IP, 루프백, 로컬호스트
- 3. 핵심 API: getByName / getAllByName / getLocalHost
- 4. IPv4/IPv6, 우선순위, 주소 선택 전략
- 5. DNS와 JVM 캐시: “가끔만” 터지는 운영 장애의 원인
- 6. 리버스 룩업과 canonical name: 느려지는 지점과 함정
- 7. 타임아웃과 블로킹: 네트워크 호출을 안전하게 다루는 법
- 8. Spring Boot 실전 패턴: 클라이언트 IP, 화이트리스트, 헬스체크
- 9. 보안 관점: SSRF, DNS Rebinding, 신뢰 경계
- 10. 요약: 언제 InetAddress를 쓰고, 무엇을 피해야 할까?
InetAddress는 “네트워크 주소를 다루는
클래스”라는 설명만으론 부족합니다. 실무에서는 DNS 해석, 캐시 정책, IPv4/IPv6 혼재,
리버스 룩업 지연 같은 요소가 얽혀서, 간단한 한 줄(InetAddress.getByName(host))이 장애의 도화선이 되기도 합니다.
이 글은 입문자가 이해하기 쉬운 흐름으로 시작하되, 운영 환경에서의 함정까지 연결해 “왜 InetAddress가 중요하고, 어떻게 안전하게 써야 하는지”를 정리합니다.
1. InetAddress는 무엇이고, 왜 백엔드에서 중요할까?
InetAddress는 자바에서 IP 주소와 호스트 이름(도메인)을 표현하는 표준 타입입니다. HTTP 클라이언트/서버, DB 커넥션, 메시지 브로커 연결 등 대부분의 네트워크 I/O는 결국 “어떤 호스트(또는 IP)로 연결할 것인가”라는 문제로 수렴하고, 그 과정에서 DNS 해석과 주소 선택이 필요합니다.
Spring 백엔드에서 InetAddress가 등장하는 대표 상황은 다음과 같습니다.
- 도메인 이름을 IP로 해석하여 특정 네트워크 정책(허용/차단)을 적용할 때
- 서버 자신의 네트워크 인터페이스/주소를 확인할 때(다만 흔히 오해가 있음)
- 로그/감사 목적으로 원격 주소를 다룰 때(단, HTTP 레벨의 클라이언트 IP는 별도)
- 내부망/외부망 분기, 사설 IP 대역 판별, 헬스체크/리졸브 검증
여기서 중요한 사실 하나: InetAddress는 네트워크 호출과 결합될 수 있고(특히 DNS), 이는 블로킹/지연/캐시 문제로 이어진다는 점입니다.
2. InetAddress가 다루는 것: 호스트 이름, IP, 루프백, 로컬호스트
InetAddress 인스턴스는 보통 두 가지 정보를 갖습니다.
- 호스트 이름(host name): 예)
example.com - IP 주소(address): 예)
93.184.216.34,2606:2800:220:1:248:1893:25c8:1946
단, “호스트 이름이 항상 실제 도메인”인 것은 아닙니다. IP 문자열(1.2.3.4)을 넘겨도 호스트 이름처럼 다룰 수 있고, 리버스 룩업을 수행하면 canonical name이
붙는 등 상황에 따라 달라집니다.
루프백과 로컬호스트
다음 개념은 반드시 정확히 구분해야 합니다.
- 루프백(Loopback): 자기 자신을 가리키는 주소. IPv4는
127.0.0.1, IPv6는::1 - localhost: 보통 hosts 파일에서 루프백으로 매핑된 호스트 이름. 하지만 환경에 따라 IPv6 우선/특정 설정에 영향을 받음
- 로컬 머신의 실제 IP: NIC(eth0 등)에 할당된 사설/공인 IP. 컨테이너/서버 환경에서는 인터페이스가 여러 개일 수 있음
3. 핵심 API: getByName / getAllByName / getLocalHost
InetAddress의 대표 API 3가지를 중심으로 동작 감각을 잡아봅시다.
3-1) getByName: “하나를 반환”하지만 사실은 선택 결과다
import java.net.InetAddress;
public class InetAddressByNameExample {
public static void main(String[] args) throws Exception {
InetAddress addr = InetAddress.getByName("example.com");
System.out.println("HostName=" + addr.getHostName());
System.out.println("HostAddress=" + addr.getHostAddress());
}
}
getByName()은 이름 해석 결과 중 “하나”를 반환합니다. 하지만 도메인이 A/AAAA 레코드를 여러 개 가지고 있으면(로드밸런싱, CDN) 실제로는 후보가 여럿입니다. 이때
“어떤 주소가 선택되는가”는 JVM/OS 설정, IPv6 우선순위, 캐시 상태에 영향을 받습니다.
3-2) getAllByName: 멀티 A/AAAA를 명시적으로 다루기
import java.net.InetAddress;
import java.util.Arrays;
public class InetAddressAllByNameExample {
public static void main(String[] args) throws Exception {
InetAddress[] addrs = InetAddress.getAllByName("google.com");
Arrays.stream(addrs).forEach(a -> {
System.out.println(a.getHostAddress());
});
}
}
실무에서 “주소를 하나만 고르기”보다 “가능한 주소 목록을 얻어서 정책적으로 선택”하는 편이 안전할 때가 있습니다. 예를 들어 특정 대역을 차단하거나, IPv4만 허용해야 하는 시스템이라면
getAllByName()로 받은 목록을 필터링하는 패턴이 유용합니다.
3-3) getLocalHost: 서버 자신의 IP를 얻는 만능키가 아니다
import java.net.InetAddress;
public class LocalHostExample {
public static void main(String[] args) throws Exception {
InetAddress local = InetAddress.getLocalHost();
System.out.println(local.getHostName());
System.out.println(local.getHostAddress());
}
}
getLocalHost()는 “서버의 대표 주소”처럼 보이지만, 컨테이너 환경(Docker/Kubernetes), 호스트 이름 설정, DNS 구성에 따라 기대와 다르게 동작할 수
있습니다. 어떤 환경에서는 루프백을 주거나, 어떤 환경에서는 사설 IP를 주거나, 심지어 이름 해석 실패로 예외가 날 수도 있습니다. “서버의 외부 접속 IP”를 정확히 알아야 한다면, 네트워크 인터페이스를
직접 순회하는 방식이 더 명확합니다(아래 참고).
참고: 네트워크 인터페이스 순회로 로컬 주소 목록 얻기
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.Collections;
public class NetworkInterfaceExample {
public static void main(String[] args) throws Exception {
for (NetworkInterface ni : Collections.list(NetworkInterface.getNetworkInterfaces())) {
for (InetAddress addr : Collections.list(ni.getInetAddresses())) {
System.out.println(ni.getName() + " -> " + addr.getHostAddress());
}
}
}
}
운영 서버에서 “어느 NIC의 어떤 IP를 써야 하는가”는 애플리케이션 요구사항과 네트워크 설계에 따라 달라집니다. InetAddress는 그 중 “주소 표현 및 이름 해석”을 맡고, NIC 선택은 보통 별도 설계가 필요합니다.
4. IPv4/IPv6, 우선순위, 주소 선택 전략
최근 환경은 IPv4와 IPv6가 혼재합니다. 도메인에 AAAA 레코드가 있고 서버가 IPv6를 지원하는 환경이면, getByName()이 IPv6 주소를 반환할 수도 있습니다. 이때
다음 문제가 발생할 수 있습니다.
- 내부망은 IPv4만 열려 있는데 클라이언트가 IPv6로 연결을 시도하여 실패
- 방화벽/보안그룹이 IPv6 대역을 허용하지 않아 간헐적 장애 발생
- 특정 레거시 시스템이 IPv6 리터럴을 처리하지 못함
이런 경우 “IPv4만 사용” 같은 정책이 필요할 수 있습니다. 대표적인 접근은 getAllByName() 결과를 IPv4/IPv6로 분류해 선택하는 것입니다.
import java.net.Inet4Address;
import java.net.InetAddress;
import java.util.Arrays;
import java.util.Optional;
public class PreferIPv4Example {
public static void main(String[] args) throws Exception {
InetAddress[] all = InetAddress.getAllByName("example.com");
Optional ipv4 = Arrays.stream(all).filter(a -> a instanceof Inet4Address).findFirst();
InetAddress chosen = ipv4.orElse(all[0]); // IPv4가 있으면 우선, 없으면 첫 번째
System.out.println("Chosen=" + chosen.getHostAddress());
}
}
주의: “첫 번째 주소”는 항상 안정적인 의미를 갖지 않습니다. CDN/로드밸런싱은 레코드 순서가 바뀔 수 있고, OS/Resolver의 정책에 따라 결과가 달라질 수 있습니다. 그러므로 “주소 하나 선택”은 정책적으로 관리해야 합니다.
5. DNS와 JVM 캐시: “가끔만” 터지는 운영 장애의 원인
InetAddress를 운영에서 어렵게 만드는 핵심은 DNS입니다. DNS는 “항상 같은 IP를 돌려준다”는 보장이 없습니다.
- 로드밸런서가 교체되며 IP가 바뀜
- 장애 조치로 A/AAAA 레코드가 변경됨
- CDN/지리 기반 라우팅으로 지역별 결과가 다름
그런데 애플리케이션은 보통 성능을 위해 DNS 결과를 캐시합니다. 자바도 예외가 아닙니다. InetAddress는 JVM 레벨에서 이름 해석 결과를 캐시할 수 있으며, 이 캐시 TTL 정책은 보안 매니저/시스템 설정 등에 영향을 받을 수 있습니다. 실무 체감으로는 “DNS가 바뀌었는데 앱이 이전 IP를 계속 물고 있어 간헐적 장애” 같은 형태로 나타납니다.
입문자에게 권장하는 실무 원칙은 다음과 같습니다.
- DNS가 바뀔 수 있는 인프라(클라우드 LB, 서비스 디스커버리)를 쓰는 경우, “JVM DNS 캐시 정책”을 운영 표준으로 문서화한다.
- 가능하면 “도메인 이름 그대로”를 커넥션 라이브러리(HTTP 클라이언트, JDBC 등)에 넘겨서, 라이브러리/OS가 갱신을 처리하도록 맡기고, 애플리케이션에서 IP를 고정하지 않는다.
- 정말 IP로 강제해야 한다면, TTL/캐시 무효화 전략을 별도로 마련한다.
즉, InetAddress로 미리 리졸브해서 IP를 박아버리는 코드는 “처음엔 빨라 보이지만” 운영에서 발목을 잡을 수 있습니다.
6. 리버스 룩업과 canonical name: 느려지는 지점과 함정
다음 메서드는 특히 주의가 필요합니다.
getCanonicalHostName()getHostName()(상황에 따라 역조회가 발생할 수 있음)
리버스 룩업(역 DNS)은 “IP -> 도메인”을 찾는 과정인데, 환경에 따라 느리거나 실패할 수 있습니다. 예를 들어 사설 IP 대역에 PTR 레코드가 없거나, DNS 서버가 역조회 요청을 처리하지 못하면 응답이 지연될 수 있습니다. 로그를 찍는다고 무심코 canonical host name을 호출했다가, 요청 처리 스레드가 블로킹되어 전체 API가 느려지는 케이스가 실제로 발생합니다.
느린 코드 패턴 예시: 요청마다 canonical name을 구함
// 위험: 요청 처리 흐름에서 reverse DNS가 발생할 수 있음
String clientName = inetAddress.getCanonicalHostName();
대부분의 웹 서비스에서 “클라이언트 식별”은 IP 문자열이면 충분합니다. 필요 이상의 역조회는 피하고, 꼭 필요하다면 비동기 처리/샘플링/캐시 등을 고려하는 편이 안전합니다.
7. 타임아웃과 블로킹: 네트워크 호출을 안전하게 다루는 법
InetAddress의 이름 해석은 블로킹일 수 있습니다. 즉, DNS 서버가 느리거나 네트워크가 불안정하면 getByName() 자체가 지연되어 요청 처리 시간이 늘어납니다. 그래서
실무에서는 다음 접근이 중요합니다.
- 요청 경로(핫패스)에서 도메인 리졸브를 반복하지 않는다.
- 리졸브가 필요하다면 애플리케이션 시작 시점에 미리 검증하거나, 별도 스케줄러에서 헬스체크한다.
- HTTP 클라이언트는 반드시 connect/read 타임아웃을 설정하고, DNS 지연에 대비해 전체 호출을 제한한다.
예시: Java 11+ HttpClient 타임아웃 설정
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
public class HttpClientTimeoutExample {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(2))
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://example.com"))
.timeout(Duration.ofSeconds(3))
.GET()
.build();
HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode());
}
}
중요 포인트: InetAddress 자체에는 “DNS 타임아웃”을 직관적으로 설정하는 API가 없습니다. 실제 타임아웃은 OS Resolver, 네트워크 설정, 그리고 상위 레벨 클라이언트의 전체 타임아웃 정책에 의해 간접적으로 통제되는 경우가 많습니다. 그러므로 “InetAddress로 리졸브해서 쓰겠다”는 설계는 타임아웃을 포함한 운영 정책까지 함께 고려해야 합니다.
8. Spring Boot 실전 패턴: 클라이언트 IP, 화이트리스트, 헬스체크
8-1) 클라이언트 IP를 InetAddress로 “읽는” 게 아니라, 먼저 HTTP 레벨에서 얻어라
Spring MVC에서 사용자가 접속한 IP는 HttpServletRequest에서 얻습니다. 여기서 주의할 점은 프록시/로드밸런서 환경에서는
X-Forwarded-For 등이 개입된다는 것입니다. InetAddress는 “네트워크 주소 타입”이지, HTTP 프록시 체인을 자동으로 이해하지는 않습니다.
import jakarta.servlet.http.HttpServletRequest;
public class ClientIpExample {
public static String resolveClientIp(HttpServletRequest request) {
String xff = request.getHeader("X-Forwarded-For");
if (xff != null && !xff.isBlank()) { // X-Forwarded-For는 "client, proxy1, proxy2" 형태일 수 있음
return xff.split(",")[0].trim();
}
return request.getRemoteAddr();
}
}
그리고 그 다음에야 필요하면 InetAddress로 파싱/분류(사설망 여부 등)를 할 수 있습니다.
8-2) IP 화이트리스트/사설망 판별
관리자 API를 사내망에서만 열고 싶을 때 “IP 대역 판별”이 등장합니다. InetAddress는 사설 IP/루프백 여부를 판단하는 메서드를 제공합니다.
import java.net.InetAddress;
public class IpClassificationExample {
public static void main(String[] args) throws Exception {
InetAddress a = InetAddress.getByName("10.0.0.10");
System.out.println("isAnyLocalAddress=" + a.isAnyLocalAddress());
System.out.println("isLoopbackAddress=" + a.isLoopbackAddress());
System.out.println("isSiteLocalAddress=" + a.isSiteLocalAddress()); // 사설망 여부(환경에 따라 의미 주의)
}
}
실무 팁: “사설망인지” 같은 판정은 네트워크 정책과 일치해야 합니다. 단순 메서드 결과만 믿기보다, 필요한 경우 CIDR 대역(예: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)을 명시적으로 매칭하는 코드를 두는 편이 더 예측 가능할 때가 많습니다.
8-3) 시작 시점 DNS 헬스체크: 런타임 핫패스에서 리졸브하지 않기
import java.net.InetAddress;
public class DnsHealthCheckExample {
public static void validateResolvable(String host) {
try {
InetAddress[] all = InetAddress.getAllByName(host);
if (all.length == 0) {
throw new IllegalStateException("No DNS records for host: " + host);
}
} catch (Exception e) {
throw new IllegalStateException("DNS resolve failed for host: " + host, e);
}
}
public static void main(String[] args) {
validateResolvable("example.com");
System.out.println("DNS OK");
}
}
이 패턴은 “배포 직후 DNS가 깨져서 바로 장애” 같은 상황을 조기에 발견하는 데 유용합니다. 다만 DNS가 동적으로 바뀌는 서비스라면 이 검증은 “시작 시점에만 OK”일 수 있으니, 운영 정책에 따라 주기적 검증이나 클라이언트 재시도 정책을 함께 설계해야 합니다.
9. 보안 관점: SSRF, DNS Rebinding, 신뢰 경계
InetAddress를 다루면 보안 이슈가 자주 따라옵니다. 대표적으로 SSRF(Server-Side Request Forgery) 대응이 그렇습니다. 사용자가 입력한
URL/호스트를 서버가 그대로 호출하면, 내부망(예: 127.0.0.1, 10.x.x.x, 클라우드 메타데이터 IP 등)으로 접근이 가능해질 수 있습니다.
입문자를 위한 핵심 원칙은 다음과 같습니다.
- 사용자 입력(host, url)을 그대로
InetAddress.getByName()후 호출하는 코드를 경계하라. - 리졸브 결과가 사설망/루프백/링크로컬 대역이면 차단하는 정책을 고려하라.
- DNS Rebinding을 생각하라: 최초 리졸브는 공인 IP였지만, 이후 DNS가 바뀌어 내부 IP로 바뀌는 공격이 가능할 수 있다. 즉 “리졸브 한 번 하고 믿는다”는 설계가 취약해질 수 있다.
예시: 단순 차단 로직(설계 출발점)
import java.net.InetAddress;
public class SsrfGuardExample {
public static void validatePublicHost(String host) {
try {
InetAddress[] all = InetAddress.getAllByName(host);
for (InetAddress a : all) {
if (a.isAnyLocalAddress() || a.isLoopbackAddress() || a.isSiteLocalAddress()) {
throw new IllegalArgumentException("Blocked private/local address: " + a.getHostAddress());
}
}
} catch (IllegalArgumentException e) {
throw e;
} catch (Exception e) {
throw new IllegalArgumentException("Failed to resolve host: " + host, e);
}
}
public static void main(String[] args) {
validatePublicHost("example.com");
System.out.println("Allowed");
}
}
주의: 위 코드는 “개념을 설명하기 위한 출발점”입니다. 실무 SSRF 방어는 더 복잡합니다. 예를 들어 IPv6, NAT64, DNS rebinding, 프록시 설정, 허용 리스트 기반 설계 등 다양한 요소가 결합됩니다. 하지만 최소한 “InetAddress로 리졸브한 뒤 내부 대역을 차단한다”는 방향성은 입문자가 반드시 알고 있어야 합니다.
10. 요약: 언제 InetAddress를 쓰고, 무엇을 피해야 할까?
마지막으로 실무 판단 기준을 표로 정리합니다.
| 목적 | 권장 API/패턴 | 주의사항 |
|---|---|---|
| 도메인 -> IP 해석 | getAllByName() 후 정책적 선택 |
IPv4/IPv6 혼재, 캐시/TTL, 로드밸런싱 고려 |
| 서버 로컬 주소 파악 | NetworkInterface 순회 | getLocalHost()는 환경에 따라 기대와 다를 수 있음 |
| 리버스 룩업(도메인 이름 얻기) | 필요할 때만, 비동기/캐시 고려 | getCanonicalHostName()는 느려질 수 있음 |
| 요청 핫패스에서 주소 처리 | IP 문자열 중심, 최소한의 리졸브 | DNS 호출은 블로킹 가능, 타임아웃 설계 필요 |
| 보안(SSRF 방어) | 리졸브 결과 대역 필터링 + 허용리스트 | DNS rebinding/IPv6/프록시 등 추가 고려 필요 |
결론적으로 InetAddress는 “IP를 담는 객체”를 넘어, DNS/캐시/네트워크 정책/보안까지 이어지는 진입점입니다. 입문자 단계에서는
getByName()이 간단해 보일 수 있지만, 운영에서는 “리졸브가 느려지거나”, “캐시 때문에 IP 갱신이 안 되거나”, “IPv6 때문에 연결이 실패하거나”, “역조회 때문에
API가 지연되는” 형태로 문제가 나타납니다. 따라서 핫패스에서 무분별한 리졸브/역조회를 피하고, 필요하면 정책적으로 목록을
처리(getAllByName)하며, 보안상으로는 입력값을 신뢰하지 않는 설계를 갖추는 것이 핵심입니다.