-
Notifications
You must be signed in to change notification settings - Fork 90
[라면] 1단계 - HTTP 웹서버 구현 미션 제출합니다! #110
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
base: ramen6315
Are you sure you want to change the base?
Changes from all commits
e57894b
a0524d4
7cf6f64
930e462
626ec3b
bd8ce69
1fbbcc6
a015415
d868586
0f6c140
472ffe9
65bd22b
69887d0
8499dad
2e586a4
c82ba70
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,21 @@ | ||
| # 웹 애플리케이션 서버 | ||
| ## 진행 방법 | ||
| * 웹 애플리케이션 서버 요구사항을 파악한다. | ||
| * 요구사항에 대한 구현을 완료한 후 자신의 github 아이디에 해당하는 브랜치에 Pull Request(이하 PR)를 통해 코드 리뷰 요청을 한다. | ||
| * 코드 리뷰 피드백에 대한 개선 작업을 하고 다시 PUSH한다. | ||
| * 모든 피드백을 완료하면 다음 단계를 도전하고 앞의 과정을 반복한다. | ||
|
|
||
| ## 우아한테크코스 코드리뷰 | ||
| * [온라인 코드 리뷰 과정](https://github.com/woowacourse/woowacourse-docs/blob/master/maincourse/README.md) | ||
| - [x] 요구사항 1 <br> | ||
| http://localhost:8080/index.html 로 접속했을 때 webapp 디렉토리의 index.html 파일을 읽어 클라이언트에 응답한다. | ||
|
|
||
| - [x] 요구사항 2 <br> | ||
| “회원가입” 메뉴를 클릭하면 http://localhost:8080/user/form.html 으로 이동하면서 회원가입할 수 있다. 회원가입한다. | ||
| - [x] 요청 받은 url정보를 분리 한다. | ||
| - [x] 분리된 url의 요청이 회원가입 요청인지 확인한다. | ||
| - [x] 회원 가입일 경우 유저 정보를 분리 한다. | ||
| - [x] 유저를 저장한다. | ||
| - [x] 메인 화면으로 돌아간다. | ||
|
|
||
| - [x] 요구사항 3 <br> | ||
| http://localhost:8080/user/form.html 파일의 form 태그 method를 get에서 post로 수정한 후 회원가입 기능이 정상적으로 동작하도록 구현한다. | ||
|
|
||
| - [x] 요구사항 4 <br> | ||
| “회원가입”을 완료하면 /index.html 페이지로 이동하고 싶다. 현재는 URL이 /user/create 로 유지되는 상태로 읽어서 전달할 파일이 없다. 따라서 redirect 방식처럼 회원가입을 완료한 후 “index.html”로 이동해야 한다. 즉, 브라우저의 URL이 /index.html로 변경해야 한다. | ||
|
|
||
| - [x] 요구사항 5 | ||
| 지금까지 구현한 소스 코드는 stylesheet 파일을 지원하지 못하고 있다. Stylesheet 파일을 지원하도록 구현하도록 한다. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| package controller; | ||
|
|
||
| import java.io.DataOutputStream; | ||
| import java.io.IOException; | ||
| import java.io.OutputStream; | ||
| import java.net.URISyntaxException; | ||
|
|
||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| import utils.FileIoUtils; | ||
| import webserver.RequestHandler; | ||
|
|
||
| public class PageController { | ||
|
|
||
| private static final Logger logger = LoggerFactory.getLogger(RequestHandler.class); | ||
| private static final int PATH_INDEX = 1; | ||
| private static final int METHOD_INDEX = 0; | ||
| private static final String TEMPLATES_PATH = "./templates"; | ||
| private static final String STATIC_PATH = "./static"; | ||
| private static final String HTML_EXTENSION_VALUE = ".html"; | ||
|
|
||
| public static void getIndexPage(final OutputStream out, final String[] request) throws | ||
| IOException, URISyntaxException { | ||
| if(!"GET".equals(request[METHOD_INDEX])) { | ||
| return; | ||
| } | ||
|
|
||
| String uri = request[PATH_INDEX]; | ||
|
|
||
| if(request[PATH_INDEX].endsWith(HTML_EXTENSION_VALUE)) { | ||
| byte[] body = FileIoUtils.loadFileFromClasspath(TEMPLATES_PATH + uri); | ||
| DataOutputStream dos = new DataOutputStream(out); | ||
| response200Header(dos, body.length); | ||
| responseBody(dos, body); | ||
| } else { | ||
| String[] headerToken = request[PATH_INDEX].split("\\."); | ||
| byte[] body = FileIoUtils.loadFileFromClasspath(STATIC_PATH + uri); | ||
| DataOutputStream dos = new DataOutputStream(out); | ||
| response200StaticResourcesHeader(dos, body.length, headerToken[headerToken.length-1]); | ||
| responseBody(dos, body); | ||
| } | ||
| } | ||
|
|
||
| private static 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) { | ||
| logger.error(e.getMessage()); | ||
| } | ||
| } | ||
|
|
||
| private static void response200StaticResourcesHeader(DataOutputStream dos, int lengthOfBodyContent, String staticValue) { | ||
| try { | ||
| dos.writeBytes("HTTP/1.1 200 OK \r\n"); | ||
| dos.writeBytes("Content-Type: text/"+staticValue+";charset=utf-8\r\n"); | ||
| dos.writeBytes("Content-Length: " + lengthOfBodyContent + "\r\n"); | ||
| dos.writeBytes("\r\n"); | ||
| } catch (IOException e) { | ||
| logger.error(e.getMessage()); | ||
| } | ||
| } | ||
|
|
||
| private static void responseBody(DataOutputStream dos, byte[] body) { | ||
| try { | ||
| dos.write(body, 0, body.length); | ||
| dos.flush(); | ||
| } catch (IOException e) { | ||
| logger.error(e.getMessage()); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| package controller; | ||
|
|
||
| import java.io.BufferedReader; | ||
| import java.io.DataOutputStream; | ||
| import java.io.IOException; | ||
| import java.io.OutputStream; | ||
| import java.util.HashMap; | ||
|
|
||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| import service.UserService; | ||
| import utils.IOUtils; | ||
| import webserver.RequestHandler; | ||
|
|
||
| public class UserController { | ||
|
|
||
| private static final Logger logger = LoggerFactory.getLogger(RequestHandler.class); | ||
| private static final int PATH_INDEX = 1; | ||
| private static final int METHOD_INDEX = 0; | ||
|
|
||
|
|
||
| public static void postSignIn (final OutputStream out, final BufferedReader bufferedReader, final String[] request, | ||
| final HashMap<String, String> requestUrl) throws IOException { | ||
|
|
||
| if (isPostSignIn(request)) { | ||
| String userInfoUrl = IOUtils.readData(bufferedReader, Integer.parseInt(requestUrl.get("Content-Length"))); | ||
| UserService.signIn(userInfoUrl); | ||
| logger.debug("userInfo: {}" + userInfoUrl); | ||
| DataOutputStream dos = new DataOutputStream(out); | ||
| response302Header(dos); | ||
| } | ||
| } | ||
|
|
||
| private static boolean isPostSignIn(final String[] request) { | ||
| return "POST".equals(request[METHOD_INDEX]) && request[PATH_INDEX].startsWith("/user/create"); | ||
| } | ||
|
|
||
| private static 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) { | ||
| logger.error(e.getMessage()); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| package service; | ||
|
|
||
| import java.util.HashMap; | ||
|
|
||
| import db.DataBase; | ||
| import model.User; | ||
| import utils.RequestUtils; | ||
|
|
||
| public class UserService { | ||
|
|
||
| public static void signIn(String userInfoUrl) { | ||
| HashMap<String, String> signInInfo = RequestUtils.parseUserInfo(userInfoUrl); | ||
| User user = new User(signInInfo.get("userId"),signInInfo.get("password") | ||
| ,signInInfo.get("name"),signInInfo.get("email")); | ||
|
|
||
| DataBase.addUser(user); | ||
| } | ||
|
|
||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,12 @@ | |
|
|
||
| import java.io.BufferedReader; | ||
| import java.io.IOException; | ||
| import java.util.HashMap; | ||
|
|
||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| import webserver.RequestHandler; | ||
|
|
||
| public class IOUtils { | ||
| /** | ||
|
|
@@ -12,9 +18,28 @@ public class IOUtils { | |
| * @return | ||
| * @throws IOException | ||
| */ | ||
| private static final Logger logger = LoggerFactory.getLogger(RequestHandler.class); | ||
| private static final String HEADER_TOKEN_SPLIT_VALUE = ": "; | ||
| private static final int HEADER_PARAMETER_INDEX = 0; | ||
| private static final int HEADER_PARAMETER_VALUE_INDEX = 1; | ||
|
|
||
| public static String readData(BufferedReader br, int contentLength) throws IOException { | ||
| char[] body = new char[contentLength]; | ||
| br.read(body, 0, contentLength); | ||
| return String.copyValueOf(body); | ||
| } | ||
|
|
||
| public static void parseHeaderToken(final BufferedReader bufferedReader, String line, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IOUtils는 IO 처리만 담당해야 합니다.
|
||
| final HashMap<String, String> requestUrl) throws IOException { | ||
| while(!"".equals(line)) { | ||
| logger.debug("header Line: {} " + line); | ||
| String[] headerToken = line.split(HEADER_TOKEN_SPLIT_VALUE); | ||
|
|
||
| if(headerToken.length == 2) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 객체지향 생활체조 2뎁스를 없애려면 어떻께 하면 될까요? |
||
| requestUrl.put(headerToken[HEADER_PARAMETER_INDEX], headerToken[HEADER_PARAMETER_VALUE_INDEX]); | ||
| } | ||
|
|
||
| line = bufferedReader.readLine(); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| package utils; | ||
|
|
||
| import java.util.HashMap; | ||
|
|
||
| public class RequestUtils { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. util로 만들지 말고 http request를 처리하는 객체를 추가해보는게 좋겠네요 |
||
|
|
||
| private static final int SIGN_IN_DATA_KEY_INDEX = 0; | ||
| private static final int SIGN_IN_DATA_VALUE_INDEX = 1; | ||
| private static final String USER_PARAMETER_SPLIT_STRING = "&"; | ||
| private static final String USER_INFO_SPLIT_STRING = "="; | ||
| private static final String URI_SPLIT_STRING = " "; | ||
|
|
||
| public static String[] separateUrl(final String requestUrl) { | ||
| return requestUrl.split(URI_SPLIT_STRING); | ||
| } | ||
|
|
||
| public static HashMap<String, String> parseUserInfo(final String userInfoUrl) { | ||
|
|
||
| HashMap<String, String> signInData = new HashMap<>(); | ||
| String[] userInfos = userInfoUrl.split(USER_PARAMETER_SPLIT_STRING); | ||
|
|
||
| for (String userInfo : userInfos) { | ||
| String[] userValues = userInfo.split(USER_INFO_SPLIT_STRING); | ||
| signInData.put(userValues[SIGN_IN_DATA_KEY_INDEX],userValues[SIGN_IN_DATA_VALUE_INDEX]); | ||
| } | ||
|
|
||
| return signInData; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,16 +1,26 @@ | ||||||
| package webserver; | ||||||
|
|
||||||
| import java.io.DataOutputStream; | ||||||
| import java.io.BufferedReader; | ||||||
| import java.io.IOException; | ||||||
| import java.io.InputStream; | ||||||
| import java.io.InputStreamReader; | ||||||
| import java.io.OutputStream; | ||||||
| import java.net.Socket; | ||||||
| import java.net.URISyntaxException; | ||||||
| import java.nio.charset.StandardCharsets; | ||||||
| import java.util.HashMap; | ||||||
|
|
||||||
| import org.slf4j.Logger; | ||||||
| import org.slf4j.LoggerFactory; | ||||||
|
|
||||||
| import controller.PageController; | ||||||
| import controller.UserController; | ||||||
| import utils.IOUtils; | ||||||
| import utils.RequestUtils; | ||||||
|
|
||||||
| public class RequestHandler implements Runnable { | ||||||
| private static final Logger logger = LoggerFactory.getLogger(RequestHandler.class); | ||||||
| private static final int PATH_INDEX = 1; | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 변수는 사용하는 곳이 없네요 |
||||||
|
|
||||||
| private Socket connection; | ||||||
|
|
||||||
|
|
@@ -23,32 +33,17 @@ public void run() { | |||||
| connection.getPort()); | ||||||
|
|
||||||
| try (InputStream in = connection.getInputStream(); OutputStream out = connection.getOutputStream()) { | ||||||
| // TODO 사용자 요청에 대한 처리는 이 곳에 구현하면 된다. | ||||||
| DataOutputStream dos = new DataOutputStream(out); | ||||||
| byte[] body = "Hello World".getBytes(); | ||||||
| response200Header(dos, body.length); | ||||||
| responseBody(dos, body); | ||||||
| } catch (IOException e) { | ||||||
| logger.error(e.getMessage()); | ||||||
| } | ||||||
| } | ||||||
| BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)); | ||||||
| HashMap<String, String> requestUrl = new HashMap<>(); | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
HashMap이라는 타입으로 정하지 말고 Map 인터페이스를 사용하는게 좋습니다. |
||||||
| String line = bufferedReader.readLine(); | ||||||
|
|
||||||
| 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) { | ||||||
| logger.error(e.getMessage()); | ||||||
| } | ||||||
| } | ||||||
| String[] request = RequestUtils.separateUrl(line); | ||||||
| IOUtils.parseHeaderToken(bufferedReader,line,requestUrl); | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. HttpRequest 객체를 추가해보면 어떨까요? |
||||||
|
|
||||||
| UserController.postSignIn(out, bufferedReader, request, requestUrl); | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 컨트롤러에서 http request를 파라미터로 받아서 자신이 처리할 요청이 맞는지 판단하도록 설계하신 것 같네요 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. was를 구현하는 과정이니 컨트롤러 대신 서블릿을 추가해보면 어떨까요? |
||||||
| PageController.getIndexPage(out, request); | ||||||
|
|
||||||
| private void responseBody(DataOutputStream dos, byte[] body) { | ||||||
| try { | ||||||
| dos.write(body, 0, body.length); | ||||||
| dos.flush(); | ||||||
| } catch (IOException e) { | ||||||
| } catch (IOException | URISyntaxException e) { | ||||||
| logger.error(e.getMessage()); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| package service; | ||
|
|
||
| import static org.assertj.core.api.Assertions.*; | ||
|
|
||
| import org.junit.jupiter.api.DisplayName; | ||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| import db.DataBase; | ||
| import model.User; | ||
|
|
||
| class UserServiceTest { | ||
|
|
||
| @DisplayName("요청한 유저 정보를 저장한다.") | ||
| @Test | ||
| void userSignInTest() { | ||
| UserService.signIn("userId=Id&password=password&name=ramen&email=mail@gmail.com"); | ||
|
|
||
| User id = DataBase.findUserById("Id"); | ||
| assertThat(id.getName()).isEqualTo("ramen"); | ||
| } | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| package utils; | ||
|
|
||
| import static org.assertj.core.api.Assertions.*; | ||
|
|
||
| import org.junit.jupiter.api.DisplayName; | ||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| class RequestUtilsTest { | ||
|
|
||
| private final String getSignInRequestUrl = "POST /user/create?userId=Id&password=password&name=name&email=email@gmail.com HTTP/1.1"; | ||
| private final String[] requestUrlArrays = RequestUtils.separateUrl(getSignInRequestUrl); | ||
|
|
||
| @DisplayName("Http 요청이 들어오면 http 요청 정보를 분리한다") | ||
| @Test | ||
| void separateHttpRequest() { | ||
|
|
||
| assertThat(requestUrlArrays[0]).isEqualTo("POST"); | ||
| assertThat(requestUrlArrays[1]).isEqualTo("/user/create?userId=Id&password=password&name=name&email=email@gmail.com"); | ||
| assertThat(requestUrlArrays[2]).isEqualTo("HTTP/1.1"); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
지금 구조라면 컨트롤러를 추가할 때마다 이 조건문과 http reponse를 처리하는 로직이 중복해서 생기겠네요
HttpRequest, HttpResponse 클래스를 만들고 각 클래스에서 input, output 처리를 하도록 만들면 중복을 줄이고 읽기 좋은 코드로 만들 수 있을거에요