웹 개발에서 URI와 URL을 혼용해서 쓰는 경우가 많다. 하지만 Java에서는 java.net.URIjava.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에서는 HttpClientURI를 직접 받으므로 URL을 쓸 일이 거의 없다.

이 프로젝트에서 S3 presigned URL을 String으로 반환하는 이유도 같다. DB 저장, JSON 응답, <img src=""> — 모든 소비자가 문자열로 쓰기 때문에 URIURL 타입으로 감쌀 이유가 없다.