HttpURLConnection을 버려야 하는 이유 — HttpClient(Java 11+)와 현대 네트워킹
// Java 1.1 — 레거시 HttpURLConnection (1997년)
HttpURLConnection conn = (HttpURLConnection) new URL("https://api.example.com/users").openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Accept", "application/json");
int responseCode = conn.getResponseCode(); // 워낙 구식이라 설정이 복잡
// 타임아웃 설정, 스트리밍, HTTP/2 — 모두 어렵거나 불가능
// Java 25 — HttpClient (Java 11+, JEP-321)
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/users"))
.header("Accept", "application/json")
.timeout(Duration.ofSeconds(10))
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode()); // 200
System.out.println(response.body()); // JSON 응답
HttpClient는 Java 11(JEP-321)에서 도입된 현대 HTTP 클라이언트다 — HTTP/2 지원, 비동기 API, WebSocket, 커넥션 풀링을 기본 제공한다. (HttpClient API)
HttpClient의 핵심 특징
| 특징 | HttpURLConnection |
HttpClient (Java 11+) |
|---|---|---|
| HTTP/2 | 미지원 | 기본 지원 |
| 비동기 | 불가 | sendAsync() → CompletableFuture |
| WebSocket | 별도 라이브러리 | 내장 지원 |
| 타임아웃 | 복잡 | .timeout() 한 줄 |
| 커넥션 풀 | 미지원 | 자동 관리 |
| API 스타일 | 명령형, 방대한 보일러플레이트 | 빌더 패턴, 선언적 |
동기식 요청
// Java 25 — 동기 GET
HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2) // HTTP/2 우선 (기본값)
.connectTimeout(Duration.ofSeconds(5)) // 연결 타임아웃
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/users/1"))
.header("Authorization", "Bearer mytoken123")
.timeout(Duration.ofSeconds(10)) // 요청 타임아웃
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("Status: " + response.statusCode());
System.out.println("Body: " + response.body());
POST / JSON 전송
// Java 25 — POST with JSON body
String json = """
{"name": "Alice", "age": 30}
""";
HttpRequest postRequest = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/users"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
HttpResponse<String> postResponse = client.send(postRequest, HttpResponse.BodyHandlers.ofString());
System.out.println("Created: " + postResponse.statusCode()); // 201
비동기식 요청 — CompletableFuture
// Java 25 — 비동기 GET
CompletableFuture<HttpResponse<String>> future = client.sendAsync(
request, HttpResponse.BodyHandlers.ofString());
future.thenApply(HttpResponse::body)
.thenAccept(System.out::println)
.exceptionally(ex -> {
System.err.println("요청 실패: " + ex.getMessage());
return null;
});
// 여러 요청을 병렬로
CompletableFuture<String> f1 = client.sendAsync(req1, BodyHandlers.ofString())
.thenApply(HttpResponse::body);
CompletableFuture<String> f2 = client.sendAsync(req2, BodyHandlers.ofString())
.thenApply(HttpResponse::body);
CompletableFuture.allOf(f1, f2).join(); // 둘 다 완료 대기
sendAsync()는 non-blocking — 호출 즉시CompletableFuture를 반환하고, 다른 작업을 수행할 수 있다. Virtual Thread 환경에서는 동기식send()를 써도 충분히 효율적이므로, 코드 단순성을 위해 동기식을 우선할 수 있다.
WebSocket (Java 11+)
// Java 25 — WebSocket 클라이언트
WebSocket ws = HttpClient.newHttpClient()
.newWebSocketBuilder()
.buildAsync(URI.create("wss://echo.example.com"),
new WebSocket.Listener() {
@Override
public CompletionStage<?> onText(WebSocket webSocket,
CharSequence data, boolean last) {
System.out.println("수신: " + data);
return null;
}
})
.join();
ws.sendText("Hello, WebSocket", true);
소켓 프로그래밍 — 저수준 TCP
HttpClient가 HTTP를 풀어간다면, Socket/ServerSocket은 TCP 레벨의 저수준 통신을 풀어간다:
// Java 25 — TCP 서버 (간단한 에코 서버)
import java.io.*;
import java.net.*;
import java.util.concurrent.*;
try (var server = new ServerSocket(8080);
var executor = Executors.newVirtualThreadPerTaskExecutor()) {
while (true) {
Socket client = server.accept(); // 클라이언트 연결 대기
executor.submit(() -> handleClient(client));
}
}
// 클라이언트 처리
void handleClient(Socket socket) {
try (socket;
var in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
var out = new PrintWriter(socket.getOutputStream(), true)) {
String line;
while ((line = in.readLine()) != null) {
out.println("Echo: " + line); // 에코 응답
}
} catch (IOException e) {
e.printStackTrace();
}
}
Java 21+에서는 Virtual Thread와
ServerSocket을 결합하여 수만 개의 동시 TCP 연결을 처리할 수 있다 — 각 연결마다 virtual thread를 할당하고, I/O 대기 시 스레드를 반납한다.
URI와 URL
// Java 25
URI uri = URI.create("https://example.com:8443/path?query=value#fragment");
System.out.println(uri.getScheme()); // https
System.out.println(uri.getHost()); // example.com
System.out.println(uri.getPort()); // 8443
System.out.println(uri.getPath()); // /path
System.out.println(uri.getQuery()); // query=value
URI는 식별자(문자열)이고,URL은 리소스의 위치(네트워크 접근 가능)다. 모든 URL은 URI이지만, 모든 URI가 URL은 아니다 (URN 등). 현대 Java에서는 주로URI를 사용한다.
InetAddress — IP 주소와 DNS 해석
// Java 25
import java.net.InetAddress;
// 호스트명 → IP 주소
InetAddress addr = InetAddress.getByName("example.com");
System.out.println(addr.getHostAddress()); // 93.184.216.34
// 로컬 주소
InetAddress local = InetAddress.getLocalHost();
System.out.println(local.getHostAddress()); // 192.168.x.x
// 여러 IP 확인 (라운드 로빈 DNS)
InetAddress[] all = InetAddress.getAllByName("google.com");
for (InetAddress a : all) {
System.out.println(a);
}
// google.com/142.250.x.x
// google.com/2606:4700:x::x (IPv6)
DNS 캐싱 함정
JVM은 기본적으로 DNS 조회 결과를 영구 캐싱한다 (보안 정책). DNS가 변경되면(예: 장애 조치, 로드 밸런서 IP 변경) 오래된 IP로 계속 연결을 시도한다.
# DNS 캐싱 제어
# networkaddress.cache.ttl=60 (60초 후 만료)
# networkaddress.cache.negative.ttl=10 (실패한 조회 10초 캐싱)
java -Dnetworkaddress.cache.ttl=60 MyApp
프로덕션에서는
networkaddress.cache.ttl을 짧게(10 ~ 60초) 설정한다. 기본값은 보안 관리자가 설치된 경우 영구(∞), 설치되지 않은 경우 구현마다 다르다. DNS 장애 조치가 늦어지는 문제의 원인이 자주 된다.
URLConnection — 레거시 진화
// Java 25 — URLConnection (레거시, 가능하면 HttpClient 사용)
import java.net.URL;
import java.net.URLConnection;
URL url = new URL("https://example.com/data");
URLConnection conn = url.openConnection();
conn.setConnectTimeout(5000);
conn.setReadTimeout(10000);
try (var in = conn.getInputStream()) {
String body = new String(in.readAllBytes());
System.out.println(body.substring(0, Math.min(100, body.length())));
}
URLConnection은HttpURLConnection의 부모 클래스다.HttpClient가 등장하기 전(Java 10 이하)에는 이 API가 유일한 표준 HTTP 클라이언트였다. 새 코드에서는 절대 쓰지 않는다 —HttpClient로 대체한다.
실습 — HttpClient로 API 호출
// Java 25 — NetworkingDemo.java
import java.net.URI;
import java.net.http.*;
import java.time.Duration;
public class NetworkingDemo {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build();
// JSONPlaceholder (무료 테스트 API) 호출
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://jsonplaceholder.typicode.com/users/1"))
.header("Accept", "application/json")
.timeout(Duration.ofSeconds(10))
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("Status: " + response.statusCode());
System.out.println("Version: " + response.version()); // HTTP_2 or HTTP_1_1
response.headers().firstValue("content-type")
.ifPresent(ct -> System.out.println("Content-Type: " + ct));
System.out.println("Body (first 200 chars): "
+ response.body().substring(0, Math.min(200, response.body().length())));
}
}
java NetworkingDemo.java
Status: 200
Version: HTTP_2
Content-Type: application/json; charset=utf-8
Body (first 200 chars): {
"id": 1,
"name": "Leanne Graham",
"username": "Bret",
...
확인할 것: HttpClient로 HTTP/2 API를 호출한다. 빌더 패턴으로 URI, 헤더, 타임아웃을 설정하고, BodyHandlers.ofString()으로 응답 본문을 문자열로 받는다.
요약 — 이 글의 결론
HttpClient(Java 11+)가 현대 Java HTTP 클라이언트의 표준이다.HttpURLConnection은 레거시 — 새 코드에서 쓰지 않는다. HTTP/2, 비동기, WebSocket, 타임아웃을 빌더 패턴으로 간결하게 설정한다.- 동기(
send)와 비동기(sendAsync)를 선택할 수 있다. Virtual Thread 환경에서는 동기식이 단순하면서도 충분히 효율적이다. Socket/ServerSocket은 TCP 레벨 저수준 통신. HTTP가 아닌 커스텀 프로토콜에 사용한다. Virtual Thread와 결합하면 수만 동시 연결을 처리할 수 있다.- WebSocket 클라이언트가 내장되어 있다. 별도 라이브러리(Java-WebSocket 등) 없이 표준 API로 WebSocket 통신이 가능하다.
URI로 리소스를 식별하고,HttpClient로 접근한다. URL보다 URI를 우선한다.
생각해 볼 문제
HttpClient는 재사용해야 하는가? 매 요청마다newHttpClient()를 호출하면 어떤 문제가 있는가?BodyHandlers.ofString()외에 어떤 핸들러가 있는가? 대용량 파일 다운로드에는 어느 것을 쓸까?- HTTP/2의 멀티플렉싱이 성능에 어떤 영향을 주는가? HTTP/1.1과 비교하라.
ServerSocket+ Virtual Thread 모델이 traditional thread pool보다 나은 이유는?InetAddress.getByName("example.com")가 반환하는 것은 무엇인가? DNS 해석은 언제 일어나는가?
참고
- HttpClient API (Java 25) - 접근 2026-07-10
- HttpRequest API - 접근 2026-07-10
- WebSocket API - 접근 2026-07-10
- JEP-321: HTTP Client (Standard) - 접근 2026-07-10
'Develop Artifacts > Java' 카테고리의 다른 글
| Java - 24. jvm gc (0) | 2026.07.12 |
|---|---|
| Java - 23. modules (0) | 2026.07.12 |
| Java - 21. jdbc (0) | 2026.07.12 |
| Java - 20. virtual threads (0) | 2026.07.12 |
| Java - 19. concurrent (0) | 2026.07.12 |
