Skip to content
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

[카프카] 3단계 - HTTP 웹 서버 구현 미션 제출합니다. #211

Open
wants to merge 48 commits into
base: include42
Choose a base branch
from
Open
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
1b171c1
feat: RequestUtils 구현
include42 Sep 9, 2020
38cfed3
feat: index.html로 연결되도록 구현
include42 Sep 9, 2020
4b53bba
feat: 잘못된 파일 경로 전달 시 예외 발생 구현
include42 Sep 10, 2020
222d346
feat: RequestUtils의 로직 수정 및 css 로딩 이슈 해결
include42 Sep 10, 2020
5981f28
feat: HttpRequest 클래스 구현, 기존 로직 일부 대체
include42 Sep 10, 2020
39ceaca
test: HttpRequest에 대한 테스트 코드 보강
include42 Sep 10, 2020
b482e0c
docs: README.md 작성
include42 Sep 14, 2020
32123e9
feat: HttpRequest의 요청 타겟을 별도 클래스로 분리
include42 Sep 14, 2020
5e41cd5
feat: 회원가입 로직 구현
include42 Sep 14, 2020
5008551
feat: RequestUtils 삭제, RequestPath에 대한 테스트 구현
include42 Sep 15, 2020
89a17cd
feat: Parameters에 대한 연산을 ParameterMapper에 담고, 이를 상속하여 RequestPath를 구현…
include42 Sep 15, 2020
0248195
feat: ParameterMapper에 대한 테스트 코드 구현, RequestBody 구현
include42 Sep 15, 2020
7e27832
feat: URI 처리를 위한 URIUtils 구현, TEST 작성
include42 Sep 15, 2020
c09d181
feat: 회원가입을 POST로 변경, body가 없는 경우 대응
include42 Sep 15, 2020
3d60b03
feat: 회원가입 완료시 302를 response로 반환, index로 리다이렉트 구현
include42 Sep 16, 2020
5388102
refactor: HttpRequest의 헤더 생성 로직 수정
include42 Sep 16, 2020
6711ba4
docs: README.md 작성, 현재 진행해야 할 사항 todo로 정리
include42 Sep 17, 2020
9ed7db3
feat: 파일의 경로를 정리해서 구하기 위해 FileType Enum 클래스 구현
include42 Sep 17, 2020
bca224c
style: 코드 전체의 인덴트와 import 정리, 불필요한 log 코드 삭제
include42 Sep 17, 2020
39a4824
feat: HTTP 요청의 Method를 Enum으로 개선
include42 Sep 28, 2020
486364f
feat: HTTP 요청의 최상단을 RequestLine으로 클래스 분리
include42 Oct 3, 2020
a58dea1
feat: lombok을 통해 User에 Builder 패턴 추가, RequestHandler의 코드 리팩토링
include42 Oct 3, 2020
4459da9
Merge branch 'include42' into level2
include42 Oct 3, 2020
223877b
test: RequestLine에 대한 테스트 코드 추가
include42 Oct 3, 2020
9f602de
refactor: 테스트 패키지 변경 및 필드 이름 변경
include42 Oct 3, 2020
e230c1f
feat: HttpRequest의 header 부분을 RequestHeader 클래스로 분리
include42 Oct 11, 2020
acc5580
feat: RequestHeader를 HttpHeader로 변경하여, 추후 Response에서도 사용하도록 구현
include42 Oct 12, 2020
f4aef8b
refactor: HttpRequest가 생성 시 parameter로 InputStream을 받도록 구조 변경
include42 Oct 12, 2020
f27ed0a
feat: HttpResponse 및 세부 클래스 구현
include42 Oct 12, 2020
cc6f7ad
feat: Controller 및 AbstractController 구현
include42 Oct 12, 2020
a543d3c
feat: CreateUserController / ResourceController 구현
include42 Oct 12, 2020
971c6e6
refactor: HttpResponse에서 주소 처리를 하도록 로직 수정
include42 Oct 12, 2020
c698f91
feat: ControllerMapper 구현 및 RequestHandler 리팩토링
include42 Oct 12, 2020
15c7a9a
refactor: Controller 패키지 위치 변경
include42 Oct 13, 2020
b789efa
feat: 쓰레드풀 적용
include42 Oct 13, 2020
37ed78e
feat: Request에 대한 테스트 코드 개편, HttpRequest 리팩토링
include42 Oct 13, 2020
1aba1fb
feat: HttpResponse에 대한 테스트 코드 작성, 미사용 메서드 삭제
include42 Oct 13, 2020
202a0b1
test: Controller 상속 클래스 및 ControllerMapper에 대한 테스트 코드 구현
include42 Oct 13, 2020
fe63d91
refactor: 피드백에 따른 코드 리팩토링
include42 Oct 20, 2020
1d61547
refactor: HttpResponse의 write 로직 구성 변경
include42 Oct 20, 2020
a334a8e
Merge remote-tracking branch 'base/include42' into level3
include42 Oct 28, 2020
501f032
docs: README에 누락된 2단계 요구사항 및 3단계 요구사항 추가
include42 Nov 20, 2020
74ff35c
feat: 로그인 기능 구현 완료
include42 Nov 20, 2020
9b80920
feat: HandleBars Template을 통해 리스트를 출력할 수 있도록 구현
include42 Nov 20, 2020
9792296
feat: 로그인 상태에서 '/user/list' GET 요청 시 사용자 정보 조회하는 기능 구현
include42 Nov 20, 2020
6d521ae
feat: 세션 관리를 위한 인터페이스와 구현 클래스, 저장소 추가
include42 Nov 23, 2020
cf11c79
feat: 세션을 통해 로그인 처리를 수행하도록 로직 및 테스트 코드 추가
include42 Nov 23, 2020
632b851
docs: 3단계 수행내역 README에 반영
include42 Nov 23, 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
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
# 웹 애플리케이션 서버
## 요구사항
## 1단계 요구사항
- [x] http://localhost:8080/index.html 로 접속했을 때 webapp 디렉토리의 index.html 파일을 읽어 클라이언트에 응답한다.
- [x] “회원가입” 메뉴를 클릭하면 http://localhost:8080/user/form.html 으로 이동하면서 회원가입할 수 있다.
- [x] http://localhost:8080/user/form.html 파일의 form 태그 method를 get에서 post로 수정한 후 회원가입 기능이 정상적으로 동작하도록 구현한다.
- [x] “회원가입”을 완료하면 /index.html 페이지로 이동하고 싶다. 현재는 URL이 /user/create 로 유지되는 상태로 읽어서 전달할 파일이 없다. 따라서 redirect 방식처럼 회원가입을 완료한 후 “index.html”로 이동해야 한다.
- [x] 지금까지 구현한 소스 코드는 stylesheet 파일을 지원하지 못하고 있다. Stylesheet 파일을 지원하도록 구현하도록 한다.

## 2단계 요구사항
- [x] 다수의 사용자 요청에 대해 Queue 에 저장한 후 순차적으로 처리가 가능하도록 해야 한다.
- [x] 서버가 모든 요청에 대해 Thread를 매번 생성하는 경우 성능상 문제가 발생할 수 있다. Thread Pool을 적용해 일정 수의 사용자 동시에 처리가 가능하도록 한다.
- [x] HTTP 요청 Header/Body 처리, 응답 Header/Body 처리만을 담당하는 역할을 분리해 재사용 가능하도록 한다.
- [x] 다형성을 활용해 클라이언트 요청 URL에 대한 분기 처리를 제거한다.

## 3단계 요구사항
- [x] 로그인할 수 있다. 로그인이 성공하면 index.html로 이동하고, 로그인이 실패하면 /user/login_failed.html로 이동해야 한다.
- [x] 앞에서 회원가입한 사용자로 로그인할 수 있어야 한다.
- [x] 로그인이 성공하면 cookie를 활용해 로그인 상태를 유지할 수 있어야 한다.
- [x] 로그인이 성공할 경우 요청 header의 Cookie header 값이 logined=true, 로그인이 실패하면 Cookie header 값이 logined=false로 전달되어야 한다.
- [x] 사용자가 “로그인” 상태일 경우(Cookie 값이 logined=true) 경우 http://localhost:8080/user/list 로 접근했을 때 사용자 목록을 출력한다. 만약 로그인하지 않은 상태라면 로그인 페이지(login.html)로 이동한다.
- [x] 서블릿에서 지원하는 HttpSession API의 일부를 지원해야 한다.
- [x] SessionStore를 통해 백엔드에서 세션에 접근할 수 있어야 한다.
- [x] 로그인 여부 확인을 세션으로 대체한다.
## 우아한테크코스 코드리뷰
* [온라인 코드 리뷰 과정](https://github.com/woowacourse/woowacourse-docs/blob/master/maincourse/README.md)
2 changes: 2 additions & 0 deletions src/main/java/controller/ControllerMapper.java
Original file line number Diff line number Diff line change
@@ -10,6 +10,8 @@ public class ControllerMapper {

static {
controllers.put("/user/create", new CreateUserController());
controllers.put("/user/list", new UserListController());
controllers.put("/user/login", new LoginController());
controllers.put("/", new IndexController());
}

1 change: 0 additions & 1 deletion src/main/java/controller/CreateUserController.java
Original file line number Diff line number Diff line change
@@ -7,7 +7,6 @@
import web.response.HttpResponse;

public class CreateUserController extends AbstractController {

public static final String CREATE_USER_LOGGING_MESSAGE = "New User created! -> {}";
public static final String INDEX_HTML_PATH = "/index.html";

40 changes: 40 additions & 0 deletions src/main/java/controller/LoginController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package controller;

import db.DataBase;
import exception.UserNotFoundException;
import model.User;
import web.request.HttpRequest;
import web.response.HttpResponse;
import web.session.HttpSession;
import web.session.SessionStore;
import web.session.WebSession;

public class LoginController extends AbstractController {
public static final String INDEX_HTML_PATH = "/index.html";
public static final String LOGIN_FAIL_HTML_PATH = "/user/login_failed.html";

@Override
protected void doPost(HttpRequest request, HttpResponse response) {
try {
String userId = request.getRequestBodyByKey("userId");
User user = DataBase.findUserById(userId)
.orElseThrow(() -> new UserNotFoundException(userId));
loginValidator(request, user);
HttpSession session = new WebSession();
session.setAttribute("email", user.getEmail());
SessionStore.addSession(session);
response.addSession(session);
response.found(INDEX_HTML_PATH);
} catch (IllegalAccessException | UserNotFoundException e) {
logger.error(e.getMessage());
response.found(LOGIN_FAIL_HTML_PATH);
}
}

private void loginValidator(HttpRequest request, User user) throws IllegalAccessException {
String password = request.getRequestBodyByKey("password");
if (!user.checkPassword(password)) {
throw new IllegalAccessException("로그인에 실패하였습니다.");
}
}
}
35 changes: 35 additions & 0 deletions src/main/java/controller/UserListController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package controller;

import com.github.jknack.handlebars.Template;
import db.DataBase;
import exception.RequestParameterNotFoundException;
import model.User;
import utils.TemplateUtils;
import web.request.HttpRequest;
import web.response.HttpResponse;
import web.session.SessionStore;

import java.io.IOException;
import java.util.Collection;

public class UserListController extends AbstractController {
public static final String LOGIN_HTML_PATH = "/user/login.html";

@Override
protected void doGet(HttpRequest request, HttpResponse response) {
try {
String sessionId = request.getSession();
if (!SessionStore.isContains(sessionId)) {
throw new IllegalAccessException("세션을 확인할 수 없습니다.");
}
String path = request.getTarget();
Template template = TemplateUtils.buildTemplate(path);
Collection<User> users = DataBase.findAll();
String result = template.apply(users);
response.ok(result);
} catch (IllegalAccessException | RequestParameterNotFoundException | IOException e) {
logger.error(e.getMessage());
response.found(LOGIN_HTML_PATH);
}
}
}
13 changes: 11 additions & 2 deletions src/main/java/db/DataBase.java
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@

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

public class DataBase {
private static final Map<String, User> users = Maps.newHashMap();
@@ -13,11 +14,19 @@ public static void addUser(User user) {
users.put(user.getUserId(), user);
}

public static User findUserById(String userId) {
return users.get(userId);
public static Optional<User> findUserById(String userId) {
if (users.containsKey(userId)) {
User foundUser = users.get(userId);
return Optional.of(foundUser);
}
return Optional.empty();
}

public static Collection<User> findAll() {
return users.values();
}

public static void deleteAll() {
users.clear();
}
}
9 changes: 9 additions & 0 deletions src/main/java/exception/UserNotFoundException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package exception;

public class UserNotFoundException extends RuntimeException {
private static final String USER_NOT_FOUND_MESSAGE = "해당 유저를 찾을 수 없습니다. id : ";

public UserNotFoundException(String id) {
super(USER_NOT_FOUND_MESSAGE + id);
}
}
4 changes: 4 additions & 0 deletions src/main/java/model/User.java
Original file line number Diff line number Diff line change
@@ -32,6 +32,10 @@ public String getEmail() {
return email;
}

public boolean checkPassword(String password) {
return this.password.equals(password);
}

@Override
public String toString() {
return "User [userId=" + userId + ", password=" + password + ", name=" + name + ", email=" + email + "]";
28 changes: 28 additions & 0 deletions src/main/java/utils/TemplateUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package utils;

import com.github.jknack.handlebars.Handlebars;
import com.github.jknack.handlebars.Helper;
import com.github.jknack.handlebars.Template;
import com.github.jknack.handlebars.io.ClassPathTemplateLoader;
import com.github.jknack.handlebars.io.TemplateLoader;

import java.io.IOException;

public class TemplateUtils {
private static final String PREFIX = "/templates";
private static final String SUFFIX = ".html";
private static final Handlebars handlebars;

static {
TemplateLoader loader = new ClassPathTemplateLoader();
loader.setPrefix(PREFIX);
loader.setSuffix(SUFFIX);

handlebars = new Handlebars(loader);
handlebars.registerHelper("increase", (Helper<Integer>) (number, options) -> number + 1);
}

public static Template buildTemplate(String path) throws IOException {
return handlebars.compile(path);
}
}
2 changes: 2 additions & 0 deletions src/main/java/web/HttpHeader.java
Original file line number Diff line number Diff line change
@@ -8,6 +8,8 @@ public class HttpHeader {
public static final String CONTENT_LENGTH = "Content-Length";
public static final String CONTENT_TYPE = "Content-Type";
public static final String LOCATION = "Location";
public static final String SET_COOKIE = "Set-Cookie";
public static final String COOKIE = "Cookie";
public static final String HEADER_DELIMITER = ": ";

private final Map<String, String> headers;
32 changes: 32 additions & 0 deletions src/main/java/web/request/Cookies.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package web.request;

import exception.RequestParameterNotFoundException;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

public class Cookies {
private static final String COOKIES_DELIMITER = "; ";
private static final String COOKIE_DELIMITER = "=";

private final Map<String, String> cookieMatcher = new HashMap<>();

public Cookies(String cookies) {
if (Objects.isNull(cookies) || cookies.isEmpty()) {
return;
}
String[] tokens = cookies.split(COOKIES_DELIMITER);
for (String token : tokens) {
String[] value = token.split(COOKIE_DELIMITER);
this.cookieMatcher.put(value[0], value[1]);
}
}

public String getCookieByKey(String key) {
if (cookieMatcher.containsKey(key)) {
return cookieMatcher.get(key);
}
throw new RequestParameterNotFoundException("[COOKIE]" + key);
}
}
7 changes: 7 additions & 0 deletions src/main/java/web/request/HttpRequest.java
Original file line number Diff line number Diff line change
@@ -17,17 +17,20 @@

public class HttpRequest {
private static final Logger logger = LoggerFactory.getLogger(HttpRequest.class);
private static final String SESSION_ID = "JSESSIONID";

private final RequestLine requestLine;
private final HttpHeader httpHeader;
private final RequestBody requestBody;
private final Cookies cookies;

public HttpRequest(InputStream inputStream) {
try {
BufferedReader request = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
requestLine = new RequestLine(request.readLine());
httpHeader = new HttpHeader(mappingHeaders(request));
requestBody = mappingBodies(request);
cookies = new Cookies(httpHeader.getHeaderByKey(HttpHeader.COOKIE));
} catch (IndexOutOfBoundsException | NullPointerException | IOException e) {
throw new InvalidHttpRequestException();
}
@@ -91,4 +94,8 @@ public String getRequestHeaderByKey(String key) {
public String getRequestBodyByKey(String key) {
return requestBody.getParameterByKey(key);
}

public String getSession() {
return cookies.getCookieByKey(SESSION_ID);
}
}
15 changes: 14 additions & 1 deletion src/main/java/web/response/HttpResponse.java
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@
import utils.FileIoUtils;
import utils.URIUtils;
import web.HttpHeader;
import web.session.HttpSession;

import java.io.DataOutputStream;
import java.io.IOException;
@@ -16,6 +17,7 @@ public class HttpResponse {
public static final String BAD_REQUEST_ERROR_MESSAGE = "errorMessage : ";
private static final Logger logger = LoggerFactory.getLogger(HttpResponse.class);
private static final String HTTP_VERSION = "HTTP/1.1";
public static final String SESSION_PREFIX = "JSESSIONID=";

private final DataOutputStream dataOutputStream;
private final HttpHeader httpHeader;
@@ -27,10 +29,14 @@ public HttpResponse(OutputStream outputStream) {
httpHeader = new HttpHeader();
}

private void addHeader(String key, String value) {
public void addHeader(String key, String value) {
httpHeader.addHeader(key, value);
}

public void addSession(HttpSession session) {
httpHeader.addHeader(HttpHeader.SET_COOKIE, SESSION_PREFIX + session.getId() + "; Path=/");
}

public void ok(String path, String contentType) throws IOException, URISyntaxException {
String filePath = URIUtils.getFilePath(path);
responseLine = new ResponseLine(ResponseStatus.OK, HTTP_VERSION);
@@ -39,6 +45,13 @@ public void ok(String path, String contentType) throws IOException, URISyntaxExc
write();
}

public void ok(String path) {
responseLine = new ResponseLine(ResponseStatus.OK, HTTP_VERSION);
addHeader(HttpHeader.CONTENT_TYPE, "text/html;charset=UTF-8");
responseBody = new ResponseBody(path.getBytes());
write();
}

public void found(String location) {
responseLine = new ResponseLine(ResponseStatus.FOUND, HTTP_VERSION);
addHeader(HttpHeader.LOCATION, location);
13 changes: 13 additions & 0 deletions src/main/java/web/session/HttpSession.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package web.session;

public interface HttpSession {
String getId();

void setAttribute(String name, Object value);

Object getAttribute(String name);

void removeAttribute(String name);

void invalidate();
}
28 changes: 28 additions & 0 deletions src/main/java/web/session/SessionStore.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package web.session;

import java.util.HashMap;
import java.util.Map;

public class SessionStore {
private static final Map<String, HttpSession> sessions = new HashMap<>();

public static HttpSession getSession(String id) {
return sessions.get(id);
}

public static void addSession(HttpSession session) {
addSession(session.getId(), session);
}

public static void addSession(String id, HttpSession session) {
sessions.put(id, session);
}

public static boolean isContains(String id) {
return sessions.containsKey(id);
}

public static void remove(String id) {
sessions.remove(id);
}
}
36 changes: 36 additions & 0 deletions src/main/java/web/session/WebSession.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package web.session;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

public class WebSession implements HttpSession {
private final Map<String, Object> attributes = new HashMap<>();

private final UUID id = UUID.randomUUID();

@Override
public String getId() {
return id.toString();
}

@Override
public void setAttribute(String name, Object value) {
attributes.put(name, value);
}

@Override
public Object getAttribute(String name) {
return attributes.get(name);
}

@Override
public void removeAttribute(String name) {
attributes.remove(name);
}

@Override
public void invalidate() {
attributes.clear();
}
}
Loading