소스: java-src/intermediate/09_http_json (MiniJson, FakeApi, ApiClient, Main). FakeApi 가 127.0.0.1 임의 포트에서 회원 API 를 흉내 내고, 같은 프로세스의 ApiClient 가 그걸 부릅니다.
cd java-src/intermediate/09_http_json
javac -encoding UTF-8 *.java && java -Dstdout.encoding=UTF-8 MainHttpResponse<String> res = client.get("/members");
System.out.println("상태: " + res.statusCode());
System.out.println("Content-Type: "
+ res.headers().firstValue("Content-Type").orElse("-"));
System.out.println("본문: " + res.body());상태: 200
Content-Type: application/json; charset=utf-8
본문: {"page":1,"size":10,"total":0,"items":[]}빈 목록이라도 200 입니다. "없음" 은 빈 배열이지 404 가 아닙니다. 404 는 자원 자체가 없을 때 씁니다(예제 4).
String body = MiniJson.write(MiniJson.obj("name", "김철수", "email", "kim@test.com"));
System.out.println("요청 본문: " + body);
HttpResponse<String> res = client.post("/members", body);
System.out.println("상태: " + res.statusCode());
System.out.println("Location: " + res.headers().firstValue("Location").orElse("-"));
System.out.println("응답 본문: " + res.body());요청 본문: {"name":"김철수","email":"[email protected]"}
상태: 201
Location: /members/1
응답 본문: {"id":1,"name":"김철수","email":"[email protected]"}Map.of() 는 항목이 둘 이상이면 순서가 실행마다 바뀝니다. 순서가 필요한 출력·테스트에서는 MiniJson.obj(...) 처럼 LinkedHashMap 기반 헬퍼를 씁니다. 201 은 새 자원의 위치를 Location 헤더로 함께 돌려줍니다.
String encoded = URLEncoder.encode("김 철수", StandardCharsets.UTF_8);
System.out.println("URLEncoder.encode(\"김 철수\") = " + encoded);
HttpResponse<String> res = client.get("/members?page=1&size=1");
System.out.println("상태: " + res.statusCode());
System.out.println("본문: " + res.body());URLEncoder.encode("김 철수") = %EA%B9%80+%EC%B2%A0%EC%88%98
상태: 200
본문: {"page":1,"size":1,"total":2,"items":[{"id":1,"name":"김철수","email":"[email protected]"}]}공백은 + 로, 한글은 퍼센트 인코딩으로 바뀝니다. page, size 는 페이징 쿼리로, total 은 전체 개수, items 는 이번 페이지 목록입니다.
Map<String, Object> parsed = MiniJson.read(res.body());
System.out.println("파싱한 total 필드: " + parsed.get("total")
+ " (타입: " + parsed.get("total").getClass().getSimpleName() + ")");
HttpResponse<String> r404 = client.get("/members/999");
System.out.println("GET /members/999 상태: " + r404.statusCode()
+ " 본문: " + r404.body());파싱한 total 필드: 2 (타입: Long)
GET /members/999 상태: 404 본문: {"error":"NOT_FOUND","message":"id 999 회원이 없습니다"}MiniJson.read 가 돌려주는 숫자는 소수점이 없으면 Long 입니다. 정수라고 바로 Integer 로 캐스팅하면 ClassCastException 이 납니다. 없는 자원은 예외가 아니라 404 상태 코드로 돌아옵니다.
private static void describeStatus(String label, int status, String body) {
String kind;
if (status >= 200 && status < 300) kind = "성공";
else if (status >= 400 && status < 500) kind = "클라이언트 오류";
else if (status >= 500) kind = "서버 오류";
else kind = "기타";
System.out.println(label + " -> " + status + " (" + kind + ") 본문: " + body);
}PUT /members/1 -> 200 (성공) 본문: {"id":1,"name":"김철수2","email":"[email protected]"}
DELETE /members/1 -> 204 (성공) 본문:
GET /error -> 500 (서버 오류) 본문: {"error":"INTERNAL","message":"서버 내부 오류 예시"}status 는 예외가 아니라 그냥 정수입니다. 계열별로 나눠 처리하면 새로운 코드(예 429)가 와도 최소한 "클라이언트 오류" 로는 분류됩니다. DELETE 204 는 본문이 비어 있는 게 정상입니다.
try {
new ApiClient("http://127.0.0.1:1").get("/members");
} catch (ConnectException e) {
System.out.println("연결 거부: " + e.getClass().getSimpleName());
}
try {
client.get("/slow", Duration.ofMillis(300));
} catch (HttpTimeoutException e) {
System.out.println("타임아웃: " + e.getClass().getSimpleName());
}연결 거부: ConnectException - 서버가 그 포트에서 응답하지 않습니다
타임아웃: HttpTimeoutException - 300ms 안에 응답이 없었습니다ConnectException 은 그 포트에 아무도 듣고 있지 않을 때, HttpTimeoutException 은 상대는 있지만 정해진 시간 안에 응답이 안 올 때 납니다. /slow 는 2초 뒤 응답하는데 제한 시간을 300ms 로 걸어서 일부러 타임아웃을 냈습니다. 둘 다 IOException 의 자손이라 상태 코드와 달리 예외로 잡아야 합니다.