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

[제이] 1단계 - HTTP 웹 서버 리팩터링 미션 제출합니다. #198

Merged
merged 24 commits into from
Nov 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
cfdedc5
docs: README.md에 요구사항 1 추가
jjj0611 Sep 16, 2020
7ae41d0
feat: Request Handler에서 Header 추출 기능 구현
jjj0611 Sep 16, 2020
48f831b
feat: request의 path로 파일을 읽어서 응답하는 기능 추가
jjj0611 Sep 16, 2020
0cfe18f
feat: Http Request 생성
jjj0611 Nov 12, 2020
1f2a23a
feat: 요구사항1 완료
jjj0611 Nov 12, 2020
f9efed8
docs: 두번째 요구사항 작성
jjj0611 Nov 12, 2020
e7f841e
refactoring: http spec 정의
jjj0611 Nov 12, 2020
880a9c9
refactoring: http request spec 추가 및 테스트 추가
jjj0611 Nov 12, 2020
7f051bf
refactor: http request spec 변경
jjj0611 Nov 15, 2020
1577d31
feat: 요구사항2 완료
jjj0611 Nov 15, 2020
790c275
docs: 요구사항3을 위한 README.md 작성
jjj0611 Nov 15, 2020
51ca0e6
feat: 회원 가입 기능이 post 요청에서 정상적으로 동작하도록 구현
jjj0611 Nov 15, 2020
804187e
docs: 요구사항4 작성
jjj0611 Nov 15, 2020
4e33b83
feat: 회원가입 완료 후 index.html로 이동하도록 변경
jjj0611 Nov 15, 2020
6d3ccd0
docs: 요구사항 5 README.md에 작성
jjj0611 Nov 15, 2020
bb413a8
feat: 요청 uri이 static resource인지를 확인하는 기능 생성
jjj0611 Nov 15, 2020
c5a52cf
feat: stylesheet 등 다양한 형식의 파일을 지원하도록 변경
jjj0611 Nov 15, 2020
49ec3b8
feat: Http Response 분리
jjj0611 Nov 16, 2020
81991f4
test: SimpleHttRequest에 대한 Test 작성
jjj0611 Nov 16, 2020
dce3ec9
test: HttpResponse에 대한 Test 작성
jjj0611 Nov 16, 2020
97b6dcc
refactor: HttpServlet 및 UserController 생성
jjj0611 Nov 16, 2020
66cac42
feat: DispatcherServlet 생성
jjj0611 Nov 16, 2020
ef60277
feat: ThreadPool 사용
jjj0611 Nov 16, 2020
e4ff0ee
docs: 구현 내용 정리
jjj0611 Nov 16, 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
68 changes: 67 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,70 @@
* 모든 피드백을 완료하면 다음 단계를 도전하고 앞의 과정을 반복한다.

## 우아한테크코스 코드리뷰
* [온라인 코드 리뷰 과정](https://github.com/woowacourse/woowacourse-docs/blob/master/maincourse/README.md)
* [온라인 코드 리뷰 과정](https://github.com/woowacourse/woowacourse-docs/blob/master/maincourse/README.md)

## 요구사항

### 1단계

#### 요구사항 1
- [x] http://localhost:8080/index.html 로 접속했을 때 webapp 디렉토리의 index.html 파일을 읽어 클라이언트에 응답한다.

#### 요구사항 2
- [x] 회원가입
- [x] http://localhost:8080/user/form.html 으로 이동해서 회원가입 할 수 있다.
```
/create?userId=javajigi&password=password&name=%EB%B0%95%EC%9E%AC%EC%84%B1&email=javajigi%40slipp.net
```
- [x] HTML과 URL을 비교해 보고 사용자가 입력한 값을 파싱해 model.User 클래스에 저장한다.
```
GET /user/create?userId=javajigi&password=password&name=%EB%B0%95%EC%9E%AC%EC%84%B1&email=javajigi%40slipp.net HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Accept: */*
```
#### 요구사항 3
- [x] 회원가입 기능을 post 요청으로 바꾸어도 정상적으로 동작하도록 구현한다.
```
POST /user/create HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Content-Length: 59
Content-Type: application/x-www-form-urlencoded
Accept: */*

userId=javajigi&password=password&name=%EB%B0%95%EC%9E%AC%EC%84%B1&email=javajigi%40slipp.net
```

#### 요구사항 4
- [x] 회원가입을 완료한 후 index.html로 이동해야 한다.
```
'회원가입'을 완료하면 /index.html 페이지로 이동하고 싶다.
현재는 URL이 /user/create 로 유지되는 상태로 읽어서 전달할 파일이 없다.
따라서 redirect 방식처럼 회원가입을 완료한 후 “index.html”로 이동해야 한다.
즉, 브라우저의 URL이 /index.html로 변경해야 한다.
```

#### 요구사항 5
- [x] stylesheet 파일을 지원하도록 구현
```
GET ./css/style.css HTTP/1.1
Host: localhost:8080
Accept: text/css,*/*;q=0.1
Connection: keep-alive
```

### 🚀 2단계 - HTTP 웹 서버 리팩토링

#### WAS 기능 요구사항
- [x] 다수의 사용자 요청에 대해 Queue에 저장한 후 순차적으로 처리가 가능하도록 해야 한다.
- [x] 서버가 모든 요청에 대해 Thread를 매번 생성하는 경우 성능상 문제가 발생할 수 있다.
Thread Pool을 적용해 일정 수의 사용자 동시에 처리가 가능하도록 한다.

#### HTTP 요청/응답 처리 기능
역할을 분리해 재사용 가능하도록 한다.
- [x] HTTP 요청 Header/Body
- [x] HTTP 응답 Header/Body

#### 다형성을 활용해 분기처리 제거
- [x] 요청과 응답에 대한 처리를 추상화
19 changes: 19 additions & 0 deletions notebook/Servlet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Servlet
## 1. 서블릿 동작과정
1. HTTP Request를 Servlet Container에 보낸다.
2. Servlet Container는 HttpServletRequest, HttpServletResponse를 생성한다.
3. 사용자가 요청한 URL을 석하여 어느 서블릿에 대한 요청인지 찾는다
4. 컨테이너는 서블릿 service() 메소드를 호출하며, POST/GET 여부에 따라 doGet() 또는 doPost()가 호출된다.
5. doGet() 이나 doPost() 메소드는 동적인 페이지를 생성한 후 HttpServletResponse 객체에 응답을 보낸다.
6. 응답이 완료되면 HttpServletRequest, HttpServletResponse 두 객체를 소멸시킨다.

## 2. 종류
1. Servlet
서블릿 프로그램을 개발할 때 반드시 구현해야하는 메서드를 선언하고 있는 인터페이스
2. GenericServlet
Servlet 인터페이스를 상속하여 필요한 기능을 구현한 추상 클래스.
service() 메서드를 제외한 모든 메서드를 재정의하여 적절한 기능으로 구현.
GenericServlet 클래스를 상속하면 애플리케이션의 프로토콜에 따라 메서드 재정의 구문을 적용해야 함
3. HttpServlet
GenericServlet을 상속받아 service를 HTTP 프로토콜 요청 메서드에 적합하게 재구현해놓음
이미 DELETE,GET,HEAD,OPTIONS,POST,PUT,TRACE를 처리하는 메서드가 모두 정의되어 있다.
81 changes: 81 additions & 0 deletions notebook/ThreadPool.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# ThreadPool
> 다수의 사용자 요청에 대해 Queue에 저장한 후 순차적으로 처리가 가능하도록 해야 한다.
서버가 모든 요청에 대해 Thread를 매번 생성하는 경우 성능상 문제가 발생할 수 있다.
Thread Pool을 적용해 일정 수의 사용자 동시에 처리가 가능하도록 한다.

## 1. Thread Pool의 정의 및 필요성
자바의 쓰레드는 CPU를 최대한 사용해서 많은 업무를 동시에 처리할 수 있도록 도와준다.
그런데 이 쓰레드의 생성에는 약간의 시간과 메모리가 필요하다.
JVM은 쓰레드의 생성 개수를 제약하지 않기 때문에 계속해서 쓰레드를 생성하게 된다면 결과적으로 성능저하와 메모리 고갈 문제가 생길 수 있다.
이렇게 무제한적인 쓰레드의 생성을 막기 위해서 쓰레드풀이라는 쓰레드 관리 방식이 사용되고 있다.
쓰레드 풀이란 쓰레드를 이용된 갯수 안에서만 사용할 수 있도록 스스로 제약하는 방식이다.

쓰레드풀 방식을 사용하는 대표적인 소프트웨어 : Tomcat과 같은 웹서버들
웹서버들은 동시에 많기는 수천~수만의 요청이 들어올 수 있는데, 그 때마다 쓰레드를 생성하는 것은 JVM의 메모리를 급속하게
소비시키며 성능에도 저하가 발생하게 된다. 이럴 때 최대 쓰레드 개수를 지정해놓고, 스레드 개수 이상의 HTTP 요청에 대해서는 처리하지 않고
기다렸다가, 놀고있는 스레드가 생기면 그 때 Http 요청을 스레드를 통해 처리한다.

이렇게 되면, 현재의 작업을 수행하기 위해 이전에 생성된 스레드 풀을 재사용함으로써 사이클 오버헤드 및 자원 낭비를 막을 수 있다.

## 2. 적절한 Thread Pool
ThreadPoolExecutor는 Executors 클래스에 들어 있는 newCachedThreadPool, newFixedThreadPool, newScheduledThreadPool과 같은 팩토리 메소드에서 생성해주는 Executor에 대한 기본적인 내용이 구현되어 있는 클래스입니다.

- newFixedThreadPool : 주어진 스레드 개수만큼 생성하고 그 수를 유지, 생성된 스레드 중 일부가 종료되었으면 스레드를 다시 생성
- newCachedThreadPool : 처리할 스레드가 많아지면 그만큼 스레드를 증가(최대 스레드 개수 : Integaer.MAX_VALUE)
- newSingleThreadExecutor : 스레드를 하나만 생성(1개로 계속 유지)
- newScheduledThreadPool : 특정 시간 이후, 또는 주기적 작업 스레드 사용시 활용

ThreadPool은 위와 같이 4가지 종류가 있다. 어떤 ThreadPool을 이용해서 구현하는 것이 좋을까?
newCachedThreadPool : DDOS 공격이 일어나면 OOM이 일어나지 않을까?
newSingleThreadExecutor : 많은 요청을 하나의 스레드로 처리한다면 성능상 문제가 발생할 것임
newScheduledThreadPool : 요청이 언제 발생할지 모르는 서비스에서는 사용하기 부적합해보임
따라서 newFixedThreadPool을 이용한다.

## 3. 세부 구현
### 적절한 ThreadPool Size
스레드풀의 가장 이상적인 크기는 스레드 풀에서 실행할 작업의 종류와 스레드풀을 활용할 애플리케이션에 특성에 따라 결정이 됨
- 스레드풀의 크기가 너무 큰 경우 : 스레드는 CPU나 메모리 등의 자원을 조금이라도 더 확보하기 위해 경쟁하게 될 것이다.
그러다보면 CPU에는 부하가 걸리고 메모리는 모자라 금방 자원 부족에 시달리게 될 것이다
- 스레드풀의 크기가 너무 작은 경우 : 작업량은 계속해서 쌓이는데 CPU나 메모리는 남아돌면서 작업 처리 속도가 떨어질 수 있습니다.

스레드 풀의 사이즈를 조정하기 위해서는 주어진 환경의 제약 사항을 확실히 이해해야 한다.
> 애플리케이션을 실제로 탑재해 동작할 하드웨어에 CPU가 몇 개나 꽂혀 있는지?
메모리는 얼마나 꽂혀 있는지?
실행하는 작업이 CPU 연산을 많이 하는지 아니면 I/O 작업을 많이 하는지?
아니면 CPU와 I/O 작업을 비슷하게 많이 사용하는지?
그다지 많이 확보할 수 없는 JDBC 연결과 같은 자원을 얼마나 사용하는지?



스레드가 DB의존적인 작업을 할 경우 DB의 Connection Pool Size와 적절히 맞추어 설정해야 효과를 볼 수 있을 것이다.

지금 이 프로그램은 DB를 따로 연결해두지 않았으니 하지만 지금은 CPU로만 생각을 해보자
java로 CPU 개수 구하는 법
int numOfCores = Runtime.getRuntime().availableProcessors();
8이 나온다.
Brian Goetz의 유명한 책인 "Java Concurrency in Practice"에서는
스레드 수 = 사용 가능한 코어 수 * (1+대기 시간/서비스 시간)
대기시간 : 작업 하나가 완료되기까지 소모되는 시간
서비스 시간 : 작업이 실제로 동작 중인 시간
즉 스레드수 = 8 * (1+대기시간/서비스시간)

### 작업 처리 요청
작업 처리 요청이란 ExecutorService의 작업 큐에 Runnable 또는 Callable 객체를 넣는 행위를 말합니다.
- execute() : Runnable을 작업 큐에 저장, 작업 처리 결과를 받지 못함
- submit() : Runnable 또는 Callable를 작업 큐에 저장, 리턴된 Future를 통해 작업 처리 결과를 얻을 수 있음

execute()는 작업 처리 도중에 예외가 발생하면 스레드가 종료되고 해당 스레드는 스레드 풀에서 제거됨.
따라서 스레드풀은 다른 작업 처리를 위해 새로운 스레드를 생성해야함
submit()은 작업 처리 도중 예외가 발생하더라도 스레드는 종료되지 않고 다음 작업을 위해 재사용됨.
그렇기 때문에 가급적이면 스레드의 오버헤드를 줄이기 위해 submit()을 사용하는 것이 좋음

-> 작업 통보 완료가 필요 없어도 submit을 쓰는 게 더 성능에 좋은가?

### Thread Pool 종료
스레드풀의 스레드는 기본적으로 main스레드가 종료되더라도 작업을 처리하기 위해 계속 실행 상태로 남아있습니다.

- shutdown() : 현재 처리 중인 작업뿐만 아니라 작업 큐에 대기하고 있는 모든 작업을 처리한 뒤에 스레드 풀을 종료시킨다.
- shutdownNow() : 현재 작업 처리 중인 스레드를 interrupt해서 작업 중지를 시도하고 스레드풀을 종료시킨다.
리턴값은 작업 큐에 있는 미처리된 작업의 목록이다(List<Runnable>)
- awaitTermination() : shutdown()메소드 호출 이후, 모든 작업을 timeout 시간 내에 완료하면 true를 리턴하고
완료하지 못하면 작업 처리중인 스레드를 interrupt하고 false를 리턴합니다.
12 changes: 12 additions & 0 deletions src/main/java/annotation/RequestMapping.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface RequestMapping {
String path();
}
34 changes: 34 additions & 0 deletions src/main/java/controller/AbstractController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package controller;

import http.HttpRequest;
import http.HttpResponse;

public abstract class AbstractController implements HttpServlet {
@Override
public void service(HttpRequest httpRequest, HttpResponse httpResponse) {
switch (httpRequest.getMethod()) {
case GET:
doGet(httpRequest, httpResponse);
break;
case POST:
doPost(httpRequest, httpResponse);
break;
case DELETE:
doDelete(httpRequest, httpResponse);
break;
case PUT:
doPut(httpRequest, httpResponse);
break;
default:
throw new IllegalArgumentException("잘못된 요청입니다.");
}
}

protected abstract void doGet(HttpRequest httpRequest, HttpResponse httpResponse);

protected abstract void doPost(HttpRequest httpRequest, HttpResponse httpResponse);

protected abstract void doDelete(HttpRequest httpRequest, HttpResponse httpResponse);

protected abstract void doPut(HttpRequest httpRequest, HttpResponse httpResponse);
}
33 changes: 33 additions & 0 deletions src/main/java/controller/DispatcherServlet.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package controller;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

import annotation.RequestMapping;
import http.HttpRequest;
import http.HttpResponse;

public class DispatcherServlet implements HttpServlet {
private static final Map<String, HttpServlet> SERVLET_MATCHER;

static {
List<HttpServlet> servlets = Arrays.asList(new UserController());
SERVLET_MATCHER = servlets.stream()
.collect(Collectors.toMap(
servlet -> servlet.getClass().getAnnotation(RequestMapping.class).path(),
servlet -> servlet));
}

@Override
public void service(HttpRequest httpRequest, HttpResponse httpResponse) {
if (!SERVLET_MATCHER.containsKey(httpRequest.getURI())) {
httpResponse.notFound();
return;
}
HttpServlet servlet = SERVLET_MATCHER.get(httpRequest.getURI());
servlet.service(httpRequest, httpResponse);
}
}
8 changes: 8 additions & 0 deletions src/main/java/controller/HttpServlet.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package controller;

import http.HttpRequest;
import http.HttpResponse;

public interface HttpServlet {
void service(HttpRequest httpRequest, HttpResponse httpResponse);
}
48 changes: 48 additions & 0 deletions src/main/java/controller/UserController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package controller;

import static http.HttpMethod.*;

import java.util.Arrays;
import java.util.List;

import annotation.RequestMapping;
import db.DataBase;
import http.HttpBody;
import http.HttpMethod;
import http.HttpRequest;
import http.HttpResponse;
import http.HttpStatus;
import model.User;

@RequestMapping(path = "/user")
public class UserController extends AbstractController {

Choose a reason for hiding this comment

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

지원되는 메서드만 남기고 지원되지 않는 메서드는 굳이 오버라이드 되지 않는 구조를 만들어 보는게 좋을것 같습니다~!

private static final List<HttpMethod> ALLOWED_METHODS = Arrays.asList(GET, DELETE, PUT);

@Override
protected void doGet(HttpRequest httpRequest, HttpResponse httpResponse) {
httpResponse.setMethodNotAllowed(ALLOWED_METHODS);
}

@Override
protected void doPost(HttpRequest httpRequest, HttpResponse httpResponse) {
HttpBody httpBody = httpRequest.getBody();
User user = new User(
httpBody.get("userId"),
httpBody.get("password"),
httpBody.get("name"),
httpBody.get("email"));
DataBase.addUser(user);
httpResponse.setStatus(HttpStatus.FOUND);
httpResponse.addHeader("Location", "/index.html");
}

@Override
protected void doDelete(HttpRequest httpRequest, HttpResponse httpResponse) {
httpResponse.setMethodNotAllowed(ALLOWED_METHODS);
}

@Override
protected void doPut(HttpRequest httpRequest, HttpResponse httpResponse) {
httpResponse.setMethodNotAllowed(ALLOWED_METHODS);
}
}
41 changes: 41 additions & 0 deletions src/main/java/http/ContentType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package http;

import java.util.Objects;

public enum ContentType {
HTML("html", "text/html;charset=utf-8"),
CSS("css", "text/css;charset=utf-8"),
JS("js", "text/javascript;charset=utf-8"),
ICO("ico", "image/icon"),
PNG("png", "image/jpeg"),
TTF("ttf", "application/font-ttf"),
WOFF("woff", "application/font-woff");

private String extension;
private String contentType;

ContentType(String extension, String contentType) {
this.extension = extension;
this.contentType = contentType;
}

public static ContentType findByURI(String uri) {
if (Objects.isNull(uri) || uri.isEmpty()) {
throw new IllegalArgumentException("잘못된 리소스입니다.");
}
String extension = uri.substring(uri.lastIndexOf(".") + 1);
return findByExtension(extension);
}

private static ContentType findByExtension(String extension) {
try {
return valueOf(extension.toUpperCase());
} catch (IllegalArgumentException | NullPointerException e) {
throw new IllegalArgumentException("잘못된 확장자입니다.");
}
}

public String getContentType() {
return contentType;
}
}
Loading