회원가입 POST 처리 후에 index.html 화면으로 돌아가게 만들고 싶었다. 처음에는 `/user/create` 분기에서 그냥 index.html 파일을 읽어서 200으로 내려주는 방식으로 해결했는데, 그 방식은 새로고침 시 POST 재전송 문제가 생길 수 있었다. 그래서 이번에는 302 리다이렉트를 직접 구현해서 “POST 처리 → GET으로 페이지 이동” 흐름으로 바꿨다.
제일 중요한 규칙 1개
HTTP는 요청 1번 → 응답 1번이 기본 단위다.
서버는 “완료했으니 /index.html로 가”라고 응답 할 수는 있지만, 그 말 자체가 /index.html 요청을 대신 해주진 않는다.
즉 302는 “새 요청을 만들게 하는 응답”이지, “index.html을 담아서 주는 응답”이 아니다.
내가 헷갈렸던 부분
회원가입 완료한 다음, 서버가 302로 /index.html로 가라고 했는데
그럼 index.html 바이트를 읽어서 보내는 건 어디서 하냐?
302 응답에는 바디가 없다며?
정답은 이거다.
index.html 바이트를 읽어서 보내는 건 `GET /index.html` 요청을 처리하는 코드에서 한다.
302는 거기까지 “도착”시키는 안내문일 뿐이다.
진짜로 ‘두 번’ 요청이 발생한다 (이게 포인트)
회원가입 완료 후의 네트워크 흐름을 실제로 쓰면 “요청이 두 번”이다.
(A) 1번째 요청: 회원가입 요청
브라우저 → 서버:
POST /user/create HTTP/1.1
... (헤더)
(blank line)
userId=...&password=...&name=...&email=...
서버는 여기서 (지금은 로그만 찍지만) 원래라면 회원 저장 같은 “회원가입 처리”를 한다.
그리고 여기서 응답 방식이 갈린다.
(B-1) 200 OK로 index.html을 “바로 보내는 방식”(이전 방식)
서버 → 브라우저:
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: ...
(blank line)
(index.html 내용)
이 방식은 `/user/create`에 대한 응답 바디로 index.html을 직접 실어 보내는 구조이다.
그래서 서버 코드가 `/user/create` 분기 안에서 바로 Files.readAllBytes(Path.of("webapp", "index.html")) 같은 걸 실행하는 형태가 된다.
요청은 1번이고, 응답에서 바로 페이지를 내려준다.
하지만 문제는 “현재 페이지의 정체”가 `/user/create POST` 결과로 남아버린다는 점이다.
그래서 새로고침 시 POST 재전송(폼 재전송 경고) 같은 흐름이 생길 수 있다.
(B-2) 302 리다이렉트 방식(지금 방식)
서버 → 브라우저:
HTTP/1.1 302 Found
Location: /index.html
(blank line)
여기서 끝이 아니다.
브라우저는 `Location`을 보고 자동으로 2번째 요청을 새로 만든다.
브라우저 → 서버 (2번째 요청):
GET /index.html HTTP/1.1
... (헤더)
(blank line)
그리고 서버는 이때 정적 파일 로직에서 index.html을 읽어서 200 OK로 내려준다.
서버 → 브라우저:
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: ...
(blank line)
(index.html 내용)
즉, 302가 index.html을 보내는 게 아니다.
302 때문에 브라우저가 GET /index.html을 “다시 요청”하고,
그 요청에 대해 서버가 index.html을 읽어서 보내는 것이다.
“회원가입 완료한 다음”인데 왜 계속 /user/create가 나오냐
“회원가입 완료한 다음”이라고 말해도, 네트워크 순서는 이렇게 된다.
- 완료 이전에 `/user/create` 요청이 반드시 한 번은 간다 (가입 처리 자체니까)
- 완료 직후에 “다음 화면을 어떻게 보여줄지” 선택한다
- 200으로 바로 HTML 내려주면: 추가 요청 없음
- 302로 보내면: GET /index.html 추가 요청 발생
“완료한 다음”은 사실 /user/create 처리가 끝난 직후를 의미하고, 그 직후에 200을 줄지 302를 줄지 갈리는 것이다.
코드
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);
DataOutputStream dos = new DataOutputStream(out);
response302Header(dos);
} else {
DataOutputStream dos = new DataOutputStream(out);
byte[] body = Files.readAllBytes(Path.of("webapp", url));
response200Header(dos, body.length);
responseBody(dos, body);
}
} catch (IOException e) {
log.error(e.getMessage());
}
}
`user/create` 처리 후에 `302`로 `Location`만 내려준다.
즉, 이 코드를 실행한다.
private void response302Header(DataOutputStream dos) {
try {
dos.writeBytes("HTTP/1.1 302 Found \r\n");
dos.writeBytes("Location: /index.html\r\n");
dos.writeBytes("\r\n");
} catch (IOException e) {
log.error(e.getMessage());
}
}
이 방식으로 얻는 효과
이제 회원가입 이후 브라우저는 최종적으로 GET /index.html 페이지에 위치하게 된다.
그래서:
- 주소창이 /user/create에 머무르지 않는다.
- 새로고침이 POST 재전송이 아니라 GET /index.html 새로고침이 된다.
- “POST 처리 후 다음 화면은 GET으로 보여준다”는 흐름이 만들어진다. (PRG 패턴 방향)
출처 : 《자바 웹 프로그래밍 Next Step》, 박재성, 로드북
'Book > 자바 웹 프로그래밍 Next Step' 카테고리의 다른 글
| 5장_: 임베디드 톰캣으로 웹 서버 띄우기 (0) | 2026.02.05 |
|---|---|
| 3장_ 로깅: System.out.println() 대신 로깅을 써야 하는 이유 (0) | 2026.02.02 |
| 3장_ 요구사항3: POST방식으로 회원가입 (0) | 2026.02.01 |
| 3장_ 메모: 요청 메시지의 형태 (0) | 2026.01.31 |
| 3장_ 메모: HTTP 요청 라인 파싱과 멀티 스레드 처리 (0) | 2026.01.31 |
