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, 4단계] 민트(유재민) 미션 제출합니다. #462

Merged
merged 34 commits into from
Sep 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
3f0294c
refactor: HttpHeaders 클래스 구현
yujamint Sep 8, 2023
8305190
refactor: RequestLine 클래스 구현
yujamint Sep 8, 2023
0d43834
refactor: HttpRequest 클래스 구현
yujamint Sep 9, 2023
cb4b214
refactor: HttpRequest.requestUri 변수명 path로 변경
yujamint Sep 9, 2023
e0a48ae
refactor: StatusLine 클래스 구현
yujamint Sep 9, 2023
3ac7fc4
refactor: HttpStatus 열거형 구현
yujamint Sep 9, 2023
49c7bc1
refactor: HeaderValue 클래스 구현
yujamint Sep 9, 2023
f7f4b30
refactor: HttpResponse 클래스 구현
yujamint Sep 9, 2023
68e6b8b
test: 테스트에서의 expected 값 일부 조정
yujamint Sep 9, 2023
f93afb6
refactor: StatusLine 클래스 제거와 그에 따른 HttpResponse 코드 수정
yujamint Sep 9, 2023
a4e4859
refactor: HttpRequest 정적 팩토리 메서드 삭제
yujamint Sep 9, 2023
75cc474
refactor: Controller 구현
yujamint Sep 9, 2023
0c52760
refactor: 쓰이지 않는 클래스 제거
yujamint Sep 9, 2023
33143c2
test: 스레드 학습 테스트 작성
yujamint Sep 10, 2023
14f4173
feat: 스레드 풀 적용
yujamint Sep 10, 2023
f10daee
feat: 동시성 컬렉션 사용
yujamint Sep 10, 2023
e9eb960
refactor: 오타 수정
yujamint Sep 11, 2023
47be420
refactor: Http Request를 읽어들이는 클래스(HttpRequestReader) 분리
yujamint Sep 11, 2023
b9c41d4
refactor: HttpResponse의 ContentType을 구하는 역할 HttpRequest 객체에 위임
yujamint Sep 11, 2023
ce51f43
refactor: ContentType 관련 변수 enum 구현
yujamint Sep 11, 2023
a734474
refactor: FileReader 구현
yujamint Sep 11, 2023
5fe3478
fix: HttpStatus message 필드 추가
yujamint Sep 11, 2023
632d976
refactor: RequestLine 상수명 변경
yujamint Sep 11, 2023
eb08f63
refactor: RequestLine 상수,변수명 변경
yujamint Sep 11, 2023
7da6384
refactor: HeaderValue 팩토리 메서드 분기 처리 개선 및 상수 사용
yujamint Sep 11, 2023
c09577b
refactor: 헤더 조회 메서드명 개선
yujamint Sep 11, 2023
d0be660
refactor: HttpHeadersType enum 구현에 따른 플레인 텍스트 제거
yujamint Sep 11, 2023
4ce0385
refactor: 존재하지 않는 계정에 대한 예외 처리 추가
yujamint Sep 11, 2023
718dc00
refactor: 세션 저장 로직 개선
yujamint Sep 11, 2023
281c806
refactor: existsByAccount 메서드 제거
yujamint Sep 11, 2023
f27518e
refactor: 리터럴 변수 상수화
yujamint Sep 11, 2023
405a942
refactor: RequestMapping 객체 구현
yujamint Sep 12, 2023
f252d96
refactor: QueryString과 Path 구분하는 Path 객체 구현
yujamint Sep 12, 2023
f8b86c9
refactor: 매직 넘버 상수화
yujamint Sep 12, 2023
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
2 changes: 1 addition & 1 deletion study/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ handlebars:
server:
tomcat:
accept-count: 1
max-connections: 1
max-connections: 2
threads:
max: 2
2 changes: 1 addition & 1 deletion study/src/test/java/thread/stage0/SynchronizationTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ private static final class SynchronizedMethods {

private int sum = 0;

public void calculate() {
public synchronized void calculate() {
setSum(getSum() + 1);
}

Expand Down
10 changes: 6 additions & 4 deletions study/src/test/java/thread/stage0/ThreadPoolsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,24 +29,26 @@ void testNewFixedThreadPool() {
executor.submit(logWithSleep("hello fixed thread pools"));
executor.submit(logWithSleep("hello fixed thread pools"));
executor.submit(logWithSleep("hello fixed thread pools"));
executor.submit(logWithSleep("hello fixed thread pools"));
executor.submit(logWithSleep("hello fixed thread pools"));

// 올바른 값으로 바꿔서 테스트를 통과시키자.
final int expectedPoolSize = 0;
final int expectedQueueSize = 0;
final int expectedPoolSize = 2;
final int expectedQueueSize = 3;

assertThat(expectedPoolSize).isEqualTo(executor.getPoolSize());
assertThat(expectedQueueSize).isEqualTo(executor.getQueue().size());
}

@Test
void testNewCachedThreadPool() {
void testNewCachedThreadPool() throws InterruptedException {
final var executor = (ThreadPoolExecutor) Executors.newCachedThreadPool();
executor.submit(logWithSleep("hello cached thread pools"));
executor.submit(logWithSleep("hello cached thread pools"));
executor.submit(logWithSleep("hello cached thread pools"));

// 올바른 값으로 바꿔서 테스트를 통과시키자.
final int expectedPoolSize = 0;
final int expectedPoolSize = 3;
final int expectedQueueSize = 0;

assertThat(expectedPoolSize).isEqualTo(executor.getPoolSize());
Expand Down
4 changes: 3 additions & 1 deletion study/src/test/java/thread/stage1/ConcurrencyTest.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package thread.stage1;

import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
Expand All @@ -17,7 +18,8 @@
*/
class ConcurrencyTest {

@Test
// @Test
@RepeatedTest(2000)
void test() throws InterruptedException {
final var userServlet = new UserServlet();

Expand Down
2 changes: 1 addition & 1 deletion study/src/test/java/thread/stage1/UserServlet.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public void service(final User user) {
join(user);
}

private void join(final User user) {
private synchronized void join(final User user) {
if (!users.contains(user)) {
users.add(user);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,5 @@ public static Optional<User> findByAccount(String account) {
return Optional.ofNullable(database.get(account));
}

public static boolean existsByAccount(String account) {
return database.containsKey(account);
}

private InMemoryUserRepository() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,27 @@
import java.io.UncheckedIOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Connector implements Runnable {

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

private static final int DEFAULT_PORT = 8080;
private static final int DEFAULT_ACCEPT_COUNT = 100;
private static final int DEFAULT_MAX_THREADS = 200;

private final ServerSocket serverSocket;
private final ExecutorService executorService;
private boolean stopped;

public Connector() {
this(DEFAULT_PORT, DEFAULT_ACCEPT_COUNT);
this(DEFAULT_PORT, DEFAULT_ACCEPT_COUNT, DEFAULT_MAX_THREADS);
}

public Connector(final int port, final int acceptCount) {
public Connector(final int port, final int acceptCount, final int maxThreads) {
executorService = Executors.newFixedThreadPool(maxThreads);
this.serverSocket = createServerSocket(port, acceptCount);
this.stopped = false;
}
Expand Down Expand Up @@ -67,7 +72,7 @@ private void process(final Socket connection) {
return;
}
var processor = new Http11Processor(connection);
new Thread(processor).start();
executorService.execute(processor);
}

public void stop() {
Expand Down
218 changes: 26 additions & 192 deletions tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java
Original file line number Diff line number Diff line change
@@ -1,37 +1,36 @@
package org.apache.coyote.http11;

import nextstep.jwp.db.InMemoryUserRepository;
import nextstep.jwp.exception.UncheckedServletException;
import nextstep.jwp.model.User;
import org.apache.coyote.Processor;
import org.apache.coyote.http11.common.FileReader;
import org.apache.coyote.http11.controller.AuthController;
import org.apache.coyote.http11.controller.Controller;
import org.apache.coyote.http11.controller.DefaultController;
import org.apache.coyote.http11.controller.RegisterController;
import org.apache.coyote.http11.controller.RequestMapping;
import org.apache.coyote.http11.request.HttpRequest;
import org.apache.coyote.http11.response.HttpResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UncheckedIOException;
import java.net.Socket;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.apache.coyote.http11.common.HttpHeaderType.CONTENT_LENGTH;
import static org.apache.coyote.http11.common.HttpHeaderType.CONTENT_TYPE;

public class Http11Processor implements Runnable, Processor {

private static final Logger log = LoggerFactory.getLogger(Http11Processor.class);
private static final String SAFARI_CHROME_ACCEPT_HEADER_DEFAULT_VALUE = "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8";

private final Socket connection;
private final SessionManager sessionManager = new SessionManager();
private final HttpRequestReader requestReader = new HttpRequestReader();
private final RequestMapping requestMapping = new RequestMapping(
new AuthController(),
new RegisterController(),
new DefaultController()
);

public Http11Processor(final Socket connection) {
this.connection = connection;
Expand All @@ -51,186 +50,21 @@ public void process(final Socket connection) {
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);

List<String> headers = new ArrayList<>();

String header = "";
while (!(header = bufferedReader.readLine()).equals("")) {
headers.add(header);
}
String[] splitStatusLine = Objects.requireNonNull(headers.get(0)).split(" ");
String requestMethod = splitStatusLine[0];
String requestUri = splitStatusLine[1];

String requestAcceptHeader = findHeader("Accept", headers);
String contentTypeHeader = getContentTypeHeaderFrom(requestAcceptHeader);

RequestHandler requestHandler;
if (requestMethod.equalsIgnoreCase("POST")) {
int contentLength = getContentLength(headers);
String requestBody = readRequestBody(bufferedReader, contentLength);
requestHandler = handlePostRequest(requestUri, requestBody);
} else {
String cookieHeader = findHeader("Cookie", headers);
requestHandler = handleGetRequest(requestMethod, requestUri, cookieHeader);
}
HttpRequest httpRequest = requestReader.readHttpRequest(bufferedReader);
HttpResponse httpResponse = new HttpResponse(httpRequest.httpVersion());

String responseBody = readFile(requestHandler.getResponseFilePath());
Controller controller = requestMapping.getController(httpRequest);
controller.service(httpRequest, httpResponse);

List<String> responseHeaders = new ArrayList<>();
responseHeaders.add("HTTP/1.1 " + requestHandler.getHttpStatus() + " ");
responseHeaders.add(contentTypeHeader);
responseHeaders.add("Content-Length: " + responseBody.getBytes().length + " ");
for (Entry<String, String> headerEntry : requestHandler.getHeaders().entrySet()) {
responseHeaders.add(headerEntry.getKey() + ": " + headerEntry.getValue());
}
String responseHeader = String.join("\r\n", responseHeaders);
httpResponse.setBody(FileReader.readFile(httpResponse.getResponseFileName()));
httpResponse.addHeader(CONTENT_LENGTH.getValue(), String.valueOf(httpResponse.getBody().getBytes().length));
httpResponse.addHeader(CONTENT_TYPE.getValue(), httpRequest.getContentTypeByAcceptHeader());
Comment on lines +53 to +61

Choose a reason for hiding this comment

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

확실히 훨씬 간결해졌네요!👍


BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream);

var response = String.join("\r\n", responseHeader, "", responseBody);

bufferedOutputStream.write(response.getBytes());
bufferedOutputStream.write(httpResponse.format().getBytes());
bufferedOutputStream.flush();
} catch (IOException | UncheckedServletException e) {
} catch (Exception e) {

Choose a reason for hiding this comment

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

왜 Exception으로 변경하셨을까용?

Copy link
Member Author

Choose a reason for hiding this comment

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

Controller.service()에서 발생할 수 있는 에러로 여러 종류가 있을 거라고 생각했습니다.
발생할 수 있는 에러를 커스텀 예외로 추상화해서 catch 했다면 좋았을 것 같지만, 당장은 모든 예외를 잡을 수 있는 Exception으로 변경해서 개발을 빠르게 하고자 했네요 ㅋㅋㅋ..

log.error(e.getMessage(), e);
}
}

private static String findHeader(String key, List<String> headers) {
return headers.stream()
.filter(it -> it.startsWith(key + ": "))
.findFirst()
.orElse("Accept: " + SAFARI_CHROME_ACCEPT_HEADER_DEFAULT_VALUE);
}

private static String getContentTypeHeaderFrom(String requestAcceptHeader) {
String[] splitAcceptHeader = requestAcceptHeader.split(" ");
String headerValue = splitAcceptHeader[1];
String[] acceptTypes = headerValue.split(";");
String[] splitAcceptTypes = acceptTypes[0].split(",");

if (Arrays.asList(splitAcceptTypes).contains("text/css")) {
return "Content-Type: text/css;charset=utf-8 ";
}
return "Content-Type: text/html;charset=utf-8 ";
}

private int getContentLength(List<String> headers) {
Optional<String> contentLengthHeader = headers.stream()
.filter(it -> it.startsWith("Content-Length"))
.findFirst();

if (contentLengthHeader.isEmpty()) {
return -1;
}
int index = contentLengthHeader.get().indexOf(" ");
return Integer.parseInt(contentLengthHeader.get().substring(index + 1));
}

private static String readRequestBody(BufferedReader bufferedReader, int contentLength) throws IOException {
char[] buffer = new char[contentLength];
bufferedReader.read(buffer, 0, contentLength);
return new String(buffer);
}

private RequestHandler handlePostRequest(String requestUri, String requestBody) {
String[] splitRequestBody = requestBody.split("&");
if (requestUri.equals("/login")) {
return handleLoginRequest(splitRequestBody);
}
if (requestUri.equals("/register")) {
return handleRegisterRequest(splitRequestBody);
}
return RequestHandler.of("GET", "404 Not Found", "static/404.html");
}

private RequestHandler handleLoginRequest(String[] splitQueryString) {
Optional<String> account = getValueOf("account", splitQueryString);
Optional<String> password = getValueOf("password", splitQueryString);

if (account.isEmpty() || password.isEmpty()) {
return RequestHandler.of("GET", "400 Bad Request", "static/401.html");
}

Optional<User> findUser = InMemoryUserRepository.findByAccount(account.get());
if (findUser.isPresent() && findUser.get().checkPassword(password.get())) {
User user = findUser.get();
log.info(user.toString());
Session session = new Session(UUID.randomUUID().toString());
session.setAttribute("user", user);
sessionManager.add(session);
RequestHandler requestHandler = RequestHandler.of("GET", "302 Found", "static/index.html");
requestHandler.addHeader("Set-Cookie", "JSESSIONID=" + session.getId());
return requestHandler;
}
return RequestHandler.of("GET", "401 Unauthorized", "static/401.html");
}

private Optional<String> getValueOf(String key, String[] splitQueryString) {
return Arrays.stream(splitQueryString)
.filter(it -> equalsKey(key, it))
.map(it -> it.substring(it.indexOf("=") + 1))
.findFirst();
}

private boolean equalsKey(String expected, String actual) {
String[] splitActual = actual.split("=");
return splitActual[0].equals(expected);
}

private RequestHandler handleRegisterRequest(String[] splitQueryString) {
Optional<String> account = getValueOf("account", splitQueryString);
Optional<String> email = getValueOf("email", splitQueryString);
Optional<String> password = getValueOf("password", splitQueryString);

if (account.isEmpty() || email.isEmpty() || password.isEmpty()) {
return RequestHandler.of("GET", "400 Bad Request", "static/register.html");
}

InMemoryUserRepository.save(new User(account.get(), password.get(), email.get()));
return RequestHandler.of("GET", "302 Found", "static/index.html");
}

private RequestHandler handleGetRequest(String requestMethod, String requestUri, String cookie) throws IOException {
if (!requestMethod.equalsIgnoreCase("GET")) {
throw new IllegalArgumentException("GET 요청만 처리 가능합니다.");
}

if (requestUri.equals("/login.html") || requestUri.equals("/login")) {
return handleLoginPageRequest(cookie);
}

String fileName = "static" + requestUri;
return RequestHandler.of("GET", "200 OK", fileName);
}

private RequestHandler handleLoginPageRequest(String cookie) throws IOException {
Optional<String> sessionId = getSessionId(cookie);
if (sessionId.isEmpty()) {
return RequestHandler.of("GET", "200 OK", "static/login.html");
}
Session session = sessionManager.findSession(sessionId.get());
User user = getUser(session);
if (InMemoryUserRepository.existsByAccount(user.getAccount())) {
return RequestHandler.of("GET", "302 Found", "static/index.html");
}
return RequestHandler.of("GET", "200 OK", "static/login.html");
}

private Optional<String> getSessionId(String cookieHeader) {
String[] splitCookie = cookieHeader.split(" ");
return getValueOf("JSESSIONID", splitCookie);
}

private User getUser(Session session) {
return (User) session.getAttribute("user");
}

private String readFile(String filePath) {
try (Stream<String> lines = Files.lines(Paths.get(filePath))) {
return lines.collect(Collectors.joining("\n", "", "\n"));
} catch (IOException | UncheckedIOException e) {
return "Hello world!";
}
}
}
Loading