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 16 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 @@ -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
205 changes: 44 additions & 161 deletions tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
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.controller.AuthController;
import org.apache.coyote.http11.controller.RegisterController;
import org.apache.coyote.http11.request.HttpHeaders;
import org.apache.coyote.http11.request.HttpRequest;
import org.apache.coyote.http11.request.RequestLine;
import org.apache.coyote.http11.response.HttpResponse;
import org.apache.coyote.http11.response.HttpStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -16,22 +20,18 @@
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;

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 AuthController authController = new AuthController();
private final RegisterController registerController = new RegisterController();

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

List<String> headers = new ArrayList<>();
HttpRequest httpRequest = readHttpRequest(bufferedReader);
HttpResponse httpResponse = new HttpResponse(httpRequest.httpVersion());

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);
if (httpRequest.path().equals("/login") || httpRequest.path().equals("/login.html")) {
authController.service(httpRequest, httpResponse);
} else if (httpRequest.path().equals("/register") || httpRequest.path().equals("/register.html")) {
registerController.service(httpRequest, httpResponse);
} else {
String cookieHeader = findHeader("Cookie", headers);
requestHandler = handleGetRequest(requestMethod, requestUri, cookieHeader);
httpResponse.setHttpStatus(HttpStatus.OK).setResponseFileName(httpRequest.path());
}

String responseBody = readFile(requestHandler.getResponseFilePath());

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(readFile(httpResponse.getResponseFileName()));
httpResponse.addHeader("Content-Length", String.valueOf(httpResponse.getBody().getBytes().length));
httpResponse.addHeader("Content-Type", getContentTypeHeaderFrom(httpRequest));

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 HttpRequest readHttpRequest(BufferedReader bufferedReader) throws IOException {
List<String> lines = readRequesteHeaders(bufferedReader);
RequestLine requestLine = RequestLine.from(lines.get(0));
HttpHeaders httpHeaders = HttpHeaders.from(lines.subList(1, lines.size()));
String requestBody = readRequestBody(bufferedReader, httpHeaders.contentLength());
return new HttpRequest(requestLine, httpHeaders, requestBody);
}

Choose a reason for hiding this comment

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

이 메서드부터 시작해서 요청을 읽고 요청라인, 헤더, 바디를 읽는 부분은 HttpRequest가 갖고있는게 어떨까요?
Http11Processor의 책임일지 HttpRequest의 책임에 더 가까울지 생각해보면 좋을 것 같아요.

Copy link
Member Author

Choose a reason for hiding this comment

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

리뷰를 보고 생각하니 확실히 책임이 분리되는 것도 괜찮을 거 같더라고요.
대신 BufferedReader를 통해 직접 읽어들이는 것보다 '읽기'만의 책임을 갖는 객체가 따로 있는 게 좋을 것 같다고 생각했습니다.
HttpRequestReader를 구현하는 방향으로 리팩토링 했으니 혹시나 별로라면 말씀해 주세요~


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 ";
private List<String> readRequesteHeaders(BufferedReader bufferedReader) throws IOException {
List<String> lines = new ArrayList<>();
String line = "";
while (!(line = bufferedReader.readLine()).equals("")) {
lines.add(line);
}
return "Content-Type: text/html;charset=utf-8 ";
return lines;
}
yujamint marked this conversation as resolved.
Show resolved Hide resolved

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

if (contentLengthHeader.isEmpty()) {
return -1;
private static String getContentTypeHeaderFrom(HttpRequest httpRequest) {
List<String> acceptHeaderValues = httpRequest.header("Accept");
if (acceptHeaderValues != null && acceptHeaderValues.contains("text/css")) {
return "text/css;charset=utf-8";
}
yujamint marked this conversation as resolved.
Show resolved Hide resolved
int index = contentLengthHeader.get().indexOf(" ");
return Integer.parseInt(contentLengthHeader.get().substring(index + 1));
return "text/html;charset=utf-8";
}

private static String readRequestBody(BufferedReader bufferedReader, int contentLength) throws IOException {
if (contentLength <= 0) {
return "";
}
char[] buffer = new char[contentLength];
bufferedReader.read(buffer, 0, contentLength);
return new String(buffer);
}
yujamint marked this conversation as resolved.
Show resolved Hide resolved

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) {
private String readFile(String fileName) {
String filePath = this.getClass().getClassLoader().getResource("static" + fileName).getPath();
try (Stream<String> lines = Files.lines(Paths.get(filePath))) {
return lines.collect(Collectors.joining("\n", "", "\n"));
} catch (IOException | UncheckedIOException e) {
yujamint marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
Loading