Skip to content

[Han, Jay] 웹서버 1단계 구현 #48

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 29 commits into from
Mar 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
f4923c4
[1단계] Request Header 출력
beginin15 Mar 16, 2020
e6ad562
[1단계] path 분리
beginin15 Mar 16, 2020
97f55a9
[1단계] Request에 따른 응답하기
beginin15 Mar 16, 2020
bffb09e
[2단계] refactor Utils
beginin15 Mar 17, 2020
d7d51d4
[2단계] User 객체 생성
beginin15 Mar 17, 2020
c70def8
[2단계] refactor requestHandler
beginin15 Mar 17, 2020
de6b63d
[3단계] 회원가입 전송 method 수정
beginin15 Mar 17, 2020
e7176bc
[3단계] Post 방식 회원가입
beginin15 Mar 17, 2020
1f2ab3e
[4단계] 회원가입 후 redirect
beginin15 Mar 17, 2020
8e70aac
[4단계] refactor decode method 생성
beginin15 Mar 17, 2020
bbf6d71
[4단계] refactor RequestHandler
beginin15 Mar 17, 2020
8bae0f8
[5단계] 회원가입 , 로그인 기능
beginin15 Mar 18, 2020
24956dc
[5단계] 로그인 기능 + redirect
beginin15 Mar 18, 2020
d9db934
[5단계] Set-Cookie 구현
beginin15 Mar 18, 2020
5c412a1
[6단계] user/list 요청
beginin15 Mar 18, 2020
2637a42
[6단계] 유저리스트를 동적으로 보여주기
beginin15 Mar 18, 2020
63832f2
[7단계] stylesheet file 요청에 대한 응답
beginin15 Mar 18, 2020
30dbdf3
[7단계] refactor RequestHandler
beginin15 Mar 19, 2020
52f53b7
[7단계] refactor RequestHandler
beginin15 Mar 19, 2020
53977ff
[7단계] refactor RequestHandler
beginin15 Mar 19, 2020
c3d7eb9
[7단계] refactor RequestHandler
beginin15 Mar 19, 2020
ae01631
[7단계] refactor RequestHandler
beginin15 Mar 19, 2020
d68a164
Merge pull request #16 from beginin15/step7
beginin15 Mar 19, 2020
39e1b94
[deploy] 배포 컴파일 에러 수정
beginin15 Mar 19, 2020
0600e24
[deploy] README 업데이트
beginin15 Mar 20, 2020
113fa7a
[Refactor 1단계] 메서드 , 클래스 분리
beginin15 Mar 20, 2020
885c742
[Refactor 2단계] HttpResponse 분리
beginin15 Mar 20, 2020
9c8d051
[Refactor 2단계] HttpResponse , utils
beginin15 Mar 20, 2020
5741c8b
Merge pull request #21 from beginin15/refactor
beginin15 Mar 20, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 4 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,4 @@
# 웹 애플리케이션 서버
## 진행 방법
* 웹 애플리케이션 서버 요구사항을 파악한다.
* 요구사항에 대한 구현을 완료한 후 자신의 github 아이디에 해당하는 브랜치에 Pull Request(이하 PR)를 통해 코드 리뷰 요청을 한다.
* 다음 단계를 도전하고 앞의 과정을 반복한다.
* 코드 리뷰가 완료되면 피드백에 대한 개선 작업을 한다.
* 다음 단계 PR 보낼 때 이전 단계 피드백이 잘 반영되었는지 점검한다.

## 온라인 코드 리뷰 과정
* [텍스트와 이미지로 살펴보는 코드스쿼드의 온라인 코드 리뷰 과정](https://github.com/code-squad/codesquad-docs/blob/master/codereview/README.md)
* [동영상으로 살펴보는 코드스쿼드의 온라인 코드 리뷰 과정](https://youtu.be/a5c9ku-_fok)
# 웹서버 구현
### [Ground Rule](https://github.com/beginin15/java-was/wiki/Ground-Rule)
### [Han's README](https://github.com/beginin15/java-was/wiki/%5BHan%5D-README)
### [Jay's README](https://github.com/beginin15/java-was/wiki/%5BJay%5D-README)
10 changes: 8 additions & 2 deletions src/main/java/db/DataBase.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import java.util.Collection;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Optional;

import com.google.common.collect.Maps;

Expand All @@ -10,12 +12,16 @@
public class DataBase {
private static Map<String, User> users = Maps.newHashMap();

public static int getSizeOfUsers() {
return users.size();
}

public static void addUser(User user) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

User 파라미터가 null인 경우에 대한 처리가 없네요
호출하는 쪽에서 null을 거르면 호출하는 곳마다 null을 체크해야하지만 해당 메소드 내부에서 안전하게 처리를 해둔다면 여러번 처리를 하지 않겠죠?

users.put(user.getUserId(), user);
}

public static User findUserById(String userId) {
return users.get(userId);
public static User findUserById(String userId) throws NoSuchElementException{
return Optional.ofNullable(users.get(userId)).orElseThrow(NoSuchElementException::new);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 호출 하는 쪽에서 Optional로 받고 예외를 발생시킬 지 말지를 정하도록 하는 것이 좋다고 생각하는 편 입니다.
위와 같이 기능 메소드에서 예외를 발생시킬 경우 예외를 발생시키지 않아도 되는 상황에 일률적으로 발생시키는 것이 되어버리며 정상적인 진행이 되도록 하기 위해 try ~ catch 처리를 해야하기 때문입니다.

}

public static Collection<User> findAll() {
Expand Down
23 changes: 23 additions & 0 deletions src/main/java/db/SessionDataBase.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package db;

import com.google.common.collect.Maps;
import model.User;

import java.util.Collection;
import java.util.Map;

public class SessionDataBase {
private static Map<String, User> sessions = Maps.newHashMap();

public static void addSession(String sessionId, User user) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

위 UserDataBase와 동일한 피드백을 드리고 싶군요

sessions.put(sessionId, user);
}

public static User getSessionedUser(String sessionId) {
return sessions.get(sessionId);
}

public static boolean isSessionIdExist(String sessionId) {
return sessions.containsKey(sessionId);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

위 메소드들과 마찬가지로 예외처리를 보강해주셔야합니다.

}
}
6 changes: 6 additions & 0 deletions src/main/java/http/HttpMethod.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package http;

public enum HttpMethod {
GET,
POST;
}
64 changes: 64 additions & 0 deletions src/main/java/http/HttpRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package http;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import util.HttpRequestUtils;
import util.IOUtils;

import java.io.BufferedReader;
import java.io.IOException;
import java.util.Map;

public class HttpRequest {

private static final Logger log = LoggerFactory.getLogger(HttpRequest.class);

private HttpMethod httpMethod;
private String path;
private Map<String, String> header;
private Map<String, String> parameter;

public HttpRequest(BufferedReader br) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HttpRequest는 입력을 파싱하여 각각을 필드로 갖고 있는 클래스로 보이는데, BufferedReader를 생성자로 꼭 받을 필요가 있을까요?
요청 데이터가 표준입력이 아닌 파일입력 또는 그외의 방식으로 변경될 경우 HttpRequest 클래스에도 그 영향이 가지 않을까요?
여기에 대한 고민 한번 해보시고 리팩토링 권해드립니다.
저는 HttpRequest가 입력값만 받아도 크게 이상 없다고 피드백 드리겠습니다.

parseRequest(br);
}

public HttpMethod getMethod() {
return httpMethod;
}

public String getPath() {
return path;
}

public String getHeader(String key) {
return header.get(key);
}

public String getParameter(String key) {
return parameter.get(key);
}

private void parseRequest(BufferedReader br) {
String requestLine;

try {
requestLine = br.readLine();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 라인 또한 사용자의 요청 입력 방식 변경이 영향 미칠 라인입니다.

httpMethod = HttpMethod.valueOf(HttpRequestUtils.getMethod(requestLine));
path = HttpRequestUtils.getURL(requestLine);
changePathIfRoot();
header = HttpRequestUtils.extractHeader(br);
if (httpMethod.equals(HttpMethod.POST)) {
String body = IOUtils.readData(br, Integer.parseInt(getHeader("Content-Length")));
parameter = HttpRequestUtils.parseQueryString(body);
}
} catch (IOException e) {
log.error("HttpRequest parse 과정 에러");
}
}

private void changePathIfRoot() {
if (this.path.equals("/")) {
path = "/index.html";
}
}
}
118 changes: 118 additions & 0 deletions src/main/java/http/HttpResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package http;


import model.User;
import util.HttpResponseUtils;

import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class HttpResponse {
Copy link

@imjinbro imjinbro Mar 22, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여러 리스폰스 코드(200, 302 등)에 맞춰 메소드를 만든 것으로 보입니다.
그리고 해당 메소드에 DataOutputStream 메소드가 있군요
HttpResponse는 말그대로 응답에 대한 필드만 가지고 있으면 될 것으로 보입니다. 2개의 역할을 모두 하고 있는 것으로 보여요
그리고 HttpResponse는 몇가지 정해져 있으니 각 응답코드에 맞춘 객체를 미리 생성해두고 재활용하면 어떨까요?
메소드로 풀어내는 것을 유지한다면 응답코드 대응할 때마다 메소드가 여러개 늘어나고, 이전에 만들었는지 체크하기도 힘들 것입니다.


private Map<String, String> header;
private DataOutputStream dos;

public HttpResponse() {
header = new HashMap<>();
}

public HttpResponse(DataOutputStream dos) {
header = new HashMap<>();
this.dos = dos;
}

// public void addHeader(String key, String value) {
// header.put(key, value);
// }

public void forward(String path) throws IOException {
byte[] body;
if (HttpResponseUtils.isFileExist(path)) {
body = HttpResponseUtils.readFile(path);
response200Header(body.length);
responseBody(body);
return;
}
body = HttpResponseUtils.notExistPage();
response404Header(body.length);
responseBody(body);
}

public void forwardBody(String responseBody) {

}

public void response200Header(int lengthOfBodyContent) throws IOException {
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");
}

public void sendRedirect(String location) throws IOException {
dos.writeBytes("HTTP/1.1 302 Found \r\n");
dos.writeBytes("Location: " + location + "\r\n");
dos.writeBytes("\r\n");
}

public void sendRedirect(String location, String sessionId) throws IOException {
dos.writeBytes("HTTP/1.1 302 Found \r\n");
dos.writeBytes("Location: " + location + "\r\n");
dos.writeBytes("Set-Cookie: JSESSIONID=" + sessionId + "; Path=/" + "\r\n");
dos.writeBytes("\r\n");
}

public void readUserList(List<User> users) throws IOException {
byte[] body = HttpResponseUtils.readFile("/user/list.html");
String userListHtml = HttpResponseUtils.getUserListHTML(body, users);
response200Header(userListHtml.length());
responseBody(userListHtml.getBytes());
}

public void readCss(String path) throws IOException {
byte[] body = Files.readAllBytes(new File("./webapp" + path).toPath());
responseCssHeader(body.length);
responseBody(body);
}

public String processHeaders() {
StringBuilder sb = new StringBuilder();
header.entrySet().stream()
.map(entry -> entry.getKey() + ": " + entry.getValue())
.forEach(header -> sb.append(header).append("\r\n"));

sb.append("\r\n");
return sb.toString();
}

private void responseCssHeader(int lengthOfBodyContent) throws IOException {
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");
}

private void response401Header(int lengthOfBodyContent) throws IOException {
dos.writeBytes("HTTP/1.1 401 Unauthorized \r\n");
dos.writeBytes("Content-Type: text/html;charset=utf-8\r\n");
dos.writeBytes("Content-Length: " + lengthOfBodyContent + "\r\n");
dos.writeBytes("\r\n");
}

private void responseBody(byte[] body) throws IOException {
dos.write(body, 0, body.length);
dos.flush();
}

private void response404Header(int lengthOfBodyContent) throws IOException {
dos.writeBytes("HTTP/1.1 404 Not Found \r\n");
dos.writeBytes("Content-Type: text/html;charset=utf-8\r\n");
dos.writeBytes("Content-Length: " + lengthOfBodyContent + "\r\n");
dos.writeBytes("\r\n");
}
}
9 changes: 9 additions & 0 deletions src/main/java/model/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ public User(String userId, String password, String name, String email) {
this.email = email;
}

public User(String userId, String password) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

생성자에서 예외처리가 필요해보입니다
userId, pwd 입력이 없을 경우에 대한 처리가 바깥에서 이뤄지거나 아예 없거나 한다면 User가 비정상적인 입력값에 의해서도 데이터가 계속해서 쌓일 수 있는 위험이 있습니다.

this.userId = userId;
this.password = password;
}

public String getUserId() {
return userId;
}
Expand All @@ -29,6 +34,10 @@ public String getEmail() {
return email;
}

public boolean isSameUser(User loginedUser) {
return this.password.equals(loginedUser.password);
}

@Override
public String toString() {
return "User [userId=" + userId + ", password=" + password + ", name=" + name + ", email=" + email + "]";
Expand Down
43 changes: 39 additions & 4 deletions src/main/java/util/HttpRequestUtils.java
Original file line number Diff line number Diff line change
@@ -1,25 +1,60 @@
package util;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;

import com.google.common.base.Strings;
import com.google.common.collect.Maps;

public class HttpRequestUtils {

private static final int INDEX_PATH = 1;
private static final int INDEX_METHOD = 0;


public static Map<String, String> extractHeader(BufferedReader br) throws IOException {
Map<String, String> header = new HashMap<>();
String requestLine;

while (!(requestLine = br.readLine()).equals("")) {
Pair pair = parseHeader(requestLine);
header.put(pair.getKey(), pair.getValue());
}
return header;
}

public static String getURL(String line) throws IOException {
if (line == null) throw new IOException("잘못된 Request Start Line");
return line.split(" ")[INDEX_PATH];
}

public static String getMethod(String line) throws IOException {
if (line == null) throw new IOException("잘못된 Request Start Line");
return line.split(" ")[INDEX_METHOD];
}


public static String decode(String line) throws UnsupportedEncodingException {
return URLDecoder.decode(line, StandardCharsets.UTF_8.toString());
}

/**
* @param queryString은
* URL에서 ? 이후에 전달되는 field1=value1&field2=value2 형식임
* @param queryString은 URL에서 ? 이후에 전달되는 field1=value1&field2=value2 형식임
* @return
*/
public static Map<String, String> parseQueryString(String queryString) {
return parseValues(queryString, "&");
}

/**
* @param 쿠키
* 값은 name1=value1; name2=value2 형식임
* @param 쿠키 값은 name1=value1; name2=value2 형식임
* @return
*/
public static Map<String, String> parseCookies(String cookies) {
Expand Down
Loading