본문 바로가기
교재 실습/자바 웹 프로그래밍 Next Step

4.2.1.4 요구사항 4 - 302 status code 적용

by Jint 2025. 2. 21.
요구사항 "회원가입"을 완료하면 /index.html 페이지로 이동하고 싶다. 현재는 URL이 /user/create 로 유지되는 상태로 읽어서 전달할 파일이 없다. 따라서 회원가입을 완료한 후 /index.html 페이지로 이동한다. 브라우저의 URL도 /user/create가 아니라 /index.html로 변경해야 한다.

 

앞의 구현을 통해 회원가입을 완료했다. 회원가입을 완료한 후 사용자에게 첫 화면(/index.html)을 보여주고 싶다. 첫 화면을 보여주는 방법은 의외로 간단하다. 회원가입 요청(/user/create)을 완료한 후 요청 URL 값을 "/index.html"로 변경하면 웹 서버는 index.html 파일을 읽어 응답으로 보낼 수 있다.

 

String url = tokens[1];
if ("/user/create".equals(url)) {
    ...
    log.debug("User : {}", user);
    url = "/index.html";
}

 

위 코드와 같이 쉽게 구현을 끝낼 수 있다. 그런데 이 구현 방식에 한 가지 문제점이 있다. 이와 같이 구현한 후 브라우저에서 새로고침 버튼을 클릭한 후 이클립스 콘솔을 확인하면, 앞에서 요청을 보냈던 회원가입 요청이 재전송되는 것을 확인할 수 있다. 현재 보고있는 화면은 첫 화면(index.html)인데 새로고침을 하면 이전과 똑같은 회원가입 요청이 발생한다.

이 같은 현상이 발생하는 이유는 브라우저가 이전 요청 정보를 유지하고 있기 때문이다. 새로고침 버튼을 클릭하면 유지하고 있던 요청을 다시 요청하는 방식으로 동작하기 때문이다. 이전 요청 정보를 확인하려면 브라우저의 URL을 확인해 보면 알 수 있다. 회원가입을 완료한 후 브라우저 URL은 /user/create 이다. 그런데 현재 보고있는 화면은 /index.html의 결과 화면을 보고 있다. 이 상태에서 새로고침을 하면 /index.html 화면을 보여주기 전에 회원가입 처리를 한 후 /index.html을 응답으로 전송하게 된다. 이와 같이 구현할 경우 같은 데이터가 중복으로 전송되는 이슈가 발생한다.

이 문제를 해결하는 방법은 회원가입을 처리하는 /user/create 요청과 첫 화면(/index.html)을 보여주는 요청을 분리한 후 HTTP의 302 상태 코드(status code)를 활용해 해결할 수 있다. 즉, 웹 서버는 /user/create 요청을 받아 회원가입을 완료한 후 응답을 보낼 때 클라이언트(웹 브라우저)에게 /index.html로 이동하도록 할 수 있다. 이 때 사용하는 상태 코드가 302 상태 코드이다. /index.html로 이동하도록 응답을 보낼 때 사용하는 응답 헤더는 Location으로 다음과 같이 응답을 보내면 된다.

 

HTTP/1.1 302 Found
Location: /index.html

 

위와 같이 응답을 보내면 클라이언트는 첫 라인의 상태 코드를 확인한 후 302라면 Location의 값을 읽어 서버에 재요청을 보내게 된다. 이와 같은 과정으로 요청을 보내면 클아이언트의 요청은 회원가입 처리를 위한 /user/create 요청이 아니라 /index.html 요청으로 변경된다. 이 상태에서 브라우저 URL을 확인해보면 /user/create 가 아닌 /index.html 으로 변경된 것을 확인할 수 있다. 이 과정이 상당히 빠르게 실행되기 때문에 눈으로 확인하기 쉽지 않다.

요구사항 4를 구현한 결과 코드는 다음과 같다.

 

- RequestHandler.java

package webserver;

import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.File;
import java.nio.file.Files;
import java.net.Socket;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class RequestHandler extends Thread {
    private static final Logger log = LoggerFactory.getLogger(RequestHandler.class);

    private Socket connection;

    public RequestHandler(Socket connectionSocket) {
        this.connection = connectionSocket;
    }

    public void run() {
        log.debug("New Client Connect! Connected IP : {}, Port : {}", connection.getInetAddress(), connection.getPort());

        try (InputStream in = connection.getInputStream(); OutputStream out = connection.getOutputStream()) {
            // TODO 사용자 요청에 대한 처리는 이 곳에 구현하면 된다.
            BufferedReader br = new BufferedReader(new InputStreamReader(in, "UTF-8"));
            String line = br.readLine();
            log.debug("request line : {}", line);

            if (line == null) {
                return;
            }

            String[] tokens = line.split(" ");
            int contentLength = 0;
            while (!line.equals("")) {
                line = br.readLine();
                log.debug("header : {}", line);
                if (line.contains("Content-Length")) {
                    contentLength = getContentLength(line);
                }
            }

            String url = tokens[1];
            // POST
            if ("/user/create".equals(url)) {
                String body = IOUtils.readData(br, contentLength);
                Map<String, Object> params = HttpRequestUtils.parseQueryString(body);
                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, "/index.html");
            }
            /*
            // GET
            if (url.startsWith("/user/create")) {
                int index = url.indexOf("?");
                String queryString = url.substring(index + 1);
                Map<String, Object> params = HttpRequestUtils.parseQueryString(queryString);
                User user = new User(params.get("userId"), params.get("password"), params.get("name"), params.get("email"));
                log.debug("User : {}", user);
            } 
            */
            else {
                DataOutputStream dos = new DataOutputStream(out);
                // byte[] body = "Hello World".getBytes();
                byte[] body = Files.readAllBytes(new File("./webapp" + tokens[1]).toPath());
                response200Header(dos, body.length);
                responseBody(dos, body);
            }
        } catch (IOException e) {
            log.error(e.getMessage());
        }
    }

    private int getContentLength(String line) {
        String[] headerTokens = line.split(":");
        return Integer.parseInt(headerTokens[1].trim());
    }

    private void response302Header(DataOutputStream dos, String url) {
        try {
            dos.writeBytes("HTTP/1.1 302 Redirect \r\n");
            dos.writeBytes("Location: " + url + " \r\n");
            dos.writeBytes("\r\n");
        } catch {
            log.error(e.getMessage());
        }
    }

    private void response200Header(DataOutputStream dos, int lengthOfBodyContent) {
        try {
            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");
        } catch (IOException e) {
            log.error(e.getMessage());
        }
    }

    private void responseBody(DataOutputStream dos, byte[] body) {
        try {
            dos.write(body, 0, body.length);
            dos.flush();
        } catch (IOException e) {
            log.error(e.getMessage());
        }
    }
}

 

302 상태 코드를 사용하기 전과 사용했을 때의 클라이언트와 서버 사이의 요청과 응답 흐름을 그림으로 살펴보면 다음과 같다.

 

                            HTTP 웹 서버
웹  /user/create 요청      ┌────────────────────────┐
브 ───────────────────────────→  · 회원가입 처리│
라                         │                                                │
우 ←───────────────────────────  · /index.html 읽기 │
저  /index.html 200 응답   └────────────────────────┘

 

302 상태 코드를 사용하기 전은 위 그림과 같이 회원가입 요청을 하면 회원가입 처리를 완료한 후 index.html 파일을 읽어 응답을 보내는 방식이었다. 클라이언트와 서버 간의 요청과 응답이 한 번만 발생한다. 302 상태 코드를 사용하는 경우는 다음 그림과 같다.

 

                            HTTP 웹 서버
    /user/create 요청      ┌────────────────────────┐
   ───────────────────────────→                  │
                           │     · 회원가입 처리                           │
웹 ←───────────────────────────                  │
브  /index.html 302 응답   └────────────────────────┘
라
우  /index.html 요청       ┌────────────────────────┐
저 ───────────────────────────→                  │
                           │     · /index.html 읽기                        │
   ←───────────────────────────                  │
    /index.html 200 응답   └────────────────────────┘

 

302 상태 코드를 활용해 페이지를 이동할 경우 요청과 응답이 한 번이 아니라 두 번 발생한다. 302 상태 코드를 활용한 페이지 이동 방식은 많은 라이브러리와 프레임워크에서 리다이렉트 이동 방식으로 알려져 있다. 앞으로 웹 애플리케이션을 개발하면서 리다이렉트 방식으로 페이지를 이동한다고 하면 내부적으로 302 상태 코드를 활용해 이동하겠구나 생격하면 된다.

이와 같이 HTTP는 서버에서 클라이언트로 응답을 보낼 때 상태 코드를 활용해 요청에 대한 처리 상태를 클라이언트가 인식할 수 있도록 한다. 대표적으로 사용되는 상태 코드는 다음과 같다.

 

· 2XX : 성공. 클라이언트가 요청한 동작을 수신하여 이해했고 승낙했으며 성공적으로 처리.
· 3XX : 리다이렉션. 클라이언트는 요청을 마치기 위해 추가 동작이 필요함.
· 4XX : 요청 오류. 클라이언트에 오류가 있음.
· 5XX : 서버 오류. 서버가 유효한 요청을 명백하게 수행하지 못했음.



참고도서 : https://roadbook.co.kr/169

 

[신간안내] 자바 웹 프로그래밍 Next Step

● 저자: 박재성 ● 페이지: 480 ● 판형: 사륙배변형(172*225) ● 도수: 1도 ● 정가: 30,000원 ● 발행일: 2016년 9월 19일 ● ISBN: 978-89-97924-24-0 93000 [강컴] [교보] [반디] [알라딘] [예스24] [인터파크] [샘

roadbook.co.kr

댓글