Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
26 changes: 19 additions & 7 deletions README.md
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 파일을 지원하도록 구현하도록 한다.
75 changes: 75 additions & 0 deletions src/main/java/controller/PageController.java
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)) {
Copy link
Copy Markdown

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 처리를 하도록 만들면 중복을 줄이고 읽기 좋은 코드로 만들 수 있을거에요

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());
}
}
}
49 changes: 49 additions & 0 deletions src/main/java/controller/UserController.java
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());
}
}
}
20 changes: 20 additions & 0 deletions src/main/java/service/UserService.java
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);
}


}
25 changes: 25 additions & 0 deletions src/main/java/utils/IOUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand All @@ -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,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

IOUtils는 IO 처리만 담당해야 합니다.
이 메서드는

  1. io로 데이터를 읽어오고
  2. 읽어온 데이터를 파싱
    하고 있습니다.
    1번만 하도록 수정하고 2번은 Http Header 클래스를 추가해서 그 클래스에게 책임을 부여하세요

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) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

객체지향 생활체조
규칙 1: 한 메서드에 오직 한 단계의 들여쓰기만 한다.

2뎁스를 없애려면 어떻께 하면 될까요?

requestUrl.put(headerToken[HEADER_PARAMETER_INDEX], headerToken[HEADER_PARAMETER_VALUE_INDEX]);
}

line = bufferedReader.readLine();
}
}
}
29 changes: 29 additions & 0 deletions src/main/java/utils/RequestUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package utils;

import java.util.HashMap;

public class RequestUtils {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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;
}
}
45 changes: 20 additions & 25 deletions src/main/java/webserver/RequestHandler.java
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;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

이 변수는 사용하는 곳이 없네요


private Socket connection;

Expand All @@ -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<>();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
HashMap<String, String> requestUrl = new HashMap<>();
Map<String, String> requestUrl = new HashMap<>();

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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

HttpRequest 객체를 추가해보면 어떨까요?
그 객체에서 http message를 읽고 header 객체도 처리하도록 객체 설계를 해보면 좋겠어요


UserController.postSignIn(out, bufferedReader, request, requestUrl);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

컨트롤러에서 http request를 파라미터로 받아서 자신이 처리할 요청이 맞는지 판단하도록 설계하신 것 같네요
컨트롤러가 하는 역할은 무엇인가요?
내가 처리할 요청이 맞는지 판단하는 책임도 가져야 할까요?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

was를 구현하는 과정이니 컨트롤러 대신 서블릿을 추가해보면 어떨까요?
서블릿으로 구현해보고 미션 진행해나가면서 Dispatcher Servlet을 추가해서 요청에 따라 어느 컨트롤러를 실행할지 결정하도록 구현하는 방향으로 가면 좋겠네요

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());
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/templates/user/form.html
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
<div class="container" id="main">
<div class="col-md-6 col-md-offset-3">
<div class="panel panel-default content-main">
<form name="question" method="get" action="/user/create">
<form name="question" method="post" action="/user/create">
<div class="form-group">
<label for="userId">사용자 아이디</label>
<input class="form-control" id="userId" name="userId" placeholder="User ID">
Expand Down
22 changes: 22 additions & 0 deletions src/test/java/service/UserServiceTest.java
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");
}

}
21 changes: 21 additions & 0 deletions src/test/java/utils/RequestUtilsTest.java
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");
}
}