문자열로만 보이던 HTTP 요청과 응답을 Java 객체로 직접 모델링하며, HTTP/1.1 메시지의 각 요소가 코드에서 어떤 책임과 타입으로 표현되는지 이해하기 위해 만든 학습 프로젝트입니다.
Spring과 같은 웹 프레임워크를 사용하면 요청은 이미 객체로 전달되고, 응답도 반환값만 작성하면 완성됩니다. 편리하지만 그 과정만 사용하다 보면 다음과 같은 질문에 답하기 어려웠습니다.
- 클라이언트가 보낸 문자열은 어떻게 요청 객체가 되는가?
- 메서드, 요청 대상, 헤더, 본문은 코드에서 어떻게 분리되는가?
- JSON과 HTML은 HTTP 그 자체인가, HTTP 안에 담기는 데이터인가?
- JDK의
HttpRequest는 왜 추상 클래스이고HttpResponse<T>는 왜 인터페이스인가?
이 질문을 직접 확인하고자 실제 네트워크 서버를 만드는 대신, 원시 HTTP 메시지를 파싱하고 다시 조립하는 과정에 집중했습니다. 문자열을 그대로 다루는 코드와 구조화된 객체를 사용하는 코드의 차이를 비교하며, 웹 프레임워크가 감추고 있는 요청·응답의 기본 구조를 이해하는 것이 이 프로젝트의 목적입니다.
flowchart LR
A["HTTP 요청 원본 String"] --> B["MyRequest.fromRaw()"]
B --> C["method · URI · headers · body"]
C --> D["예제 처리 로직"]
D --> E["MyResponse.json() / html()"]
E --> F["HTTP 응답 원본 String"]
이 흐름은 메모리 안의 문자열과 객체 사이에서 진행됩니다. 소켓을 열거나 실제 서버와 통신하지는 않습니다.
HTTP는 클라이언트와 서버가 요청과 응답을 주고받기 위한 메시지 규칙입니다.
sequenceDiagram
participant Client as 클라이언트
participant Server as 서버
Client->>Server: HTTP Request
Server-->>Client: HTTP Response
HTTP/1.1 메시지는 시작 줄, 헤더, 빈 줄, 선택적인 본문으로 구성됩니다.
POST /users HTTP/1.1
Host: localhost:8080
Content-Type: application/json; charset=UTF-8
Content-Length: 26
{"name":"허혜","age":20}- 요청 줄: 메서드, 요청 대상(request-target), HTTP 버전
- 헤더: 요청에 대한 부가 정보
- 본문: 서버에 전달할 데이터
HTTP/1.1 201 Created
Content-Type: application/json; charset=UTF-8
Content-Length: 89
{
"content": "id 허연혜님의 name: 허혜, age: 20으로 등록되었습니다."
}- 상태 줄: HTTP 버전, 상태 코드, 사유 문구
- 헤더: 응답 데이터와 전송 방식에 대한 정보
- 본문: 클라이언트에 전달할 데이터
Content-Type과 Content-Length는 항상 존재하는 고정 요소가 아니라 메시지에 필요할 때 사용하는 헤더입니다.
HTTP는 데이터를 전달하는 규칙이고, JSON과 HTML은 본문에 담을 수 있는 표현 형식입니다.
HTTP 메시지
├── 시작 줄
├── 헤더
└── 본문
├── JSON
├── HTML
└── 그 밖의 데이터 형식
따라서 서버는 요청 메시지를 해석해 필요한 작업을 수행하고, 결과 데이터의 형식을 Content-Type으로 알려 응답합니다. 전송 관점에서 본문은 바이트이며, 수신자는 해당 미디어 타입과 문자 인코딩에 따라 이를 JSON이나 HTML 등의 형식으로 해석합니다.
| 파일 | 역할 |
|---|---|
MyRequest.java |
원시 요청 문자열을 요청 줄, 헤더, 본문으로 분해하고 HttpRequest의 형태로 제공 |
MyResponse.java |
상태 코드, 헤더, 본문을 가진 HttpResponse<String>을 만들고 HTTP/1.1 형태의 문자열로 조립 |
MainJSON.java |
POST 요청의 사용자 정의 헤더와 JSON 형태 본문을 읽어 201 Created JSON 응답 생성 |
MainHtml.java |
GET 요청의 사용자 정의 헤더를 읽어 200 OK HTML 응답 생성 |
MyRequest는 java.net.http.HttpRequest를 상속합니다. fromRaw() 정적 팩터리 메서드는 원시 요청을 다음 순서로 변환합니다.
- 빈 줄(
\r\n\r\n)을 기준으로 헤더 영역과 본문을 분리합니다. - 요청 줄에서 메서드, 요청 대상, HTTP 버전을 추출합니다.
- 나머지 줄을 이름과 값으로 분리해
HttpHeaders를 만듭니다. Host헤더와 요청 대상을 조합해URI를 만듭니다.- 파싱 결과를 변경할 수 없는 필드에 저장합니다.
그 결과 호출부는 문자열 위치를 직접 계산하지 않고 request.method(), request.uri(), request.headers(), request.body()처럼 의미가 드러나는 메서드로 요청을 다룰 수 있습니다.
MyResponse는 HttpResponse<String>을 구현합니다. json()과 html() 팩터리 메서드가 본문 형식에 맞는 Content-Type을 지정하고, UTF-8 바이트 수로 Content-Length를 계산합니다.
toRawHttpResponse()는 상태 줄, 헤더, 빈 줄, 본문 순서로 값을 조립해 다시 HTTP/1.1 형태의 문자열을 만듭니다. 이를 통해 파싱된 요청 객체 → 처리 결과 → 응답 객체 → 원시 응답 문자열의 흐름을 한 번에 확인할 수 있습니다.
일반적으로 JDK의 요청 객체는 HttpRequest.newBuilder()로 생성하며, 실제 구현은 내부 클래스에 감춰져 있습니다. 동시에 HttpRequest는 보호된 생성자를 제공하므로 이 프로젝트의 MyRequest처럼 직접 하위 클래스를 구현할 수도 있습니다.
추상 메서드는 구현 세부 사항을 숨기는 공통 계약을 만들고, 클래스가 제공하는 final equals()와 final hashCode()는 모든 하위 타입에 동일한 동등성 규칙을 적용합니다. 이 동등성은 메서드, URI, 헤더를 기준으로 하며 본문, 제한 시간, 선호 버전까지 완전히 같은지를 의미하지는 않습니다.
표준 Builder로 생성한 요청은 완성 후 변경되지 않아 안전하게 재사용할 수 있습니다. 다만 추상 클래스를 상속한 사용자 구현까지 자동으로 불변이 되는 것은 아니므로, MyRequest에서는 모든 상태를 final로 두고 생성 과정을 팩터리 메서드로 제한했습니다.
HttpResponse<T>는 상태 코드, 헤더, 본문, 관련 요청, URI, 실제 프로토콜 버전을 읽는 공통 계약을 제공합니다. 일반적인 애플리케이션에서는 HttpClient.send() 또는 sendAsync()의 결과로 받지만, 테스트나 학습 목적의 구현체를 직접 만들 수도 있습니다.
이 프로젝트의 MyResponse는 HttpResponse<String>을 구현해 본문 타입을 문자열로 정하고, 학습에 필요한 응답 생성과 직렬화 책임을 추가했습니다. 이를 통해 요청 쪽에서는 추상 클래스를 상속하고, 응답 쪽에서는 인터페이스를 구현하는 차이를 코드로 비교했습니다.
- JDK 17 이상
프로젝트 루트에서 다음 명령을 실행합니다.
mkdir -p out
javac -d out \
src/http/MyRequest.java \
src/http/MyResponse.java \
src/MainJSON.java \
src/MainHtml.javaJSON 응답 예제:
java -cp out MainJSONHTML 응답 예제:
java -cp out MainHtml실행하면 원본 요청 문자열, MyRequest로 파싱된 값, MyResponse에 담긴 값, 다시 조립한 응답 문자열을 순서대로 확인할 수 있습니다.
http-message-lab/
├── src/
│ ├── http/
│ │ ├── MyRequest.java
│ │ └── MyResponse.java
│ ├── MainJSON.java
│ └── MainHtml.java
├── .gitignore
└── README.md
이 프로젝트는 HTTP의 구조를 이해하기 위한 실습이며 범용 HTTP 구현체가 아닙니다.
- 실제 네트워크 통신, 서버 소켓, 라우팅을 구현하지 않습니다.
- 시작 줄 오류, 중복 헤더, 잘못된
Content-Length, 청크 전송 등을 완전하게 검증하지 않습니다. - JSON 라이브러리 대신 정규식으로 예제의 일부 값만 읽으므로 범용 JSON 파서가 아닙니다.
jwtToken이라는 사용자 정의 헤더 값을 사용할 뿐, 실제 JWT 검증이나 인증을 수행하지 않습니다.MyRequest.version()은 비교를 위해HTTP/2문자열을 JDK의 버전 열거형으로 변환하지만,toRawHttpResponse()는 실제 HTTP/2 프레임을 만들지 않습니다. 모든 실행 예제와 유효한 텍스트 직렬화 범위는 HTTP/1.1입니다.
- HTTP는 요청과 응답을 주고받기 위한 메시지 규칙이며, JSON과 HTML은 본문에 담기는 데이터 형식이다.
- 규칙이 있는 문자열을 객체로 모델링하면 파싱, 검증, 조회, 확장을 각 책임으로 나눌 수 있다.
Content-Length는 Java 문자열 길이가 아니라 실제 전송할 바이트 길이를 기준으로 계산해야 한다.- 추상 클래스와 인터페이스의 차이는 문법에 그치지 않고, 공통 동작을 어디에서 보장하고 구현체에 어떤 계약을 제공할지에 관한 설계 선택이다.
- 프레임워크가 만들어 주는 요청·응답 객체의 뒤에는 원시 메시지를 해석하고 다시 직렬화하는 과정이 있다.
이 실습을 통해 HTTP 요청과 응답을 단순한 문자열이 아니라 명확한 규칙과 책임을 가진 객체 모델로 바라보게 되었습니다.