기존에는 회원가입이 GET 방식으로 동작했다. 즉 `/user/create?userId=...&password=...` 같은 형태로 데이터가 URL에 붙어서 들어왔다. 그런데 요구사항은 `http://localhost:8080/user/form.html`의 `<form>` 태그에서 method를 get에서 post로 바꾸고도 회원가입이 정상 동작하도록 서버를 고치라는 것이다.
POST로 바뀌면 중요한 변화가 생긴다.
- GET: 데이터가 URL 쿼리스트링에 붙어서 온다.
- POST: 데이터가 요청 바디(request body) 로 온다. (URL에는 보통 /user/create까지만 온다)
즉 서버는 더 이상 URL에서 ? 뒤를 파싱하면 안 되고, 헤더에서 Content-Length를 읽고, 그 길이만큼 바디를 읽어서 파싱해야 한다.
이걸 RequestHandler에서 직접 구현한 것이 아래 코드이다.
요구사항 전제: form 태그가 이렇게 바뀜
`/user/form.html`에서 대략 이런 식으로 바뀌는 상황이다.
- 기존(예시)
<form action="/user/create" method="get">
- 변경 후
<form action="/user/create" method="post">
이렇게 되면 브라우저는 `/user/create`로 POST 요청을 날리고, 입력값은 URL이 아니라 body에 담아서 보낸다.
서버는 요청을 “첫 줄(요청라인)”부터 읽는다
HTTP 요청은 텍스트이고, 첫 줄이 제일 중요하다.
- 요청라인 형식
<메소드> <URL> <HTTP버전>
그래서 서버는 먼저 첫 줄을 읽는다.
String requestLine = br.readLine();
if (requestLine == null || requestLine.isBlank()) {
return;
}
여기서 빈 줄이면 아예 요청이 없는 것으로 보고 종료한다.
그리고 첫 줄을 공백으로 분리해서 method와 url을 뽑는다.
String[] parts = requestLine.split(" ");
String method = parts[0];
String url = parts[1];
이제 서버는 “GET인지 POST인지”, “어떤 경로인지” 판단할 수 있게 된다.
POST에서 핵심: 헤더를 읽고 Content-Length를 찾아야 한다
POST는 body가 있다.
그런데 body를 읽을 때 얼마나 읽어야 하는지가 중요하다. 그 길이는 보통 헤더의 `Content-Length`에 있다.
요청 헤더는 “두 번째 줄부터 빈 줄 전까지”이다.
그래서 빈 줄이 나올 때까지 헤더를 읽는다.
int contentLength = 0;
requestLine = br.readLine();
while (requestLine != null && !requestLine.isEmpty()) {
log.debug("header : {}", requestLine);
if (requestLine.startsWith("Content-Length:")) {
contentLength = getContentLength(requestLine);
}
requestLine = br.readLine();
}
여기서 중요한 포인트 2개
- `requestLine.isEmpty()`가 되는 순간이 “빈 줄”이다.
→ HTTP에서 헤더 끝을 의미하는 그 빈 줄이다. - 헤더를 읽는 동안 `Content-Length:`를 발견하면 숫자를 뽑아서 저장한다.
→ 이 값이 있어야 body를 정확히 읽을 수 있다.
`getContentLength()`는 헤더 한 줄에서 `:` 기준으로 나눠서 길이 숫자를 추출한다.
private int getContentLength(String line) {
String headTokens[] = line.split(":");
return Integer.parseInt(headTokens[1].trim());
}
POST /user/create 인 경우: body를 읽고 파싱해서 User 생성
이제 조건 분기가 들어간다.
- method가 POST인지
- url이 `/user/create`인지
if ("POST".equals(method) && url.startsWith("/user/create")) {
String requestBody = IOUtils.readData(br, contentLength);
Map<String, String> params = HttpRequestUtils.parseQueryString(requestBody);
User user = new User(
params.get("userId"),
params.get("password"),
params.get("name"),
params.get("email")
);
log.debug("User : {}", user);
url = "index.html";
}
여기서 흐름을 딱 나누면 이렇다.
(1) body 읽기
String requestBody = IOUtils.readData(br, contentLength);
`IOUtils.readData()`는 “contentLength만큼” 읽어서 문자열로 반환하는 함수이다.
POST로 회원가입 폼이 전송되면 body는 대체로 이런 형태로 온다.
userId=javajigi&password=password&name=JaeSung&email=javajigi%40slipp.net
즉 GET의 querystring이 URL에 붙어있던 게, POST에서는 body로 내려온 것뿐이다. 포맷은 거의 같다.
(2) body 파싱
Map<String, String> params = HttpRequestUtils.parseQueryString(requestBody);
`parseQueryString()`은 &로 쪼개고, =로 키/값을 나눠서 Map으로 만들어준다.
(3) User 생성
User user = new User(...);
log.debug("User : {}", user);
여기까지 오면 “폼에서 입력한 값이 제대로 서버로 들어와서 User로 만들어졌다”를 확인할 수 있다.
(4) 처리 후 index.html 응답
url = "index.html";
“응답으로 index.html 파일을 내려준다”는 의미로 url을 바꾸었다.
사용자는 회원가입 후 메인 페이지를 보는 흐름을 경험하게 된다.
정적 파일 응답 처리: `/`는 `index.html`로 매핑
브라우저가 처음 접속할 때 `GET /`를 보내는 경우가 많다.
이때 /를 그대로 파일로 읽으면 디렉터리 문제가 터질 수 있다.
그래서 홈 요청은 `index.html`로 강제 매핑한다.
if ("/".equals(url)) url = "index.html";
응답은 “상태라인 + 헤더 + 빈 줄 + 바디”로 구성된다
정적 파일을 내려줄 때는 byte 배열로 읽고, 200 OK 응답을 만들어서 내려준다.
byte[] body = Files.readAllBytes(Path.of("webapp", url));
response200Header(dos, body.length);
responseBody(dos, body);
(1) 200 OK 헤더 작성
dos.writeBytes("HTTP/1.1 200 OK \r\n");
dos.writeBytes("Content-Type: text/html;charset=utf-8\r\n");
dos.writeBytes("Content-Length: " + lengthOfBodyContent + "\r\n");
dos.writeBytes("\r\n");
- HTTP/1.1 200 OK는 상태라인이다.
- 그 다음은 헤더이다.
- 마지막 \r\n 한 번 더가 “빈 줄”이다.
→ 이 빈 줄 다음부터가 바디라는 뜻이다.
(2) 바디 전송
dos.write(body, 0, body.length);
dos.flush();
근데 이렇게 하면 문제가 있다.
지금은 `POST /user/create`의 응답 바디로 index.html 내용을 그대로 내려주는 방식인데,
이 방식은 브라우저의 주소창이 `/user/create`로 남아 "이동한 것처럼 보이지만 실제로는 이동이 아닌 상태"이다.
그래서 새로고침 시 POST가 재전송 되면서, 사용자가 입력한 데이터가 그대로 쌓이거나 에러가 나게 된다.
다음에는 index.html로 리다이렉트 하는 방식으로 개선해 보겠다.
출처 : 《자바 웹 프로그래밍 Next Step》, 박재성, 로드북
'Book > 자바 웹 프로그래밍 Next Step' 카테고리의 다른 글
| 3장_ 로깅: System.out.println() 대신 로깅을 써야 하는 이유 (0) | 2026.02.02 |
|---|---|
| 3장_ 요구사항 4: 302 status code 적용 (0) | 2026.02.01 |
| 3장_ 메모: 요청 메시지의 형태 (0) | 2026.01.31 |
| 3장_ 메모: HTTP 요청 라인 파싱과 멀티 스레드 처리 (0) | 2026.01.31 |
| 3장_ 요구사항2 : GET 방식으로 회원가입하기 (0) | 2026.01.31 |
