🔗 참고자료

  • 책 [자바 웹 프로그래밍 Next Step] - 저자 박재성 => 링크

 

 

✍ 공부하게 된 계기

자바와 좀 더 친해지고 웹 개발에 전반적인 내용을 이해하고 싶었습니다.

스프링 프레임워크의 도움을 받는 게 아닌, 직접 자바단에서 개발을 해서 Request를 날려보고,

스프링이 해주고 있던 일들을 조금 더 자세하게 알고 싶었습니다.

위와 같은 내용을 공부하기 위해 [자바 웹 프로그래밍 Next Step]은 정말 좋은 책이라는 걸 느꼈습니다.

아직 3장까지 읽어보지는 않았지만 다양한 과제들이 있고 힌트들이 있습니다.

 

 

 

과제 구현한 깃허브 레포지토리 => 링크

 

 

 

1️⃣ 요구사항 1단계

 

과제: index.html 응답하기

  • http://localhost:8080/index.html로 접속했을 때 webapp 디렉토리의 index.html 파일을 읽어 클라이언트에 응답한다.

 

1차로 구현한 소스코드

// ...생략...
public class RequestHandler extends Thread {

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

        try (InputStream in = connection.getInputStream(); OutputStream out = connection.getOutputStream()) {

            InputStream inputStream = new FileInputStream("webapp/index.html");

            BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));

            StringBuffer sb = new StringBuffer();

            String tempStr;
            while ( (tempStr = br.readLine()) != null ) {
                sb.append(tempStr + "\r\n");
            }

            DataOutputStream dos = new DataOutputStream(out);
            response200Header(dos, sb.toString().getBytes().length);
            responseBody(dos, sb.toString().getBytes());
        } catch (IOException e) {
            log.error(e.getMessage());
        }
    }

// ...생략...
}

 

index.html은 아래의 캡처사진처럼 출력이 완료되었습니다.
하지만 index.html로 접속 했을 때 해당 index.html이 출력되도록은 아직 구현 안 했습니다.

 

 

 

처음에 CSS가 적용이 안돼서 뭔가 문제가 있는 줄 알았습니다.

하지만 크롬 개발자 도구함을 열어보니까 css파일(styles.css)은 정상적으로 불러와지고 있었습니다(아래 사진).

 

 

 

 

최종적으로 구현한 소스코드

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

        try (InputStream in = connection.getInputStream(); OutputStream out = connection.getOutputStream()) {

            InputStreamReader inputStreamReader = new InputStreamReader(in);
            BufferedReader bufferedReader = new BufferedReader(inputStreamReader);

            String line = bufferedReader.readLine();

            if (line == null) { return; }


            String[] tokens = line.split(" ");

            String url = tokens[1];

            log.debug("connect URL : {}", url );

            byte[] bytes = Files.readAllBytes(new File("./webapp" + url).toPath());

            DataOutputStream dos = new DataOutputStream(out);
            response200Header(dos, bytes.length);
            responseBody(dos, bytes);

        } catch (IOException e) {
            log.error(e.getMessage());
        }
    }

 

  • 비록 힌트를 보고 했지만, localhost:8080/index.html 접속할 때 해당 파일이 불러와지도록 구현했습니다.
  • InputStreamReader를 통해 소켓의 InputStream을 읽었습니다.
  • InputStreamReader로 읽은 것을 BufferedReader 클래스 객체 생성 시 생성자에 집어넣습니다.
  • bufferedReader.readLine()을 통해 HTTP 요청 첫 번째 라인을 읽어 String 자료형 line 변수에 할당한다.
    => 첫번째 라인에 HTTP method와 요청 url이 담겨있다.
    ex) GET /index.html HTTP/1.1
  • line 변수를 split 함수를 사용해서 공백을 기준으로 나눠 String 배열인 tokens 변수에 할당합니다.
  • tokens 변수의 [1] 번째 인덱스에 있는 요청 URL만 String 자료형 url 변수에 할당합니다.
  • 요청 URL를 한번 로그로 띄어봅니다. => 클라이언트에서 요청한 URL 모니터링용
  • Files와 File 클래스를 사용해서 ./webapp 경로에 있는 파일들을 byte[] 형식으로 전부 읽어옵니다.
  • 읽어온 bytes 값을 DataOutputStream 클래스를 사용해 write 합니다.

 

 

정리

클라이언트로부터 받은 HTTP 리퀘스트 메시지를 InputStream에 받고 InputStreamReader로 읽습니다.

BufferedReader를 사용해 HTTP 리퀘스트 메시지를 한줄씩 읽습니다.

받은 HTTP 리퀘스트 메시지 내용에 따라 적절한 처리를 실행하여 Outputstream과 DataOutputStream을 사용해서
응답 메시지를 만들고, wrtie를 통해 이것을 클라이언트에 반송합니다.

 

 

 

구현하면서 느낀 점

  • 소켓을 연결하고 클라이언트와 서버가 HTTP Request, Response를 주고받는 과정을 알게 되었습니다.
  • 다양한 입출력 클래스를 알게 되었고 아직 입출력에 대해 모르는 게 많다는 걸 느꼈습니다.
  • 힌트를 봤지만 제대로 이해하지 못했습니다.
    => 힌트를 읽고서도 그걸 코드로 구현하지 못함
    => 계속 고민하면서 구현하다가 신기하게도 조금 쉬고 다시 코드를 보니까 쉽게 해결이 되었습니다.

 

 

2️⃣ 요구사항 2단계

GET 방식으로 회원가입하기

  • "회원가입" 메뉴를 클릭하면 http://localhost:8080/user/form.html으로 이동하면서 회원가입할 수 있다.
  • 회원가입을 하면 다음과 같은 형태로 사용자가 입력한 값이 서버에 전달된다.
    /user/create?userId=javajigi&password=password&name=Gaemi&email=gaemi@daum.net
  • HTML과 URL을 비교해 보고 사용자가 입력한 값을 파싱(문자열을 원하는 형태로 분리하거나 조작하는 것을 의미)해
    model.user 클래스에 저장한다.

 

 

구현한 소스코드

 

RequestHandler 클래스 run() 메소드

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

        try (InputStream in = connection.getInputStream(); OutputStream out = connection.getOutputStream()) {

            InputStreamReader inputStreamReader = new InputStreamReader(in, "UTF-8");
            BufferedReader bufferedReader = new BufferedReader(inputStreamReader);

            String line = bufferedReader.readLine();

            if (line == null) { return; }

            String[] tokens = line.split(" ");

            String uri = tokens[1];

            if (userSignupCheck(uri)) return;

            log.debug("connect URL : {}", uri);

            Path path = Paths.get("./webapp", uri);

            if (! Files.exists(path)) {
                log.debug("Path : {} not exists", path);
                return;
            }

            File file = path.toFile();
            path.isAbsolute();

            String fileExtension = FilenameUtils.getExtension(file.getName());
            byte[] bytes = Files.readAllBytes(file.toPath());

            DataOutputStream dos = new DataOutputStream(out);
            response200Header(dos, bytes.length, fileExtension);
            responseBody(dos, bytes);
        } catch (IOException e) {
            log.error(e.getMessage());
        }
    }
  • if (userSignupCheck(uri)) return;
    userSignupCheck() 메소드로 사용자가 입력한 값을 파싱 및 저장합니다.

 

 

RequestHandler 클래스 userSignupCheck() 메소드

 private static boolean userSignupCheck(String uri) {
        if (uri.contains("/create")) {

            String userInfoQueryString = uri.split("\\?")[1];

            UserService userService = new UserService();
            userService.signupUser(userInfoQueryString);

            return true;
        }
        return false;
    }
  • /user/create?userId=gaemi&password=1234&name=Gaemi&email=gaemi@daum.net
    사용자가 입력한 값은 위와 같이 서버에 전달된다.
  • uri에 /create가 포함되어있는지 확인한다.
    => 포함되어 있으면 split() 메소드를 사용해서 "?" 를 기준으로 나눕니다.
    => 나눈 값에서 유저 정보가 담긴 queryString 값만 String 자료형인 userInfoQueryString 변수에 저장합니다.
  • 저장된 값을 UserService 클래스의 signupUser() 메소드를 실행시킵니다.

 

 

 

UserService클래스

public class UserService {

    public boolean signupUser(String userInfoqueryString) {

        Map<String, String> userInfo = HttpRequestUtils.parseQueryString(userInfoqueryString);

        String userId = userInfo.getOrDefault("userId", "");
        String password = userInfo.getOrDefault("password", "");
        String name = userInfo.getOrDefault("name", "");
        String email = userInfo.getOrDefault("email", "");

        User user = new User(userId, password, name, email);

        DataBase.addUser(user);

        return true;
    }
}
  • RequestHandler 클래스에 회원가입 로직을 추가하는 것보다는 분리하는 게 좋겠다고 생각해서
    UserService를 만들어서 분리를 했습니다.
  • 저자인 자바지기님이 미리 만들어놓으신 HttpRequestUtils 클래스의 parseQueryString() 메소드를 사용해서,
    쿼리 스트링의 값을 분리 "&" 기준으로 분리를 해서 Map<String, String>을 리턴 받습니다.
  • 리턴 받은 값을 userInfo 객체에 할당하고 유저의 정보를 각 String 변수에 할당합니다.
    => 여기서 유저가 값을 입력하지 않으면 빈 문자열을 집어넣도록 했습니다.
    => 참고로 유저 정보 중에 빈 값 관련 처리를 아직 안 했습니다.
  • 입력받은 유저 정보를 바탕으로 User 객체를 생성합니다.
  • 저자인 자바지기님이 만들어놓은 DataBase 클래스 addUser() 메소드를 사용해서 저장합니다.

 

 

구현하면서 느낀 점

  •  예외 처리를 제대로 안 해놔서 조금 아쉬웠습니다.
    => 책에서 다음 과제가 GET 방식이 아닌 POST 방식으로 값을 받아온다. 그때는 더 견고하게 코드를 짤 예정입니다.
  • 과제를 진행하면 할수록 코드가 조금 더러워지고 있습니다.
    => 리팩터링은 틈틈이 계속 진행해야겠다고 생각했습니다.
  • 다른 사람들이 제출한 코드를 직접 보고 나의 코드를 보완해봐야겠다고 생각했습니다.

 

3️⃣ 4️⃣ 요구사항 3, 4

POST 방식으로 회원가입하기

  • http://localhost:8080/user/form.html 파일의 form 태그 method를 get에서 post로 수정한 후 
    회원가입이 정상적으로 동작하도록 구현한다.

 

더보기

구현한 소스코드

RequestHandler 클래스의 run() 메소드

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

        try (InputStream in = connection.getInputStream(); OutputStream out = connection.getOutputStream()) {

            InputStreamReader inputStreamReader = new InputStreamReader(in, "UTF-8");
            BufferedReader br = new BufferedReader(inputStreamReader);

            ArrayList<String> httpHeaderList = new ArrayList<>();

            String line;
            while ( !(line = br.readLine()).equals("") ) {
                httpHeaderList.add(line);
            }

            String firstHttpHeader = httpHeaderList.get(0);

            if (firstHttpHeader == null) { return; }

            String[] tokens = firstHttpHeader.split(" ");

            String httpMethod = tokens[0];
            uri = tokens[1];
            statusCode = "200";
            description = "OK";


            if (httpMethod.equals("POST")) {

                int contentLength = Integer.parseInt(httpHeaderList.get(3).split(" ")[1]);
                String userInfoQueryString = IOUtils.readData(br, contentLength);

                uri = userSignupCheck(userInfoQueryString);
                statusCode = "302";
                description = "Redirection";

            } else if (httpMethod.equals("GET")) {

                if (uri.contains("/create")) {
                    String userInfoQueryString = uri.split("\\?")[1];
                    uri = userSignupCheck(userInfoQueryString);
                }
            }



            log.debug("connect URL : {}", uri);

            Path path = Paths.get("./webapp", uri);

            // ToDo : 해당 파일이 없으면 에러 페이지로 이동되도록 구현 필요
            if (! Files.exists(path)) {
                log.debug("Path : {} not exists", path);
                return;
            }

            File file = path.toFile();
            path.isAbsolute();

            String fileExtension = FilenameUtils.getExtension(file.getName());
            byte[] bytes = Files.readAllBytes(file.toPath());

            DataOutputStream dos = new DataOutputStream(out);
            response200Header(dos, bytes.length, fileExtension);
            responseBody(dos, bytes);
        } catch (IOException e) {
            log.error(e.getMessage());
        }
    }

 

  • 클라이언트로부터 받은 HttpHeader의 값을 각 라인별로 ArrayList<String>에 저장했습니다.
  • ArrayList에서 0번째 인덱스의 값을 " " 을 기준으로 나눠 int 배열인 tokens[] 에 할당한다.
  • Http Header 첫 줄에서 uri와 HttpMethod 문자열을 저장합니다.
  • Response로 보낼 statuscode와 description에 값을 넣습니다.
  • 여기서 HttpMethod의 문자열이 POST인지 GET인지 확인하는 조건문을 넣어놨다.
    => POST이면 HttpHeader에 있는 ContentLength 값을 찾아서 int 변수에 할당한다.
    * Body의 값을 어떻게 가져올까 계속 고민했었는데 저자인 자바지기님이 구현한 함수를 사용하면 됐었다.
    IOUtils클래스의 readdata() 메소드를 사용해서 body의 값을 파싱한다.
  • 받아온 Body의 값을 userSignupCheck() 함수에 인자로 넣고 userService클래스의 signupUser() 메소드로 DB에 저장
  • 완료가 되면 redirect url인 index.html의 값을 받아와 uri 변수에 할당한다.
  • Response를 보낼 때 Http Header에 담을 status code와 description 값을 할당한다.
    => 여기서 redirect를 하기 위해서는 status code에 302를 넣어줘야 한다. => 위키
    => Description에 Redirection 문자열을 넣어준다.

 

 

RequestHandler 클래스의 response200Header() 메소드

    private void response200Header(DataOutputStream dos, int lengthOfBodyContent, String fileExtension) {
        try {
            dos.writeBytes("HTTP/1.1 " + statusCode + " " + description + "\r\n");
            dos.writeBytes("Content-Type: text/" + fileExtension + ";charset=utf-8\r\n");
            dos.writeBytes("Content-Length: " + lengthOfBodyContent + "\r\n");

            if (statusCode.equals("302")) {
                dos.writeBytes("Location: " + uri);
            }
            dos.writeBytes("\r\n");
        } catch (IOException e) {
            log.error(e.getMessage());
        }
    }
  • 위와 같이 statusCode가 302이면 Location 값을 추가했다.
    ex) redirect 할 uri가 추가된다 => index.html

 

 

RequestHandler 클래스의 userSignupCheck() 메소드

    private static String userSignupCheck(String userInfoQueryString) {
        UserService userService = new UserService();
        return userService.signupUser(userInfoQueryString);
    }
  • GET과 POST 메소드에도 사용하기 위해 userSignupCheck() 함수의 라인수가 줄어들었다.

 

 

구현하면서 느낀 점

  • 리팩터링을 제대로 안 하고 구현만하고 있어서 코드가 점점 더러워지고 있다.
    => 다음 과제를 하기 전에 리팩터링을 하는 시간을 한번 가지자.
  • HTTP 책을 제대로 읽었다고 생각했지만 모르는 게 너무 많았다.
    => HTTP Header에 들어있는 값이 무엇이고 어떤 의미를 가지는지 알 수 있도록 책을 한번 더 읽어보자.
  • response200Header에 302를 처리하는 로직이 들어가 있다.
    => 분리를 꼭 하자.
    => 함수를 분리를 제대로 하지 않아서 중복되는 코드가 꽤 많아졌다. 리팩터링을 꼭 하자.

 

 

리팩터링한 소스코드

*저자인 자바지기님이 작성한 코드를 참고해서 코드가 거의 비슷합니다.

 

ReuqestHandler 클래스의 run() 메소드

// ... 생략 ...
InputStreamReader inputStreamReader = new InputStreamReader(in, "UTF-8");
BufferedReader br = new BufferedReader(inputStreamReader);

String requestLine = br.readLine();

if (requestLine == null) return;

log.debug("request line : {}", requestLine);

String[] tokens = requestLine.split(" ");
String uri = getDefaultUrl(tokens[1]);

int contentLength = 0;
boolean logined = false;
while (!requestLine.equals("")) {
    requestLine = br.readLine();
    log.debug("header : {} ", requestLine);

    if (requestLine.contains("Content-Length")) {
        contentLength = getContentLength(requestLine);
    }

    if (requestLine.contains("Cookie")) {
        logined = isLogin(requestLine);
    }
}

if ("/user/create".equals(uri)) {
    String body = IOUtils.readData(br, contentLength);
    Map<String, String> userInfo = HttpRequestUtils.parseQueryString(body);
    User user = new User(userInfo.get("userId"), userInfo.get("password"),
            userInfo.get("name"), userInfo.get("email"));
    DataBase.addUser(user);

    DataOutputStream dos = new DataOutputStream(out);
    response302Header(dos);
    
 // ... 생략 ...
  • 클라이언트의 Request에 첫 줄만 BufferReader로 읽어온다.
    => 여기서 첫 줄이 null이면 run() 메서드를 종료한다.
  • Http Request 첫 라인에서 uri만 String 변수에 할당합니다.
  • while문을 사용해서 BufferdReader에 있는 request가 빈 문자열이 나올 때까지 읽습니다.
    => "Content-Length"가 포함된 문자열은 getContentLength() 메서드를 사용해 값을 int 자료형에 할당합니다.
    => "Cookie"가 포함된 문자열은 로그인 여부를 확인하는 isLogin() 메서드를 사용해서 로그인 여부를 확인합니다.
  • uri가 "/user/create"와 같은지 확인합니다.
    => 같으면 자바지기 저자가 만들어놓은 IOUtils.readData() 메서를 사용해서 body의 값을 읽어 String 변수에 할당
    => 자바지기 저자가 만들어놓은 HttpRequestUtils.parseQueryString()을 사용해서 body값에서 request에 담긴 유저정보를 Map<String, Strin> 자료형에 할당합니다.
    => userInfo에 있는 데이터를 가지고 User 객체를 만들고 저자가 만들어놓은 임시 DataBase에 추가합니다.(인메모리)
  • DataOutputStream의 객체를 만들어서 response302Header() 메서드의 인자에 넣습니다.

 

 

RequestHandler 클래스의 getContentLength() 메소드

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

 

  • Content-Length 개체가 담긴 문자열을 ":" 기준으로 split 한다.
    ex) Content-Length: <length>
  • split해서 할당된 문자열의  1번째 인덱스에 있는 값을 trim 해주고 int형으로 파싱해서 리턴한다.

 

 

 

RequestHandler 클래스의 response302Header() 메소드

private void response302Header(DataOutputStream dos) {
    try {
        dos.writeBytes("HTTP/1.1 302 Redirect \r\n");
        dos.writeBytes("Location: /index.html \r\n");
        dos.writeBytes("\r\n");
    } catch (IOException e) {
        log.error(e.getMessage());
    }
}
  • Redirect를 하기 위한 헤더를 세팅한다.
  • Location에 Redirect를 할 uri를 추가한다.

 

 

 

리팩터링하면서 느낀점

  • 다른 사람이 구현한 코드를 보고 분석하는것도 엄청 중요하다는걸 한번 더 느꼈습니다.
  • 저자가 최대한 깔끔하게 작성한 코드이지만, 구조적으로 문제가 있다는 것을 느끼게 되었습니다.
    => run 메소드가 점점 방대해 질 수 밖에 없는 구조
    => 조건문이 계속 늘어나고 나중에 리팩터링 하거나 기능을 수정하기 너무 힘들어진다.
    => 하드코딩하는 부분이 늘어날 수 밖에 없다.
  • 남이 구현한 코드를 갔다가 사용했어도 해당 코드를 직접 뜯어보고 글로 작성해보자.

 

 

 

 

 

5️⃣ 요구사항 5단계

로그인하기

  • "로그인" 메뉴를 클릭하면 http://localhost:8080/user/login.html으로 이동해 로그인할 수 있다.
  • 로그인이 성공하면 /index.html로 이동하고, 로그인이 실패하면 /user/login_failed.html로 이동해야 한다.
  • 앞에서 회원가입한 사용자로 로그인할 수 있어야 한다.
  • 로그인이 성공 하면 쿠키를 활용해 로그인 상태를 유지할 수 있어야 한다.
  • 로그인이 성공할 경우 요청 헤더의 Cookie 헤더 값이 logined=true,
    로그인이 실패하면 Cookie 헤더 값이 logined=false로 전달되어야 한다.

 

 

구현한 소스코드

Requesthandler 클래스의 httpMethodCheck() 메소드

    private void httpMethodCheck(
            String httpMethod,
            ArrayList<String> httpHeaderList,
            BufferedReader br
    ) throws IOException {

        if (httpMethod.equals("POST")) {

            int contentLength = Integer.parseInt(httpHeaderList.get(3).split(" ")[1]);
            String userInfoQueryString = IOUtils.readData(br, contentLength);

            if (uri.contains("/user/login")) {

                if (userService.userLogin(userInfoQueryString)) {
                    uri = "/index.html";
                    cookie = "Set-cookie: logined=true";
                } else {
                    uri = "/user/login_failed.html";
                    cookie = "Set-cookie: logined=false";
                }
            } else {
                uri = userService.signupUser(userInfoQueryString);
            }
            statusCode = "302";
            description = "Redirection";

        } else if (httpMethod.equals("GET")) {
            statusCode = "200";
            description = "OK";

            if (uri.contains("/create")) {
                String userInfoQueryString = uri.split("\\?")[1];
                uri = userService.signupUser(userInfoQueryString);
            }
        }
    }

 

  • 클라이언트에서 request를 POST method로 서버로 보낼 때 /user/login URI이면 로그인 로직을 처리하도록 했습니다.
  • "Set-cookie: key=value" 문자열을 클라이언트에게 Response를 보낼 때 헤더에 담아서 보냅니다.
  • 로그인이 정상적으로 작동하면 index.html 페이지로 redirect 되도록 했습니다.
  • 로그인이 정상적으로 작동되지 않으면 /user/login_failed.html로 redirect 되도록 했습니다.

 

 

UserService 클래스의 userLogin() 메소드

    public boolean userLogin(String userInfoQueryString) {

        Map<String, String> userInfo = HttpRequestUtils.parseQueryString(userInfoQueryString);

        String userId = userInfo.getOrDefault("userId", "");

        User user = DataBase.findUserById(userId);

        if (user == null) {
            return false;
        }

        return true;
    }
  • 로그인을 처리하는 로직을 Service 레이어에 따로 분리해놨습니다.
    => 분리를 해서 RequestHandler의 코드가 좀 줄어들기는 했는데 여러가지 에로사항이 있었습니다.
    * 구현하면서 느낀점에 에로사항 정리

 

 

구현하면서 느낀 점

  • 스프링에서는 Service, Controller 분리가 어렵지 않았었습니다.
    이렇게 분리 처리를 하는 것에서 스프링 프레임워크에서 많은 것들을 대신 해준다는 것을 알게되었습니다.
  • 스프링과 같은 아키텍쳐로 분리를 해보려고 하고 있습니다.
    => 지금은 RequestHandler의 코드가 너무 길어지고 복잡해지고 있습니다.
    => 아직은 코드가 많이 더러운데 하나씩 리팩토링을 하면서 바꿔갈 예정입니다.
  • 조건문(if)을 너무 많이 사용하게 되고 가독성을 떨어트리는 것 같습니다.
    => 책에도 적혀있는 내용처럼 else()를 사용하지 않고 구현하는 방식으로 진행해볼 예정입니다.
  • 아직 많이 부족한게 많지만 그래도 구현이 되어서 신기했습니다.
    그리고 기존에는 이해가 안되었던 MVC의 아키텍쳐에 대해서 점점 알아가게 되었습니다.

 

 

 

6️⃣ 요구사항 7단계

사용자 목록 출력

  • 접근하고 있는 사용자가 "로그인" 상태일 경우(Cookie 값이 logined=true) http://localhost:8080/user/list로 접근 했을 때
    사용자 목록을 출력한다.
    => 만약 로그인 하지 않은 상태라면 로그인 패이지(login.html)로 이동한다.

 

 

구현한 소스코드

RequestHandler 클래스 httpMethodCheck 메소드

    private void httpMethodCheck(
            String httpMethod,
            Map<String, String> httpHeaderMap,
            BufferedReader br
    ) throws IOException {
    
    // ... 생략 ...
    
        } else if (httpMethod.equals("GET")) {
            statusCode = "200";
            description = "OK";

            if (uri.contains("/create")) {
                String userInfoQueryString = uri.split("\\?")[1];
                uri = userService.signupUser(userInfoQueryString);
            } else if (uri.contains("/list.html")) {
                Map<String, String> cookies= HttpRequestUtils.parseCookies(httpHeaderMap.get("Cookie"));
                boolean isLogin = Boolean.parseBoolean(cookies.get("logined"));

                if (!isLogin) {
                    statusCode = "302";
                    description = "Redirection";
                    uri = "/index.html";
                }

            }
        }
    }
  • httpHeader에서 Cookie의 값을 저자인 자바지기님이 만든 HttpRequestUtils 클래스의 parseCookies 메소드를 사용해 파싱한다.
  • 파싱한 값은 Map<String, String> 으로 리턴 받는다.
  • 리턴 받은 값에서 key "logined"의 value를 가져와 boolean 자료형으로 파싱한다.
  • 파싱한 boolean 값으로 로그인 여부를 확인한다.
  • 로그인이 되어있지 않으면 /index.html 로 리다이렉트 시킨다.

 

 

구현하면서 느낀 점

  • 구현을 하는데 시간이 꽤 적게 소요되었습니다.
  • 요구사항이 그렇게 어렵지는 않아서 쉽게 끝낼 수 있었습니다.
    => 저자인 자바지기님이 cookie 값을 파싱하기 쉽도록 미리 구현해 놓은게 있어서 더욱 쉬웠습니다.
  • httpHeader의 값을 Map 자료형으로 파싱하는 방식으로 리팩터링을 진행했습니다.
    => 아직 RequestHandler의 코드가 너무 복잡하고 결합도가 너무 높습니다.
    => 과제 완료 후 리팩터링을 우선으로 진행 예정

 

 

 

7️⃣ 요구사항 7단계

CSS 지원하기

  • 지금까지 구현한 소스코드는 CSS 파일을 지원하지 못하고 있다.
    CSS 파일을 지원하도록 구현한다.

 

 

구현한 소스코드

 

RequestHandler 클래스 Run() 메서드

public void run() {
    // ... 생략 ...
        } else if (uri.endsWith(".css")) {
            responseCssResource(out, uri);
        } else {
            responseResource(out, uri);
        }
    } catch (IOException e) {
        log.error(e.getMessage());
    }
}

    // ... 생략 ...
  • 요청 uri의 확장자가 .css 이면 responseCssResource() 함수가 실행된다.

 

ReqiestHandler 클래스 responseCssResource() 메서드

private void responseCssResource(OutputStream out, String url) throws IOException {
    DataOutputStream dos = new DataOutputStream(out);
    byte[] body = Files.readAllBytes(new File("./webapp" + url).toPath());
    response200CssHeader(dos, body.length);
    responseBody(dos, body);
}
  • webapp 폴더에 있는 css 파일의 바이트 배열로 읽어서 헤더와 바디에 넣어주고 클라이언트에게 응답을 보낸다.

 

 

RequestHandler클래스 response200CssHeader() 메서드

private void response200CssHeader(DataOutputStream dos, int lengthOfBodyContent) {
    try {
        dos.writeBytes("HTTP/1.1 200 OK \r\n");
        dos.writeBytes("Content-Type: text/css;charset=utf-8 \r\n");
        dos.writeBytes("Content-Length: " + lengthOfBodyContent + "\r\n");
        dos.writeBytes("\r\n");
    } catch (IOException e) {
        log.error(e.getMessage());
    }
}
  • 위와 같은 양식으로 header 내용을 write한다.

 

 

 

구현하면서 느낀 점

  • 생각보다 빨리 구현해서 놀랐습니다.
  • 사전에 네트워크 관련 공부를 한게 크게 도움이 되었습니다.
    => 책 [그림으로 배우는 HTTP & Network] => yes24링크
  • 2단계를 먼저 진행해야 하는데 CSS를 꼭 적용해보고 싶어서 7단계로 훌쩍 넘어와버렸습니다.
    앞에 구현하는 것과 크게 영향이 없어보여서 다행이였습니다.

 

 

 

8️⃣ 과제를 하면서 궁금한 것들

 

 

❓ Content-Length를 넣는 이유는

  • Content-Length는 수신자에게 보내지는, 바이트 단위를 가지는 개체 본문의 크기를 나타냅니다.

 

❓ DataOutputStream이란

 

 

반응형

+ Recent posts