본문으로 건너뛰기

Ch 16.2 URL과 HTTP 통신

1. URL 클래스

URL(Uniform Resource Locator)은 인터넷 상에 존재하는 자원(웹 페이지, 이미지, 파일 등)의 위치를 나타내는 표준 주소 체계입니다.

https://www.google.com:443/search?q=java&hl=ko#results
│ │ │ │ │ │
│ │ │ │ │ └─ Fragment (#)
│ │ │ │ └─ Query (?key=value)
│ │ │ └─ Path (/search)
│ │ └─ Port (443)
│ └─ Host (www.google.com)
└─ Protocol (https)
import java.net.*;

public class URLExample {
public static void main(String[] args) throws MalformedURLException {
URL url = new URL("https://www.example.com:443/search?q=java&hl=ko#section1");

// URL 구성 요소 파싱
System.out.println("프로토콜: " + url.getProtocol()); // https
System.out.println("호스트: " + url.getHost()); // www.example.com
System.out.println("포트: " + url.getPort()); // 443
System.out.println("경로: " + url.getPath()); // /search
System.out.println("쿼리: " + url.getQuery()); // q=java&hl=ko
System.out.println("앵커: " + url.getRef()); // section1
System.out.println("파일: " + url.getFile()); // /search?q=java&hl=ko

// 기본 포트 사용 시 getPort()는 -1 반환
URL url2 = new URL("https://www.example.com/page");
System.out.println("포트 (기본): " + url2.getPort()); // -1
System.out.println("기본포트: " + url2.getDefaultPort()); // 443

// URI로 변환 (더 엄격한 검증)
URI uri = url.toURI();
System.out.println("URI: " + uri);
}
}

2. URLConnection: HTTP 연결 기본

URLConnection은 URL 자원에 대한 연결을 나타내는 추상 클래스입니다. HTTP 통신에서는 하위 클래스인 HttpURLConnection을 사용합니다.

import java.io.*;
import java.net.*;

public class URLConnectionExample {
public static void main(String[] args) throws Exception {
URL url = new URL("https://httpbin.org/get");
URLConnection conn = url.openConnection();

// 연결 설정
conn.setConnectTimeout(5000); // 연결 타임아웃 5초
conn.setReadTimeout(10000); // 읽기 타임아웃 10초
conn.setRequestProperty("User-Agent", "Java-URLConnection/1.0");

// 헤더 정보 조회
System.out.println("Content-Type: " + conn.getContentType());
System.out.println("Content-Length: " + conn.getContentLength());
System.out.println("Last-Modified: " + new java.util.Date(conn.getLastModified()));

// 응답 본문 읽기
try (BufferedReader br = new BufferedReader(
new InputStreamReader(conn.getInputStream(), "UTF-8"))) {
StringBuilder response = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
response.append(line).append('\n');
}
System.out.println("응답:\n" + response);
}
}
}

3. HttpURLConnection: GET/POST 요청

import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;

public class HttpURLConnectionExample {
// GET 요청
static String get(String urlStr) throws Exception {
URL url = new URL(urlStr);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();

conn.setRequestMethod("GET");
conn.setConnectTimeout(5000);
conn.setReadTimeout(10000);
conn.setRequestProperty("Accept", "application/json");
conn.setRequestProperty("User-Agent", "Java-HttpClient/1.0");

int responseCode = conn.getResponseCode();
System.out.println("GET " + urlStr + " → " + responseCode);

if (responseCode == HttpURLConnection.HTTP_OK) {
return readResponse(conn.getInputStream());
} else {
throw new RuntimeException("HTTP 오류: " + responseCode);
}
}

// POST 요청 (JSON body)
static String post(String urlStr, String jsonBody) throws Exception {
URL url = new URL(urlStr);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();

conn.setRequestMethod("POST");
conn.setConnectTimeout(5000);
conn.setReadTimeout(10000);
conn.setDoOutput(true); // 요청 본문 전송 허용
conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
conn.setRequestProperty("Accept", "application/json");

// 요청 본문 전송
try (OutputStream os = conn.getOutputStream()) {
byte[] input = jsonBody.getBytes(StandardCharsets.UTF_8);
os.write(input, 0, input.length);
}

int responseCode = conn.getResponseCode();
System.out.println("POST " + urlStr + " → " + responseCode);

InputStream is = (responseCode >= 200 && responseCode < 300)
? conn.getInputStream()
: conn.getErrorStream();

return readResponse(is);
}

static String readResponse(InputStream is) throws IOException {
StringBuilder sb = new StringBuilder();
try (BufferedReader br = new BufferedReader(
new InputStreamReader(is, StandardCharsets.UTF_8))) {
String line;
while ((line = br.readLine()) != null) {
sb.append(line).append('\n');
}
}
return sb.toString();
}

public static void main(String[] args) {
try {
// GET 요청
String getResponse = get("https://httpbin.org/get");
System.out.println("GET 응답 (처음 200자):");
System.out.println(getResponse.substring(0, Math.min(200, getResponse.length())));

// POST 요청
String jsonBody = "{\"name\": \"Alice\", \"age\": 30}";
String postResponse = post("https://httpbin.org/post", jsonBody);
System.out.println("\nPOST 응답 (처음 200자):");
System.out.println(postResponse.substring(0, Math.min(200, postResponse.length())));

} catch (Exception e) {
System.out.println("요청 실패 (네트워크 필요): " + e.getMessage());
}
}
}

4. Java 11+ HttpClient: 현대적 HTTP API

Java 11에서 도입된 java.net.http.HttpClient는 기존 HttpURLConnection의 불편함을 해소한 현대적 API입니다.

4.1 동기 요청 (Synchronous)

import java.net.URI;
import java.net.http.*;
import java.net.http.HttpResponse.BodyHandlers;
import java.time.Duration;

public class HttpClientSync {
public static void main(String[] args) throws Exception {
// HttpClient 생성 (재사용 권장)
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.followRedirects(HttpClient.Redirect.NORMAL) // 리다이렉트 자동 처리
.version(HttpClient.Version.HTTP_2) // HTTP/2 우선 사용
.build();

// GET 요청 구성
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://httpbin.org/get"))
.timeout(Duration.ofSeconds(10))
.header("Accept", "application/json")
.header("User-Agent", "Java-HttpClient/11")
.GET()
.build();

// 동기 전송 및 응답 수신
HttpResponse<String> response = client.send(request, BodyHandlers.ofString());

System.out.println("상태 코드: " + response.statusCode());
System.out.println("헤더 Content-Type: " + response.headers().firstValue("content-type").orElse("없음"));
System.out.println("응답 본문 (처음 300자):");
System.out.println(response.body().substring(0, Math.min(300, response.body().length())));
}
}

4.2 POST 요청 (JSON 전송)

import java.net.URI;
import java.net.http.*;
import java.net.http.HttpResponse.BodyHandlers;
import java.net.http.HttpRequest.BodyPublishers;

public class HttpClientPost {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newHttpClient();

String jsonBody = """
{
"title": "foo",
"body": "bar",
"userId": 1
}
""";

HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://jsonplaceholder.typicode.com/posts"))
.header("Content-Type", "application/json")
.POST(BodyPublishers.ofString(jsonBody))
.build();

HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
System.out.println("상태: " + response.statusCode()); // 201 Created
System.out.println("응답: " + response.body());
}
}

4.3 비동기 요청 (Asynchronous)

import java.net.URI;
import java.net.http.*;
import java.net.http.HttpResponse.BodyHandlers;
import java.util.concurrent.CompletableFuture;

public class HttpClientAsync {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newHttpClient();

// 비동기 요청: sendAsync는 즉시 CompletableFuture 반환
CompletableFuture<HttpResponse<String>> future = client.sendAsync(
HttpRequest.newBuilder()
.uri(URI.create("https://httpbin.org/delay/1")) // 1초 지연 API
.GET()
.build(),
BodyHandlers.ofString()
);

System.out.println("요청 전송됨. 다른 작업 수행 중...");

// 응답 처리 체인 (콜백 방식)
future
.thenApply(HttpResponse::statusCode)
.thenAccept(code -> System.out.println("응답 코드: " + code))
.exceptionally(e -> {
System.out.println("오류: " + e.getMessage());
return null;
});

// 여러 요청 동시 실행
HttpClient asyncClient = HttpClient.newHttpClient();
CompletableFuture<HttpResponse<String>> req1 = asyncClient.sendAsync(
HttpRequest.newBuilder().uri(URI.create("https://httpbin.org/get")).build(),
BodyHandlers.ofString()
);
CompletableFuture<HttpResponse<String>> req2 = asyncClient.sendAsync(
HttpRequest.newBuilder().uri(URI.create("https://httpbin.org/ip")).build(),
BodyHandlers.ofString()
);

// 모두 완료될 때까지 대기
CompletableFuture.allOf(req1, req2).join();
System.out.println("요청1 상태: " + req1.get().statusCode());
System.out.println("요청2 상태: " + req2.get().statusCode());
}
}

5. HttpURLConnection vs HttpClient 비교

항목HttpURLConnectionHttpClient (Java 11+)
도입 버전Java 1.1Java 11
API 스타일명령형빌더 패턴 (유창한 API)
비동기 지원없음sendAsync (CompletableFuture)
HTTP/2 지원없음있음
코드 가독성낮음높음
스레드 안전인스턴스별재사용 가능
권장 상황레거시 코드Java 11+ 신규 개발
실무에서는 외부 라이브러리

대부분의 실무 프로젝트에서는 OkHttp, Retrofit, 또는 Spring의 RestTemplate/WebClient를 사용합니다. 표준 API는 기반 이해를 위해 학습합니다.

6. 실전 예제: 공개 REST API 호출하여 JSON 응답 출력

import java.net.URI;
import java.net.http.*;
import java.net.http.HttpResponse.BodyHandlers;

/**
* JSONPlaceholder (https://jsonplaceholder.typicode.com) 공개 API 호출 예제
* 실제 실행 시 인터넷 연결 필요
*/
public class RestApiExample {
static final HttpClient client = HttpClient.newHttpClient();
static final String BASE_URL = "https://jsonplaceholder.typicode.com";

// 게시글 목록 조회
static String getPosts() throws Exception {
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/posts?_limit=3"))
.header("Accept", "application/json")
.GET()
.build();
return client.send(req, BodyHandlers.ofString()).body();
}

// 특정 게시글 조회
static String getPost(int id) throws Exception {
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/posts/" + id))
.GET()
.build();
HttpResponse<String> res = client.send(req, BodyHandlers.ofString());
return "상태: " + res.statusCode() + "\n" + res.body();
}

// 게시글 생성 (POST)
static String createPost(String title, String body, int userId) throws Exception {
String json = String.format(
"{\"title\":\"%s\",\"body\":\"%s\",\"userId\":%d}",
title, body, userId
);

HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/posts"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();

HttpResponse<String> res = client.send(req, BodyHandlers.ofString());
return "상태: " + res.statusCode() + "\n" + res.body();
}

// 게시글 삭제 (DELETE)
static String deletePost(int id) throws Exception {
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/posts/" + id))
.DELETE()
.build();

HttpResponse<String> res = client.send(req, BodyHandlers.ofString());
return "삭제 상태: " + res.statusCode(); // 200
}

public static void main(String[] args) {
try {
System.out.println("=== 게시글 목록 (3개) ===");
System.out.println(getPosts());

System.out.println("\n=== 게시글 1번 ===");
System.out.println(getPost(1));

System.out.println("\n=== 게시글 생성 ===");
System.out.println(createPost("새 제목", "새 내용", 1));

System.out.println("\n=== 게시글 1번 삭제 ===");
System.out.println(deletePost(1));

} catch (Exception e) {
System.out.println("네트워크 오류 (인터넷 연결 필요): " + e.getMessage());
}
}
}

7. HTTPS 인증서 처리

import javax.net.ssl.*;
import java.net.http.*;
import java.net.URI;
import java.security.cert.X509Certificate;

public class HttpsExample {
/**
* 개발 환경에서 자체 서명 인증서를 사용할 때만 활용
* 프로덕션에서 절대 사용 금지!
*/
static HttpClient createInsecureClient() throws Exception {
TrustManager[] trustAll = new TrustManager[]{
new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() { return null; }
public void checkClientTrusted(X509Certificate[] certs, String authType) {}
public void checkServerTrusted(X509Certificate[] certs, String authType) {}
}
};

SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustAll, new java.security.SecureRandom());

return HttpClient.newBuilder()
.sslContext(sslContext)
.build();
}

public static void main(String[] args) throws Exception {
// 일반 HTTPS 요청 (신뢰할 수 있는 인증서)
HttpClient client = HttpClient.newHttpClient();
HttpResponse<String> response = client.send(
HttpRequest.newBuilder().uri(URI.create("https://httpbin.org/get")).build(),
HttpResponse.BodyHandlers.ofString()
);
System.out.println("HTTPS 응답: " + response.statusCode());
}
}
HTTPS 인증서 무시 금지

trustAll TrustManager는 개발 환경에서만 사용하세요. 프로덕션에서 인증서 검증을 비활성화하면 중간자 공격(MITM)에 취약해집니다.