도입
Spring MVC나 Servlet API처럼 더 큰 웹 프레임워크에 익숙하다면 HttpExchange 는 상당히 작고 직접적인 인터페이스로 보입니다. 하지만 바로 그 단순함 덕분에 JDK만으로 가벼운 내장 HTTP 서버를 만들 때 매우 직관적으로 사용할 수 있습니다.
핵심은 이 객체가 요청 정보와 응답 작성 통로를 한 번에 들고 있다는 점입니다. 즉 요청 메서드, URI, 헤더, 본문을 여기서 읽고, 같은 객체를 통해 응답 헤더와 본문도 작성합니다.
그래서 HttpExchange 를 이해한다는 것은 단순히 메서드 몇 개를 아는 것이 아니라, JDK 기본 HTTP 서버가 하나의 요청-응답 생명주기를 어떻게 모델링하는지 이해하는 일에 가깝습니다.
필요성
JDK 내장 HTTP 서버 API는 복잡한 웹 애플리케이션 프레임워크라기보다, 간단한 HTTP/HTTPS 서버를 만들기 위한 최소 단위 도구에 가깝습니다. 이 구조에서 HttpExchange 는 실제 요청 처리의 중심입니다.
예를 들어 로컬 개발용 콜백 서버, 사내 유틸리티 서버, 테스트용 모의 HTTP 엔드포인트, 간단한 상태 확인용 관리 포트처럼 외부 프레임워크 없이 빠르게 HTTP 응답을 만들고 싶을 때 유용합니다.
즉 HttpExchange 는 거대한 웹 프레임워크의 추상화 대신, 요청과 응답을 더 직접 제어해야 하는 상황에서 쓰는 낮은 층의 핸들링 인터페이스라고 볼 수 있습니다.
- JDK만으로 간단한 내장 HTTP 서버를 만들고 싶을 때
- 테스트용 mock endpoint 나 callback receiver 가 필요할 때
- 무거운 웹 프레임워크 없이 요청/응답을 직접 제어하고 싶을 때
- 로컬 개발, 디버깅, 관리성 엔드포인트 같은 가벼운 용도에서 빠르게 서버를 띄우고 싶을 때
정의
HttpExchange 는 하나의 HTTP 요청과 그에 대한 응답 생성을 캡슐화한 추상 클래스이며, HttpHandler 가 실제 처리 단위로 받는 객체다공식 문서 기준으로 HttpExchange 는 요청을 읽고 응답을 구성하고 전송하는 데 필요한 기능을 제공합니다. 즉 요청-응답 쌍 전체를 하나의 교환 단위로 모델링한 것입니다.
또 이 클래스는 AutoCloseable 을 구현하므로, 자원 종료라는 측면에서도 명시적으로 다뤄야 하는 객체입니다. 요청 본문 스트림과 응답 본문 스트림을 열고 닫는 책임이 exchange 수명주기와 직접 연결됩니다.
즉 HttpExchange 는 단순 DTO가 아니라, 요청 상태와 응답 송신 상태를 함께 들고 있는 입출력 중심 객체로 이해하는 편이 더 정확합니다.
"HttpExchange 의 본질은 요청 정보를 담는 객체가 아니라
요청 읽기와 응답 쓰기 전체를 한 생명주기로 묶는 입출력 컨텍스트에 가깝습니다."
핵심 원리
HttpExchange 로부터 요청 메서드·URI·헤더·본문을 읽고, 응답 헤더를 설정한 뒤 sendResponseHeaders() 와 응답 스트림으로 응답을 마무리한다공식 문서가 제시하는 전형적인 순서는 매우 분명합니다. 먼저 요청 메서드를 보고, 필요하면 요청 헤더를 확인하고, 요청 본문을 읽습니다. 그다음 응답 헤더를 설정하고, sendResponseHeaders() 를 호출한 뒤 응답 본문 스트림에 실제 바이트를 씁니다.
여기서 특히 중요한 점은 응답 본문 스트림을 얻기 전에 반드시 sendResponseHeaders() 를 호출해야 한다는 것입니다. 또 exchange를 제대로 끝내려면 응답 스트림을 닫아야 하고, 이 과정에서 요청 스트림도 함께 닫힐 수 있습니다.
즉 HttpExchange 는 단순히 request/response를 저장하는 객체가 아니라, 읽기와 쓰기의 순서를 포함한 프로토콜 핸들링 객체라고 보는 편이 맞습니다.
HttpExchange Lifecycle
1) getRequestMethod()
2) getRequestHeaders()
3) getRequestBody()
4) getResponseHeaders()
5) sendResponseHeaders(status, length)
6) getResponseBody()
7) write response bytes
8) close streams / close exchange
주요 메서드
| 메서드 | 역할 | 실무 해석 |
|---|---|---|
getRequestMethod() |
HTTP 메서드 반환 | GET, POST 같은 분기 기준 |
getRequestURI() |
요청 URI 반환 | path, query 파싱의 시작점 |
getRequestHeaders() |
요청 헤더 조회 | immutable Headers 반환 |
getRequestBody() |
요청 본문 읽기 | InputStream 로 직접 읽어야 함 |
getResponseHeaders() |
응답 헤더 설정 | mutable Headers 반환 |
sendResponseHeaders(int,long) |
상태 코드와 본문 길이 전송 시작 | 응답 스트림 전에 반드시 호출 |
getResponseBody() |
응답 본문 쓰기 | OutputStream 에 바이트 기록 |
close() |
exchange 종료 | 요청/응답 스트림 정리의 편의 메서드 |
getRemoteAddress(), getLocalAddress() |
원격/로컬 소켓 주소 확인 | 로깅, 접근 추적에 유용 |
getProtocol() |
프로토콜 문자열 반환 | HTTP/1.1 같은 요청 프로토콜 확인 |
getPrincipal() |
인증 사용자 조회 | Authenticator 가 연결된 경우에만 의미 있음 |
getAttribute(), setAttribute() |
교환 범위 속성 저장 | Filter 와 handler 사이 out-of-band 통신에 사용 |
헤더와 스트림
Headers 라는 Map 형태 객체로 다뤄지고, 요청 스트림과 응답 스트림은 exchange 종료와 직접 연결된다Headers 는 Map<String, List<String>> 형태이며, 헤더 이름은 대소문자를 구분하지 않습니다. 요청 헤더는 읽기 전용이고, 응답 헤더는 쓰기 가능합니다.
또 요청 본문과 응답 본문은 각각 InputStream, OutputStream 으로 직접 다뤄야 합니다. 즉 바디를 자동으로 문자열이나 JSON으로 바꿔 주는 레이어는 기본 제공되지 않습니다.
이 점 때문에 HttpExchange 는 프레임워크보다 훨씬 로우 레벨한 느낌을 줍니다. 하지만 반대로 말하면, 요청/응답 전송을 바이트 수준에서 더 직접 제어할 수 있다는 뜻이기도 합니다.
기본 구현
HttpExchange 를 가장 빨리 이해하는 방법은 HttpServer 에 context 를 등록하고, handler 안에서 요청을 읽어 응답을 써 보는 것이다import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.Executors;
public class SimpleHttpExchangeExample {
public static void main(String[] args) throws IOException {
HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
server.createContext("/hello", exchange -> {
String method = exchange.getRequestMethod();
String path = exchange.getRequestURI().getPath();
String response = "method=" + method + ", path=" + path;
byte[] bytes = response.getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().set("Content-Type", "text/plain; charset=UTF-8");
exchange.sendResponseHeaders(200, bytes.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(bytes);
}
});
server.setExecutor(Executors.newFixedThreadPool(4));
server.start();
}
}
sendResponseHeaders() 와 getResponseBody() 의 순서입니다. 응답 본문 스트림은 헤더 전송이 시작된 뒤에만 정상적으로 사용할 수 있습니다.패턴 1. 요청 읽기와 응답 쓰기
요청 처리에서 먼저 볼 것은 메서드와 URI입니다. 이 둘만으로도 많은 엔드포인트 분기를 처리할 수 있습니다. 필요하면 헤더나 본문을 추가로 읽으면 됩니다.
응답 쪽에서는 상태 코드와 헤더를 먼저 결정하고, 그다음 바디를 쓰는 순서를 지켜야 합니다. 서블릿 컨테이너처럼 더 높은 추상화가 자동으로 감춰 주지 않기 때문에 이 순서를 직접 관리해야 합니다.
String method = exchange.getRequestMethod();
String path = exchange.getRequestURI().getPath();
String userAgent = exchange.getRequestHeaders().getFirst("User-Agent");
exchange.getResponseHeaders().set("Content-Type", "text/plain; charset=UTF-8");
exchange.sendResponseHeaders(200, responseBytes.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(responseBytes);
}
패턴 2. sendResponseHeaders() 길이 규칙 이해하기
sendResponseHeaders(status, responseLength) 의 두 번째 인자는 단순 메타데이터가 아니라 응답 바디 전송 방식 자체를 결정합니다.
값이 0보다 크면 정확히 그 바이트 수를 써야 하고, 부족하거나 넘치면 IOException 이 발생할 수 있습니다. 0이면 chunked transfer encoding 이고, -1 이하이면 응답 바디를 쓰지 않는 것으로 해석됩니다.
즉 이 메서드는 단순 상태 코드 전송이 아니라, 응답 본문 계약까지 함께 시작하는 메서드라고 보는 편이 맞습니다.
// 고정 길이 응답
exchange.sendResponseHeaders(200, bytes.length);
// chunked 응답
exchange.sendResponseHeaders(200, 0);
// 본문 없는 응답
exchange.sendResponseHeaders(204, -1);
패턴 3. Context, Filter, 인증과 함께 보기
HttpExchange 는 단독으로도 쓸 수 있지만, 실제로는 HttpContext, Filter, Authenticator 와 연결해서 보면 더 입체적으로 이해된다HttpServer 는 URI path를 기준으로 HttpContext 를 찾고, 그 context에 연결된 HttpHandler 로 요청을 넘깁니다. 공식 문서상 매핑은 요청 URI path와 context path의 longest matching prefix 기준이며, 경로 비교는 literal 하고 case-sensitive 합니다.
또 Filter 는 handler 전후의 공통 전처리·후처리에 쓰이고, HttpExchange.setAttribute() / getAttribute() 를 통해 filter와 handler 사이에서 부가 정보를 넘길 수 있습니다.
인증기가 context에 연결된 경우에는 getPrincipal() 로 현재 인증된 사용자를 읽을 수 있습니다. 즉 HttpExchange 는 단일 요청 처리 객체이면서, context와 filter와 auth가 만나는 접점이기도 합니다.
한계와 주의점
HttpExchange 는 매우 유용하지만, 기본 제공 HTTP 서버 전체가 최소한의 단순 HTTP 의미를 우선한 구조라서 풀기능 고성능 웹 서버처럼 보면 안 된다공식 모듈 문서도 이 API와 기본 구현이 minimal HTTP server와 simple HTTP semantics를 주로 겨냥한다고 설명합니다. 즉 JDK 내장 HTTP 서버는 범용 고성능 애플리케이션 서버를 대체하려는 성격이 아닙니다.
따라서 간단한 내장 서버, 로컬 도구, 테스트용 엔드포인트, 디버깅용 서버에는 잘 맞지만, 대규모 프로덕션 웹 서비스 전체를 이 계층 위에 바로 얹는 것은 신중하게 봐야 합니다.
또 응답 길이와 스트림 종료를 직접 관리해야 하므로, 상위 프레임워크가 자동으로 해 주던 안전장치가 없다는 점도 함께 기억해야 합니다.
- 기본 구현은 최소한의 내장 HTTP 서버 용도에 더 가깝다
sendResponseHeaders()와 바디 길이 규칙을 직접 맞춰야 한다- 요청 본문을 다 읽지 않고 닫으면 연결 재사용에 영향을 줄 수 있다
- 헤더·본문 직렬화, JSON 변환, 예외 매핑 같은 편의 기능은 기본 제공되지 않는다
자주 하는 실수
HttpExchange 를 어렵게 만드는 가장 흔한 원인은 요청/응답 순서와 스트림 종료 규칙을 평범한 DTO처럼 가볍게 보는 데 있다sendResponseHeaders()전에getResponseBody()를 호출함- 고정 길이 응답인데 실제 바이트 수를 맞추지 않음
- 응답 스트림을 닫지 않아 exchange 종료가 어정쩡하게 남음
- 요청 본문을 끝까지 읽지 않고 바로 닫아 연결 재사용 문제를 만듦
- 요청 헤더도 수정 가능할 것이라고 생각함
HttpExchange를 범용 고성능 웹 프레임워크 API처럼 기대함
실무 루틴
HttpExchange 를 실무에서 다룰 때는 메서드·URI·헤더·본문 읽기와 상태 코드·헤더·바디 쓰기의 순서를 먼저 고정하고, 예외와 종료 처리를 항상 한 묶음으로 설계하는 편이 안전하다- 먼저
getRequestMethod()와getRequestURI()로 요청 성격을 판단한다. - 필요한 경우에만 헤더와 요청 본문을 읽고, 본문은 가능하면 끝까지 소비한다.
- 응답 헤더를 설정한 뒤
sendResponseHeaders()를 호출한다. - 그다음에만
getResponseBody()로 바디를 쓴다. - 고정 길이 응답이면 실제 바이트 수를 정확히 맞춘다.
- 마지막에는 응답 스트림 또는
exchange.close()로 수명주기를 명확히 끝낸다.
디버깅
sendResponseHeaders() 를 getResponseBody() 보다 먼저 호출했는지 확인한다.exchange.close() 를 호출했는지 본다.HttpContext path 매핑이 longest prefix / literal / case-sensitive 규칙에 맞는지 확인한다.점검 체크리스트
- 요청 메서드와 URI는 기대한 값인가
- 요청 헤더는 immutable 인데 수정하려고 하지는 않았는가
- sendResponseHeaders 를 먼저 호출했는가
- responseLength 와 실제 바이트 수가 맞는가
- 요청/응답 스트림을 정상적으로 닫았는가
- context path 가 literal / case-sensitive 하게 매핑되는 점을 놓치지 않았는가
요약
HttpExchange 의 핵심은 요청과 응답을 한 생명주기로 묶어 메서드·URI·헤더·본문을 읽고, 상태 코드·응답 헤더·본문을 직접 작성하게 해 주는 데 있으며, JDK 내장 HTTP 서버에서는 이 객체가 실질적인 처리 중심이 된다- ✅
HttpExchange는 요청 하나와 응답 하나를 묶는 exchange 객체다. - ✅ 요청 읽기와 응답 쓰기의 전형적인 순서는 공식 문서에 정해져 있다.
- ✅ 요청 헤더는 immutable, 응답 헤더는 mutable 이다.
- ✅ 응답 본문은
sendResponseHeaders()이후에만 써야 한다. - ✅ 고정 길이 응답이면 정확한 바이트 수를 써야 한다.
- ✅ 요청 본문을 다 읽지 않고 닫으면 연결 재사용에 영향을 줄 수 있다.
- ✅
Filter,HttpContext,Authenticator와 함께 보면 구조가 더 잘 보인다. - ✅ JDK 기본 HTTP 서버는 단순 내장 서버 용도에 더 잘 맞고, 풀기능 고성능 서버를 목표로 한 것은 아니다.