웹 개발에서 URI와 URL을 혼용해서 쓰는 경우가 많다. 하지만 Java에서는 java.net.URI와 java.net.URL이 별개의 클래스이고, 동작 방식이 꽤 다르다. 차이를 정확히 알아야 올바른 클래스를 선택할 수 있다.
개념 차이
URI는 Uniform Resource Identifier다. 리소스를 식별하는 문자열이다.
URL은 Uniform Resource Locator다. 리소스의 위치를 알려주는 문자열이다.
URL은 URI의 부분집합이다. 모든 URL은 URI지만, 모든 URI가 URL은 아니다.
URI (전체 집합)
├── URL — 위치로 식별 (https://example.com/api/users)
└── URN — 이름으로 식별 (urn:isbn:0451450523)
https://example.com/image.png→ URI이면서 URL. 프로토콜 + 위치가 있으니 "어디서 가져올지" 알 수 있다urn:isbn:0451450523→ URI이지만 URL은 아니다. 책의 식별자이지, 어디서 가져올지는 모른다
Java에서의 차이
java.net.URI
순수한 파싱/조합용 클래스다. 네트워크 연결을 하지 않는다.
URI uri = new URI("https://example.com/path?query=1");
uri.getHost(); // "example.com"
uri.getPath(); // "/path"
uri.getQuery(); // "query=1"
문법 검증만 수행한다. 존재하지 않는 프로토콜이어도 파싱이 가능하다.
java.net.URL
네트워크 연결이 가능한 클래스다. openConnection()으로 실제 HTTP 요청을 보낼 수 있다.
URL url = URI.create("https://example.com/path").toURL();
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
생성 시 프로토콜 핸들러가 있어야 한다. http, https, ftp 등 알려진 프로토콜만 가능하다.
equals()의 함정
URL.equals()는 DNS 조회를 수행한다. 두 URL이 같은 IP로 해석되면 같다고 판단한다. 네트워크 호출이 발생하므로 느리고, DNS 결과에 따라 동일한 URL이 다르게 판단될 수 있다.
// URL.equals()는 DNS 조회 — 예측 불가능
new URL("https://example.com").equals(new URL("https://example.com")) // 느림
// URI.equals()는 순수 문자열 비교 — 안전
new URI("https://example.com").equals(new URI("https://example.com")) // 빠르고 예측 가능
Map의 key나 Set에 URL을 넣으면 equals()와 hashCode()가 DNS에 의존해서 예측 불가능한 동작이 생긴다. 컬렉션에는 항상 URI를 사용한다.
실무에서의 선택 기준
| 상황 | 선택 |
|---|---|
| URL 파싱, 경로 추출, 쿼리 파라미터 분리 | URI |
| URL 문자열을 변수로 저장 | String 또는 URI |
| 네트워크 요청 | HttpClient + URI |
| 컬렉션에 URL 저장 | URI (절대 URL 아님) |
URI를 기본으로 쓰고, 실제 연결이 필요할 때만 URL로 변환하는 것이 권장 패턴이다. 현대 Java에서는 HttpClient가 URI를 직접 받으므로 URL을 쓸 일이 거의 없다.
이 프로젝트에서 S3 presigned URL을 String으로 반환하는 이유도 같다. DB 저장, JSON 응답, <img src=""> — 모든 소비자가 문자열로 쓰기 때문에 URI나 URL 타입으로 감쌀 이유가 없다.