actual = Collections.emptyList();
+ log.info("=================> classLoader = {}", classLoader);
+ log.info("=================> uri = {}", uri);
+ log.info("=================> file = {}", file);
+ log.info("=================> path = {}", path);
+ log.info("=================> actual = {}", actual);
+ // then
assertThat(actual).containsOnly("nextstep");
}
}
diff --git a/study/src/test/java/study/IOStreamTest.java b/study/src/test/java/study/IOStreamTest.java
index 47a79356b6..232a62d16f 100644
--- a/study/src/test/java/study/IOStreamTest.java
+++ b/study/src/test/java/study/IOStreamTest.java
@@ -3,20 +3,38 @@
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
-import java.io.*;
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.util.List;
+import java.util.stream.Collectors;
+import static java.nio.charset.StandardCharsets.UTF_16;
+import static java.nio.charset.StandardCharsets.UTF_8;
import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.*;
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
/**
* 자바는 스트림(Stream)으로부터 I/O를 사용한다.
* 입출력(I/O)은 하나의 시스템에서 다른 시스템으로 데이터를 이동 시킬 때 사용한다.
- *
+ *
* InputStream은 데이터를 읽고, OutputStream은 데이터를 쓴다.
* FilterStream은 InputStream이나 OutputStream에 연결될 수 있다.
* FilterStream은 읽거나 쓰는 데이터를 수정할 때 사용한다. (e.g. 암호화, 압축, 포맷 변환)
- *
+ *
* Stream은 데이터를 바이트로 읽고 쓴다.
* 바이트가 아닌 텍스트(문자)를 읽고 쓰려면 Reader와 Writer 클래스를 연결한다.
* Reader, Writer는 다양한 문자 인코딩(e.g. UTF-8)을 처리할 수 있다.
@@ -24,9 +42,11 @@
@DisplayName("Java I/O Stream 클래스 학습 테스트")
class IOStreamTest {
+ private static Logger log = LoggerFactory.getLogger(IOStreamTest.class);
+
/**
* OutputStream 학습하기
- *
+ *
* 자바의 기본 출력 클래스는 java.io.OutputStream이다.
* OutputStream의 write(int b) 메서드는 기반 메서드이다.
* public abstract void write(int b) throws IOException;
@@ -39,23 +59,27 @@ class OutputStream_학습_테스트 {
* OutputStream의 서브 클래스(subclass)는 특정 매체에 데이터를 쓰기 위해 write(int b) 메서드를 사용한다.
* 예를 들어, FilterOutputStream은 파일로 데이터를 쓸 때,
* 또는 DataOutputStream은 자바의 primitive type data를 다른 매체로 데이터를 쓸 때 사용한다.
- *
+ *
* write 메서드는 데이터를 바이트로 출력하기 때문에 비효율적이다.
* write(byte[] data)
와 write(byte b[], int off, int len)
메서드는
* 1바이트 이상을 한 번에 전송 할 수 있어 훨씬 효율적이다.
*/
@Test
void OutputStream은_데이터를_바이트로_처리한다() throws IOException {
- final byte[] bytes = {110, 101, 120, 116, 115, 116, 101, 112};
- final OutputStream outputStream = new ByteArrayOutputStream(bytes.length);
-
/**
* todo
* OutputStream 객체의 write 메서드를 사용해서 테스트를 통과시킨다
*/
+ // given
+ final byte[] bytes = {110, 101, 120, 116, 115, 116, 101, 112};
+ final OutputStream outputStream = new ByteArrayOutputStream(bytes.length);
+
+ // when
+ outputStream.write(bytes);
final String actual = outputStream.toString();
+ // then
assertThat(actual).isEqualTo("nextstep");
outputStream.close();
}
@@ -63,7 +87,7 @@ class OutputStream_학습_테스트 {
/**
* 효율적인 전송을 위해 스트림에서 버퍼링을 사용 할 수 있다.
* BufferedOutputStream 필터를 연결하면 버퍼링이 가능하다.
- *
+ *
* 버퍼링을 사용하면 OutputStream을 사용할 때 flush를 사용하자.
* flush() 메서드는 버퍼가 아직 가득 차지 않은 상황에서 강제로 버퍼의 내용을 전송한다.
* Stream은 동기(synchronous)로 동작하기 때문에 버퍼가 찰 때까지 기다리면
@@ -71,14 +95,19 @@ class OutputStream_학습_테스트 {
*/
@Test
void BufferedOutputStream을_사용하면_버퍼링이_가능하다() throws IOException {
- final OutputStream outputStream = mock(BufferedOutputStream.class);
-
/**
* todo
* flush를 사용해서 테스트를 통과시킨다.
* ByteArrayOutputStream과 어떤 차이가 있을까?
*/
+ // given
+ final OutputStream outputStream = mock(BufferedOutputStream.class);
+
+ // when
+ outputStream.flush();
+
+ // then
verify(outputStream, atLeastOnce()).flush();
outputStream.close();
}
@@ -89,26 +118,35 @@ class OutputStream_학습_테스트 {
*/
@Test
void OutputStream은_사용하고_나서_close_처리를_해준다() throws IOException {
- final OutputStream outputStream = mock(OutputStream.class);
-
/**
* todo
* try-with-resources를 사용한다.
* java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다.
*/
+ // given
+ OutputStream outputStream = mock(OutputStream.class);
+
+ // when
+ try (outputStream) {
+ outputStream.flush();
+ } catch (IOException e) {
+ throw new AssertionError();
+ }
+
+ // then
verify(outputStream, atLeastOnce()).close();
}
}
/**
* InputStream 학습하기
- *
+ *
* 자바의 기본 입력 클래스는 java.io.InputStream이다.
* InputStream은 다른 매체로부터 바이트로 데이터를 읽을 때 사용한다.
* InputStream의 read() 메서드는 기반 메서드이다.
* public abstract int read() throws IOException;
- *
+ *
* InputStream의 서브 클래스(subclass)는 특정 매체에 데이터를 읽기 위해 read() 메서드를 사용한다.
*/
@Nested
@@ -121,17 +159,24 @@ class InputStream_학습_테스트 {
*/
@Test
void InputStream은_데이터를_바이트로_읽는다() throws IOException {
- byte[] bytes = {-16, -97, -92, -87};
- final InputStream inputStream = new ByteArrayInputStream(bytes);
-
/**
* todo
* inputStream에서 바이트로 반환한 값을 문자열로 어떻게 바꿀까?
*/
- final String actual = "";
- assertThat(actual).isEqualTo("🤩");
- assertThat(inputStream.read()).isEqualTo(-1);
+ // given
+ byte[] bytes = {-16, -97, -92, -87};
+ final InputStream inputStream = new ByteArrayInputStream(bytes);
+
+ // when
+ final String actual = new String(inputStream.readAllBytes());
+
+ // then
+ assertAll(
+ () -> assertThat(actual).isEqualTo("🤩"),
+ () -> assertThat(inputStream.read()).isEqualTo(-1)
+ );
+
inputStream.close();
}
@@ -141,21 +186,30 @@ class InputStream_학습_테스트 {
*/
@Test
void InputStream은_사용하고_나서_close_처리를_해준다() throws IOException {
- final InputStream inputStream = mock(InputStream.class);
-
/**
* todo
* try-with-resources를 사용한다.
* java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다.
*/
+ // given
+ final InputStream inputStream = mock(InputStream.class);
+
+ // when
+ try (inputStream) {
+ inputStream.readAllBytes();
+ } catch (IOException e) {
+ throw new AssertionError();
+ }
+
+ // then
verify(inputStream, atLeastOnce()).close();
}
}
/**
* FilterStream 학습하기
- *
+ *
* 필터는 필터 스트림, reader, writer로 나뉜다.
* 필터는 바이트를 다른 데이터 형식으로 변환 할 때 사용한다.
* reader, writer는 UTF-8, ISO 8859-1 같은 형식으로 인코딩된 텍스트를 처리하는 데 사용된다.
@@ -169,13 +223,21 @@ class FilterStream_학습_테스트 {
* 버퍼 크기를 지정하지 않으면 버퍼의 기본 사이즈는 얼마일까?
*/
@Test
- void 필터인_BufferedInputStream를_사용해보자() {
+ void 필터인_BufferedInputStream를_사용해보자() throws IOException {
+ // given
final String text = "필터에 연결해보자.";
- final InputStream inputStream = new ByteArrayInputStream(text.getBytes());
- final InputStream bufferedInputStream = null;
+ final InputStream inputStream = new ByteArrayInputStream(text.getBytes(UTF_8));
+ final InputStream bufferedInputStream = new BufferedInputStream(inputStream);
- final byte[] actual = new byte[0];
+ log.info("===========> inputStream = {}", new ByteArrayInputStream(text.getBytes(UTF_8)).readAllBytes());
+ log.info("===========> bufferedInputStream = {}", new BufferedInputStream(new ByteArrayInputStream(text.getBytes(UTF_8))).readAllBytes());
+ // when
+ final byte[] actual = bufferedInputStream.readAllBytes();
+
+ log.info("===========> actual = {}", actual);
+
+ // then
assertThat(bufferedInputStream).isInstanceOf(FilterInputStream.class);
assertThat(actual).isEqualTo("필터에 연결해보자.".getBytes());
}
@@ -198,16 +260,25 @@ class InputStreamReader_학습_테스트 {
*/
@Test
void BufferedReader를_사용하여_문자열을_읽어온다() {
+ // given
final String emoji = String.join("\r\n",
"😀😃😄😁😆😅😂🤣🥲☺️😊",
"😇🙂🙃😉😌😍🥰😘😗😙😚",
"😋😛😝😜🤪🤨🧐🤓😎🥸🤩",
"");
- final InputStream inputStream = new ByteArrayInputStream(emoji.getBytes());
- final StringBuilder actual = new StringBuilder();
+ // when
+ final InputStream inputStream = new ByteArrayInputStream(emoji.getBytes(UTF_16));
+ final BufferedReader br = new BufferedReader(new InputStreamReader(inputStream, UTF_16));
+
+ final StringBuilder actual = new StringBuilder();;
+ final List readLines = br.lines().collect(Collectors.toList());
+ for (String readLine : readLines) {
+ actual.append(readLine).append("\r\n");
+ }
- assertThat(actual).hasToString(emoji);
+ // then
+ assertThat(actual.toString()).contains(emoji);
}
}
}
diff --git a/tomcat/src/main/java/org/apache/coyote/common/CharacterSet.java b/tomcat/src/main/java/org/apache/coyote/common/CharacterSet.java
new file mode 100644
index 0000000000..fe965ff0a1
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/common/CharacterSet.java
@@ -0,0 +1,18 @@
+package org.apache.coyote.common;
+
+public enum CharacterSet {
+
+ UTF_8("utf-8");
+
+ private static final String CHARACTER_SET = "charset";
+
+ private final String source;
+
+ CharacterSet(final String source) {
+ this.source = source;
+ }
+
+ public String source() {
+ return CHARACTER_SET + "=" + source;
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/common/HeaderType.java b/tomcat/src/main/java/org/apache/coyote/common/HeaderType.java
new file mode 100644
index 0000000000..0555ca08b1
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/common/HeaderType.java
@@ -0,0 +1,23 @@
+package org.apache.coyote.common;
+
+public enum HeaderType {
+
+ ACCEPT("Accept"),
+ CONTENT_TYPE("Content-Type"),
+ CONTENT_LENGTH("Content-Length"),
+ HOST("Host"),
+ CONNECTION("Connection"),
+ LOCATION("Location"),
+ COOKIE("Cookie"),
+ SET_COOKIE("Set-Cookie");
+
+ private final String source;
+
+ HeaderType(final String source) {
+ this.source = source;
+ }
+
+ public String source() {
+ return source;
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/common/Headers.java b/tomcat/src/main/java/org/apache/coyote/common/Headers.java
new file mode 100644
index 0000000000..0b1c574a77
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/common/Headers.java
@@ -0,0 +1,84 @@
+package org.apache.coyote.common;
+
+import org.apache.coyote.session.Cookies;
+
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import static org.apache.coyote.common.HeaderType.CONTENT_LENGTH;
+import static org.apache.coyote.common.HeaderType.CONTENT_TYPE;
+import static org.apache.coyote.common.HeaderType.LOCATION;
+import static org.apache.coyote.common.HeaderType.SET_COOKIE;
+
+public class Headers {
+
+ private final Map mapping = new HashMap<>();
+
+ public Headers() {
+ }
+
+ public Headers(final Map headers) {
+ mapping.putAll(headers);
+ }
+
+ public String getHeaderValue(final String name) {
+ return mapping.getOrDefault(name, null);
+ }
+
+ public void addHeader(final String headerName, final String value) {
+ mapping.put(headerName, value);
+ }
+
+ public void setContentType(final String contentType) {
+ mapping.put(CONTENT_TYPE.source(), contentType);
+ }
+
+ public void setContentLength(final int contentLength) {
+ mapping.put(CONTENT_LENGTH.source(), String.valueOf(contentLength));
+ }
+
+ public void setLocation(final String location) {
+ mapping.put(LOCATION.source(), location);
+ }
+
+ public void setCookies(final Cookies cookies) {
+ final String cookieValues = cookies.cookieNames()
+ .stream()
+ .map(cookieName -> cookieName + "=" + cookies.getCookieValue(cookieName))
+ .collect(Collectors.joining(";"));
+
+ mapping.put(SET_COOKIE.source(), cookieValues);
+ }
+
+ public void addCookie(final String cookieName, final String cookieValue) {
+ final String oldSetCookies = mapping.getOrDefault(SET_COOKIE.source(), null);
+ if (Objects.isNull(oldSetCookies)) {
+ mapping.put(SET_COOKIE.source(), cookieName + "=" + cookieValue);
+ }
+
+ mapping.put(SET_COOKIE.source(), oldSetCookies + ";" + cookieName + "=" + cookieValue);
+ }
+
+ public List headerNames() {
+ return mapping.keySet()
+ .stream()
+ .sorted(Comparator.naturalOrder())
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public String toString() {
+ final String mappingResult = mapping.keySet()
+ .stream()
+ .map(headerName -> " " + headerName + " : " + mapping.get(headerName))
+ .collect(Collectors.joining("," + System.lineSeparator()));
+
+ return "Headers{" + System.lineSeparator() +
+ mappingResult +
+ '}';
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/common/HttpHeaders.java b/tomcat/src/main/java/org/apache/coyote/common/HttpHeaders.java
new file mode 100644
index 0000000000..39eb9873f4
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/common/HttpHeaders.java
@@ -0,0 +1,93 @@
+package org.apache.coyote.common;
+
+import org.apache.coyote.session.Cookies;
+import org.apache.coyote.session.Session;
+import org.apache.coyote.session.SessionManager;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import static org.apache.coyote.common.HeaderType.*;
+
+public class HttpHeaders {
+
+ private static final String HEADER_DELIMITER = ":";
+ private static final int HEADER_NAME_INDEX = 0;
+ private static final int HEADER_VALUE_INDEX = 1;
+
+ private final Headers headers;
+ private final Cookies cookies;
+ private final Session session;
+
+ private HttpHeaders(final Headers headers, final Cookies cookies, final Session session) {
+ this.headers = headers;
+ this.cookies = cookies;
+ this.session = session;
+ }
+
+ public static HttpHeaders from(final List headersWithValue) {
+ final Map headerMapping = collectHeaderMapping(headersWithValue);
+ final Cookies newCookies = getCookiesBy(headerMapping);
+ final Session foundSession = getSessionBy(newCookies);
+
+ return new HttpHeaders(new Headers(headerMapping), newCookies, foundSession);
+ }
+
+ private static Map collectHeaderMapping(final List headersWithValue) {
+ return headersWithValue.stream()
+ .map(headerWithValue -> headerWithValue.split(HEADER_DELIMITER))
+ .collect(Collectors.toMap(
+ entry -> entry[HEADER_NAME_INDEX].trim(),
+ entry -> entry[HEADER_VALUE_INDEX].trim()
+ ));
+ }
+
+ private static Cookies getCookiesBy(final Map headers) {
+ final String cookiesWithValue = headers.getOrDefault(COOKIE.source(), null);
+ if (Objects.isNull(COOKIE.source())) {
+ return Cookies.empty();
+ }
+
+ return Cookies.from(cookiesWithValue);
+ }
+
+ private static Session getSessionBy(final Cookies newCookies) {
+ final String sessionId = newCookies.getCookieValue("JSESSIONID");
+ if (Objects.isNull(sessionId)) {
+ return Session.empty();
+ }
+
+ return SessionManager.findSession(sessionId);
+ }
+
+ public String getHeaderValue(final String headerName) {
+ return headers.getHeaderValue(headerName);
+ }
+
+ public String getCookieValue(final String cookieName) {
+ return cookies.getCookieValue(cookieName);
+ }
+
+ public Headers headers() {
+ return headers;
+ }
+
+ public Cookies cookies() {
+ return cookies;
+ }
+
+ public Session session() {
+ return session;
+ }
+
+ @Override
+ public String toString() {
+ return "HttpHeaders{" +
+ "headers=" + headers + System.lineSeparator() +
+ " " + cookies + System.lineSeparator() +
+ " " + session + System.lineSeparator() +
+ '}';
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/common/HttpVersion.java b/tomcat/src/main/java/org/apache/coyote/common/HttpVersion.java
new file mode 100644
index 0000000000..455272a38d
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/common/HttpVersion.java
@@ -0,0 +1,32 @@
+package org.apache.coyote.common;
+
+import java.util.Arrays;
+
+public enum HttpVersion {
+
+ HTTP_1_1("HTTP/1.1");
+
+ private final String source;
+
+ HttpVersion(final String source) {
+ this.source = source;
+ }
+
+ public static HttpVersion from(final String source) {
+ return Arrays.stream(HttpVersion.values())
+ .filter(httpVersion -> httpVersion.source.equals(source))
+ .findFirst()
+ .orElseThrow(() -> new IllegalArgumentException("입력된 값으로 HttpVersion을 조회할 수 없습니다."));
+ }
+
+ public String version() {
+ return source;
+ }
+
+ @Override
+ public String toString() {
+ return "HttpVersion{" +
+ "source='" + source + '\'' +
+ '}';
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/common/MediaType.java b/tomcat/src/main/java/org/apache/coyote/common/MediaType.java
new file mode 100644
index 0000000000..fc067b2886
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/common/MediaType.java
@@ -0,0 +1,34 @@
+package org.apache.coyote.common;
+
+import java.util.Arrays;
+
+public enum MediaType {
+
+ TEXT_HTML("text/html", ".html"),
+ TEXT_CSS("text/css", ".css"),
+ APPLICATION_JAVASCRIPT("application/javascript", ".js");
+
+ private final String source;
+ private final String type;
+
+ MediaType(final String source, final String type) {
+ this.source = source;
+ this.type = type;
+ }
+
+ public static MediaType from(final String resourceUrl) {
+ return Arrays.stream(MediaType.values())
+ .filter(mediaType -> resourceUrl.endsWith(mediaType.type))
+ .findFirst()
+ .orElse(TEXT_HTML);
+ }
+
+ public String source() {
+ return source;
+ }
+
+ @Override
+ public String toString() {
+ return source;
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/exception/CoyoteException.java b/tomcat/src/main/java/org/apache/coyote/exception/CoyoteException.java
new file mode 100644
index 0000000000..a2913345f5
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/exception/CoyoteException.java
@@ -0,0 +1,8 @@
+package org.apache.coyote.exception;
+
+public abstract class CoyoteException extends RuntimeException {
+
+ protected CoyoteException(final String message) {
+ super(message);
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/exception/CoyoteHttpException.java b/tomcat/src/main/java/org/apache/coyote/exception/CoyoteHttpException.java
new file mode 100644
index 0000000000..a22fa13a2e
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/exception/CoyoteHttpException.java
@@ -0,0 +1,8 @@
+package org.apache.coyote.exception;
+
+public class CoyoteHttpException extends CoyoteException {
+
+ public CoyoteHttpException(final String message) {
+ super(message);
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/exception/CoyoteIOException.java b/tomcat/src/main/java/org/apache/coyote/exception/CoyoteIOException.java
new file mode 100644
index 0000000000..5e5510c298
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/exception/CoyoteIOException.java
@@ -0,0 +1,8 @@
+package org.apache.coyote.exception;
+
+public class CoyoteIOException extends CoyoteException {
+
+ public CoyoteIOException(final String message) {
+ super(message);
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/handler/HomeRequestHandler.java b/tomcat/src/main/java/org/apache/coyote/handler/HomeRequestHandler.java
new file mode 100644
index 0000000000..bef97d716d
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/handler/HomeRequestHandler.java
@@ -0,0 +1,27 @@
+package org.apache.coyote.handler;
+
+import org.apache.coyote.request.HttpRequest;
+import org.apache.coyote.response.HttpResponse;
+import org.apache.coyote.response.ResponseBody;
+
+import static org.apache.coyote.common.HttpVersion.HTTP_1_1;
+import static org.apache.coyote.common.MediaType.TEXT_HTML;
+import static org.apache.coyote.response.HttpStatus.OK;
+
+public class HomeRequestHandler implements RequestHandler {
+
+ private static final String DEFAULT = "Hello world!";
+
+ @Override
+ public HttpResponse handle(final HttpRequest httpRequest) {
+ final ResponseBody responseBody = new ResponseBody(DEFAULT);
+
+ return HttpResponse.builder()
+ .setHttpVersion(HTTP_1_1)
+ .setHttpStatus(OK)
+ .setContentType(TEXT_HTML.source())
+ .setContentLength(responseBody.length())
+ .setResponseBody(responseBody)
+ .build();
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/handler/MappingInfo.java b/tomcat/src/main/java/org/apache/coyote/handler/MappingInfo.java
new file mode 100644
index 0000000000..2e8183d81a
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/handler/MappingInfo.java
@@ -0,0 +1,36 @@
+package org.apache.coyote.handler;
+
+import org.apache.coyote.request.HttpRequest;
+
+import java.util.Objects;
+
+public class MappingInfo {
+
+ private final String httpMethod;
+ private final String requestPath;
+
+ public MappingInfo(final String httpMethod, final String requestPath) {
+ this.httpMethod = httpMethod;
+ this.requestPath = requestPath;
+ }
+
+ public static MappingInfo from(final HttpRequest request) {
+ return new MappingInfo(
+ request.requestLine().httpMethod().name(),
+ request.requestLine().requestPath().source()
+ );
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ final MappingInfo that = (MappingInfo) o;
+ return Objects.equals(httpMethod, that.httpMethod) && Objects.equals(requestPath, that.requestPath);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(httpMethod, requestPath);
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/handler/RequestHandler.java b/tomcat/src/main/java/org/apache/coyote/handler/RequestHandler.java
new file mode 100644
index 0000000000..4abb60b683
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/handler/RequestHandler.java
@@ -0,0 +1,9 @@
+package org.apache.coyote.handler;
+
+import org.apache.coyote.request.HttpRequest;
+import org.apache.coyote.response.HttpResponse;
+
+public interface RequestHandler {
+
+ HttpResponse handle(final HttpRequest httpRequest);
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/handler/RequestHandlerComposite.java b/tomcat/src/main/java/org/apache/coyote/handler/RequestHandlerComposite.java
new file mode 100644
index 0000000000..d9a71f3bf3
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/handler/RequestHandlerComposite.java
@@ -0,0 +1,34 @@
+package org.apache.coyote.handler;
+
+import org.apache.coyote.handler.get.UserLoginRequestGetHandler;
+import org.apache.coyote.handler.get.UserRegisterRequestGetHandler;
+import org.apache.coyote.handler.post.UserRegisterRequestPostHandler;
+import org.apache.coyote.request.HttpRequest;
+import org.apache.coyote.response.HttpResponse;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.apache.coyote.request.HttpMethod.GET;
+import static org.apache.coyote.request.HttpMethod.POST;
+
+public class RequestHandlerComposite {
+
+ private RequestHandlerComposite() {
+ }
+
+ private static final Map mapping = new HashMap<>();
+
+ static {
+ mapping.put(new MappingInfo(GET.name(), "/"), new HomeRequestHandler());
+ mapping.put(new MappingInfo(GET.name(), "/login"), new UserLoginRequestGetHandler());
+ mapping.put(new MappingInfo(GET.name(), "/register"), new UserRegisterRequestGetHandler());
+ mapping.put(new MappingInfo(POST.name(), "/register"), new UserRegisterRequestPostHandler());
+ }
+
+ public static HttpResponse handle(final HttpRequest httpRequest) {
+ final RequestHandler requestHandler = mapping.getOrDefault(MappingInfo.from(httpRequest), new ResourceRequestHandler());
+
+ return requestHandler.handle(httpRequest);
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/handler/ResourceRequestHandler.java b/tomcat/src/main/java/org/apache/coyote/handler/ResourceRequestHandler.java
new file mode 100644
index 0000000000..dae94946c7
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/handler/ResourceRequestHandler.java
@@ -0,0 +1,28 @@
+package org.apache.coyote.handler;
+
+import org.apache.coyote.common.MediaType;
+import org.apache.coyote.request.HttpRequest;
+import org.apache.coyote.response.HttpResponse;
+import org.apache.coyote.response.ResponseBody;
+import org.apache.coyote.util.ResourceReader;
+
+import static org.apache.coyote.common.HttpVersion.HTTP_1_1;
+import static org.apache.coyote.response.HttpStatus.OK;
+
+public class ResourceRequestHandler implements RequestHandler {
+
+ @Override
+ public HttpResponse handle(final HttpRequest httpRequest) {
+ final String requestPath = httpRequest.requestLine().requestPath().source();
+ final String resourceBody = ResourceReader.read(requestPath);
+ final ResponseBody responseBody = new ResponseBody(resourceBody);
+
+ return HttpResponse.builder()
+ .setHttpVersion(HTTP_1_1)
+ .setContentType(MediaType.from(requestPath).source())
+ .setHttpStatus(OK)
+ .setResponseBody(responseBody)
+ .setContentLength(resourceBody.length())
+ .build();
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/handler/get/UserLoginRequestGetHandler.java b/tomcat/src/main/java/org/apache/coyote/handler/get/UserLoginRequestGetHandler.java
new file mode 100644
index 0000000000..80753cde01
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/handler/get/UserLoginRequestGetHandler.java
@@ -0,0 +1,102 @@
+package org.apache.coyote.handler.get;
+
+import nextstep.jwp.db.InMemoryUserRepository;
+import nextstep.jwp.model.User;
+import org.apache.coyote.handler.RequestHandler;
+import org.apache.coyote.request.HttpRequest;
+import org.apache.coyote.request.QueryParams;
+import org.apache.coyote.response.HttpResponse;
+import org.apache.coyote.response.ResponseBody;
+import org.apache.coyote.session.Cookies;
+import org.apache.coyote.session.Session;
+import org.apache.coyote.session.SessionManager;
+import org.apache.coyote.util.ResourceReader;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Objects;
+import java.util.Optional;
+import java.util.UUID;
+
+import static org.apache.coyote.common.HttpVersion.HTTP_1_1;
+import static org.apache.coyote.common.MediaType.TEXT_HTML;
+import static org.apache.coyote.response.HttpStatus.FOUND;
+import static org.apache.coyote.response.HttpStatus.OK;
+
+public class UserLoginRequestGetHandler implements RequestHandler {
+
+ private static final String LOGIN_PAGE_URI = "/login.html";
+ private static final String LOGIN_SUCCESS_REDIRECT_URI = "/index.html";
+ private static final String LOGIN_FAIL_REDIRECT_URI = "/401.html";
+
+ private final Logger log = LoggerFactory.getLogger(this.getClass());
+
+ @Override
+ public HttpResponse handle(final HttpRequest httpRequest) {
+ if (isAuthenticated(httpRequest)) {
+ return redirectHomePageWhenAuthenticated();
+ }
+
+ final QueryParams queryParams = httpRequest.requestLine().queryParams();
+ if (queryParams.isEmpty()) {
+ return responseLoginPage();
+ }
+
+ final String account = queryParams.getParamValue("account");
+ final String password = queryParams.getParamValue("password");
+ final Optional maybeUser = InMemoryUserRepository.findByAccount(account);
+ if (maybeUser.isPresent() && maybeUser.get().checkPassword(password)) {
+ log.info("===========> 로그인된 유저 = {}", maybeUser.get());
+ return redirectHomePageWhenNewAuthentication(maybeUser.get());
+ }
+
+ return redirect401PageWhenFailed();
+ }
+
+ private boolean isAuthenticated(final HttpRequest httpRequest) {
+ final Session foundSession = SessionManager.findSession(httpRequest.getCookieValue("JSESSIONID"));
+
+ return Objects.nonNull(foundSession);
+ }
+
+ private HttpResponse redirectHomePageWhenAuthenticated() {
+ return HttpResponse.builder()
+ .setHttpVersion(HTTP_1_1)
+ .setHttpStatus(FOUND)
+ .sendRedirect(LOGIN_SUCCESS_REDIRECT_URI)
+ .build();
+ }
+
+ private HttpResponse responseLoginPage() {
+ final String loginPageResource = ResourceReader.read(LOGIN_PAGE_URI);
+ final ResponseBody loginPageBody = new ResponseBody(loginPageResource);
+
+ return HttpResponse.builder()
+ .setHttpVersion(HTTP_1_1)
+ .setHttpStatus(OK)
+ .setContentType(TEXT_HTML.source())
+ .setContentLength(loginPageBody.length())
+ .setResponseBody(loginPageBody)
+ .build();
+ }
+
+ private HttpResponse redirectHomePageWhenNewAuthentication(final User loginUser) {
+ final Session newSession = new Session(UUID.randomUUID().toString());
+ newSession.setAttribute("account", loginUser.getAccount());
+ SessionManager.add(newSession);
+
+ return HttpResponse.builder()
+ .setHttpVersion(HTTP_1_1)
+ .setCookies(Cookies.ofJSessionId(newSession.id()))
+ .sendRedirect(LOGIN_SUCCESS_REDIRECT_URI)
+ .build();
+ }
+
+ private HttpResponse redirect401PageWhenFailed() {
+ return HttpResponse.builder()
+ .setHttpVersion(HTTP_1_1)
+ .setHttpStatus(FOUND)
+ .sendRedirect(LOGIN_FAIL_REDIRECT_URI)
+ .build();
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/handler/get/UserRegisterRequestGetHandler.java b/tomcat/src/main/java/org/apache/coyote/handler/get/UserRegisterRequestGetHandler.java
new file mode 100644
index 0000000000..78505342bc
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/handler/get/UserRegisterRequestGetHandler.java
@@ -0,0 +1,30 @@
+package org.apache.coyote.handler.get;
+
+import org.apache.coyote.handler.RequestHandler;
+import org.apache.coyote.request.HttpRequest;
+import org.apache.coyote.response.HttpResponse;
+import org.apache.coyote.response.ResponseBody;
+import org.apache.coyote.util.ResourceReader;
+
+import static org.apache.coyote.common.HttpVersion.HTTP_1_1;
+import static org.apache.coyote.common.MediaType.TEXT_HTML;
+import static org.apache.coyote.response.HttpStatus.OK;
+
+public class UserRegisterRequestGetHandler implements RequestHandler {
+
+ private static final String REGISTER_PAGE_URI = "/register.html";
+
+ @Override
+ public HttpResponse handle(final HttpRequest httpRequest) {
+ final String registerPageResource = ResourceReader.read(REGISTER_PAGE_URI);
+ final ResponseBody registerPageBody = new ResponseBody(registerPageResource);
+
+ return HttpResponse.builder()
+ .setHttpVersion(HTTP_1_1)
+ .setHttpStatus(OK)
+ .setContentType(TEXT_HTML.source())
+ .setContentLength(registerPageBody.length())
+ .setResponseBody(registerPageBody)
+ .build();
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/handler/post/UserRegisterRequestPostHandler.java b/tomcat/src/main/java/org/apache/coyote/handler/post/UserRegisterRequestPostHandler.java
new file mode 100644
index 0000000000..7f765afb16
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/handler/post/UserRegisterRequestPostHandler.java
@@ -0,0 +1,40 @@
+package org.apache.coyote.handler.post;
+
+import nextstep.jwp.db.InMemoryUserRepository;
+import nextstep.jwp.model.User;
+import org.apache.coyote.handler.RequestHandler;
+import org.apache.coyote.request.HttpRequest;
+import org.apache.coyote.request.RequestBody;
+import org.apache.coyote.response.HttpResponse;
+import org.apache.coyote.session.Cookies;
+import org.apache.coyote.session.Session;
+import org.apache.coyote.session.SessionManager;
+
+import java.util.UUID;
+
+import static org.apache.coyote.common.HttpVersion.HTTP_1_1;
+
+public class UserRegisterRequestPostHandler implements RequestHandler {
+
+ private static final String REGISTER_SUCCESS_REDIRECT_URI = "/index.html";
+
+ @Override
+ public HttpResponse handle(final HttpRequest httpRequest) {
+ final RequestBody requestBody = httpRequest.requestBody();
+ final String account = requestBody.getBodyValue("account");
+ final String password = requestBody.getBodyValue("password");
+ final String email = requestBody.getBodyValue("email");
+
+ InMemoryUserRepository.save(new User(account, password, email));
+
+ final Session newSession = new Session(UUID.randomUUID().toString());
+ newSession.setAttribute("account", account);
+ SessionManager.add(newSession);
+
+ return HttpResponse.builder()
+ .setHttpVersion(HTTP_1_1)
+ .setCookies(Cookies.ofJSessionId(newSession.id()))
+ .sendRedirect(REGISTER_SUCCESS_REDIRECT_URI)
+ .build();
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java
index 7f1b2c7e96..98756e3a57 100644
--- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java
+++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java
@@ -2,10 +2,17 @@
import nextstep.jwp.exception.UncheckedServletException;
import org.apache.coyote.Processor;
+import org.apache.coyote.request.HttpRequest;
+import org.apache.coyote.response.HttpResponse;
+import org.apache.coyote.handler.RequestHandlerComposite;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.io.BufferedReader;
import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
import java.net.Socket;
public class Http11Processor implements Runnable, Processor {
@@ -26,19 +33,16 @@ public void run() {
@Override
public void process(final Socket connection) {
- try (final var inputStream = connection.getInputStream();
- final var outputStream = connection.getOutputStream()) {
+ try (final InputStream inputStream = connection.getInputStream();
+ final OutputStream outputStream = connection.getOutputStream();
+ final BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));) {
- final var responseBody = "Hello world!";
+ final HttpRequest request = HttpRequest.from(br);
+ log.info("=============> HTTP Request : \n {}", request);
+ final HttpResponse response = RequestHandlerComposite.handle(request);
+ log.info("=============> HTTP Response : \n {}", response);
- final var response = String.join("\r\n",
- "HTTP/1.1 200 OK ",
- "Content-Type: text/html;charset=utf-8 ",
- "Content-Length: " + responseBody.getBytes().length + " ",
- "",
- responseBody);
-
- outputStream.write(response.getBytes());
+ outputStream.write(Http11ResponseConverter.convertToBytes(response));
outputStream.flush();
} catch (IOException | UncheckedServletException e) {
log.error(e.getMessage(), e);
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11ResponseConverter.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11ResponseConverter.java
new file mode 100644
index 0000000000..aca18e09e5
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11ResponseConverter.java
@@ -0,0 +1,48 @@
+package org.apache.coyote.http11;
+
+import org.apache.coyote.response.HttpResponse;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+public class Http11ResponseConverter {
+
+ private static final String SPACE = " ";
+ private static final String ENTER = "\r\n";
+
+ private Http11ResponseConverter() {
+ }
+
+ public static byte[] convertToBytes(final HttpResponse response) {
+ final StringBuilder responseByteBuilder = new StringBuilder();
+ appendResponseLine(response, responseByteBuilder);
+ appendResponseHeaders(response, responseByteBuilder);
+ appendResponseBody(response, responseByteBuilder);
+
+ return responseByteBuilder.toString().getBytes(UTF_8);
+ }
+
+ private static void appendResponseLine(final HttpResponse response, final StringBuilder responseByteBuilder) {
+ responseByteBuilder
+ .append(response.httpVersion().version()).append(SPACE)
+ .append(response.httpStatus().statusCode()).append(SPACE)
+ .append(response.httpStatus().statusName()).append(SPACE)
+ .append(ENTER);
+ }
+
+ private static void appendResponseHeaders(final HttpResponse response, final StringBuilder responseByteBuilder) {
+ response.httpHeaders()
+ .headerNames()
+ .forEach(headerName -> {
+ final String headerValue = response.httpHeaders().getHeaderValue(headerName);
+ responseByteBuilder
+ .append(headerName).append(": ").append(headerValue).append(SPACE)
+ .append(ENTER);
+ });
+ }
+
+ private static void appendResponseBody(final HttpResponse response, final StringBuilder responseByteBuilder) {
+ responseByteBuilder
+ .append(ENTER)
+ .append(response.responseBody().source());
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/request/HttpMethod.java b/tomcat/src/main/java/org/apache/coyote/request/HttpMethod.java
new file mode 100644
index 0000000000..c5c5ef09cb
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/request/HttpMethod.java
@@ -0,0 +1,22 @@
+package org.apache.coyote.request;
+
+import java.util.Arrays;
+
+import static java.util.Locale.ENGLISH;
+
+public enum HttpMethod {
+
+ GET, POST, PATCH, PUT, DELETE, OPTION;
+
+ public static HttpMethod from(final String source) {
+ return Arrays.stream(HttpMethod.values())
+ .filter(httpMethod -> httpMethod.contains(source))
+ .findFirst()
+ .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 HttpMethod 값입니다."));
+ }
+
+ private boolean contains(final String source) {
+ return source.toUpperCase(ENGLISH)
+ .contains(this.name());
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/request/HttpRequest.java b/tomcat/src/main/java/org/apache/coyote/request/HttpRequest.java
new file mode 100644
index 0000000000..54d33553c3
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/request/HttpRequest.java
@@ -0,0 +1,111 @@
+package org.apache.coyote.request;
+
+import org.apache.coyote.common.Headers;
+import org.apache.coyote.common.HttpHeaders;
+import org.apache.coyote.common.MediaType;
+import org.apache.coyote.exception.CoyoteIOException;
+import org.apache.coyote.session.Cookies;
+import org.apache.coyote.session.Session;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+import static org.apache.coyote.common.HeaderType.CONTENT_LENGTH;
+
+public class HttpRequest {
+
+ private static final String HEADER_END_CONDITION = "";
+
+ private final RequestLine requestLine;
+ private final HttpHeaders httpHeaders;
+ private final MediaType mediaType;
+ private final RequestBody requestBody;
+
+ private HttpRequest(final RequestLine requestLine,
+ final HttpHeaders headersV2,
+ final MediaType mediaType,
+ final RequestBody requestBody
+ ) {
+ this.requestLine = requestLine;
+ this.httpHeaders = headersV2;
+ this.mediaType = mediaType;
+ this.requestBody = requestBody;
+ }
+
+ public static HttpRequest from(final BufferedReader br) {
+ try {
+ final RequestLine requestLine = RequestLine.from(br.readLine());
+ final HttpHeaders httpHeaders = parseToHttpHeaders(br);
+ final MediaType mediaType = MediaType.from(requestLine.requestPath().source());
+ final RequestBody requestBody = parseToResponseBody(br, httpHeaders);
+
+ return new HttpRequest(requestLine, httpHeaders, mediaType, requestBody);
+ } catch (IOException e) {
+ throw new CoyoteIOException("HTTP 요청 정보를 읽던 도중에 예외가 발생하였습니다.");
+ }
+ }
+
+ private static HttpHeaders parseToHttpHeaders(final BufferedReader br) throws IOException {
+ final List headersWithValue = new ArrayList<>();
+ String header = br.readLine();
+ while (!header.equals(HEADER_END_CONDITION)) {
+ headersWithValue.add(header);
+ header = br.readLine();
+ }
+
+ return HttpHeaders.from(headersWithValue);
+ }
+
+ private static RequestBody parseToResponseBody(final BufferedReader br, final HttpHeaders httpHeaders) throws IOException {
+ final String contentLengthHeader = httpHeaders.getHeaderValue(CONTENT_LENGTH.source());
+ if (Objects.isNull(contentLengthHeader)) {
+ return RequestBody.empty();
+ }
+
+ final int contentLength = Integer.parseInt(contentLengthHeader);
+ final char[] buffer = new char[contentLength];
+ br.read(buffer, 0, contentLength);
+ return RequestBody.from(new String(buffer));
+ }
+
+ public RequestLine requestLine() {
+ return requestLine;
+ }
+
+ public String getCookieValue(final String cookieName) {
+ return httpHeaders.getCookieValue(cookieName);
+ }
+
+ public Headers headers() {
+ return httpHeaders.headers();
+ }
+
+ public Cookies cookies() {
+ return httpHeaders.cookies();
+ }
+
+ public Session session() {
+ return httpHeaders.session();
+ }
+
+ public MediaType mediaType() {
+ return mediaType;
+ }
+
+ public RequestBody requestBody() {
+ return requestBody;
+ }
+
+ @Override
+ public String toString() {
+ return "HttpRequest{" + System.lineSeparator() +
+ " requestLine = " + requestLine + ", " + System.lineSeparator() +
+ " httpHeaders = " + httpHeaders + System.lineSeparator() +
+ " mediaType = " + mediaType + System.lineSeparator() +
+ " requestBody = " + requestBody + System.lineSeparator() +
+ '}';
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/request/QueryParams.java b/tomcat/src/main/java/org/apache/coyote/request/QueryParams.java
new file mode 100644
index 0000000000..d34a82bcaf
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/request/QueryParams.java
@@ -0,0 +1,71 @@
+package org.apache.coyote.request;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+public class QueryParams {
+
+ private static final String QUERY_PARAM_START_CONDITION = "?";
+ private static final String QUERY_PARAMS_DELIMITER = "&";
+ private static final String QUERY_PARAM_VALUE_DELIMITER = "=";
+ private static final int PARAM_NAME_INDEX = 0;
+ private static final int PARAM_VALUE_INDEX = 1;
+
+ private final Map params = new HashMap<>();
+
+ private QueryParams(final Map params) {
+ this.params.putAll(params);
+ }
+
+ public static QueryParams empty() {
+ return new QueryParams(new HashMap<>());
+ }
+
+ public static QueryParams from(final String queryParamsWithValue) {
+ final Map mapping = new HashMap<>();
+
+ final int paramStartIndex = queryParamsWithValue.indexOf(QUERY_PARAM_START_CONDITION) + 1;
+ final String[] queryParams = queryParamsWithValue.substring(paramStartIndex).split(QUERY_PARAMS_DELIMITER);
+ for (String queryParam : queryParams) {
+ final String[] paramWithValue = queryParam.split(QUERY_PARAM_VALUE_DELIMITER);
+ mapping.put(paramWithValue[PARAM_NAME_INDEX], paramWithValue[PARAM_VALUE_INDEX]);
+ }
+
+ return new QueryParams(mapping);
+ }
+
+ public boolean isEmpty() {
+ return params.isEmpty();
+ }
+
+ public List paramNames() {
+ return new ArrayList<>(params.keySet());
+ }
+
+ public String getParamValue(final String name) {
+ return params.getOrDefault(name, null);
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ final QueryParams that = (QueryParams) o;
+ return Objects.equals(params, that.params);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(params);
+ }
+
+ @Override
+ public String toString() {
+ return "QueryParams{" +
+ "params=" + params +
+ '}';
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/request/RequestBody.java b/tomcat/src/main/java/org/apache/coyote/request/RequestBody.java
new file mode 100644
index 0000000000..e13b6ee873
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/request/RequestBody.java
@@ -0,0 +1,54 @@
+package org.apache.coyote.request;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class RequestBody {
+
+ private static final String REQUEST_DELIMITER = "&";
+ private static final String KEY_VALUE_DELIMITER = "=";
+ private static final int NAME_INDEX = 0;
+ private static final int VALUE_INDEX = 1;
+
+ private final Map body;
+
+ private RequestBody(final Map body) {
+ this.body = body;
+ }
+
+ public static RequestBody from(final String requestBodyValue) {
+ if (requestBodyValue.isBlank()) {
+ return RequestBody.empty();
+ }
+
+ final Map mapping = Arrays.asList(requestBodyValue.split(REQUEST_DELIMITER))
+ .stream()
+ .map(bodyEntry -> bodyEntry.split(KEY_VALUE_DELIMITER))
+ .collect(Collectors.toMap(keyValue -> keyValue[NAME_INDEX], keyValue -> keyValue[VALUE_INDEX]));
+
+ return new RequestBody(mapping);
+ }
+
+ public static RequestBody empty() {
+ return new RequestBody(Collections.emptyMap());
+ }
+
+ public List names() {
+ return new ArrayList<>(body.keySet());
+ }
+
+ public String getBodyValue(final String name) {
+ return body.getOrDefault(name, null);
+ }
+
+ @Override
+ public String toString() {
+ return "RequestBody{" +
+ "body=" + body +
+ '}';
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/request/RequestLine.java b/tomcat/src/main/java/org/apache/coyote/request/RequestLine.java
new file mode 100644
index 0000000000..083af834c0
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/request/RequestLine.java
@@ -0,0 +1,79 @@
+package org.apache.coyote.request;
+
+import org.apache.coyote.common.HttpVersion;
+import org.apache.coyote.exception.CoyoteHttpException;
+
+import java.util.regex.Pattern;
+
+public class RequestLine {
+
+ private static final Pattern QUERY_PARAM_PATTERN = Pattern.compile("^[^?]*\\?[^?]*$");
+ private static final String REQUEST_LINE_DELIMITER = " ";
+ private static final String QUERY_PARAM_START_DELIMITER = "?";
+ private static final int REQUEST_LINE_LENGTH = 3;
+ private static final int HTTP_METHOD_INDEX = 0;
+ private static final int HTTP_REQUEST_URI_INDEX = 1;
+ private static final int HTTP_VERSION_INDEX = 2;
+
+ private final HttpMethod httpMethod;
+ private final HttpVersion httpVersion;
+ private final RequestPath requestPath;
+ private final QueryParams queryParams;
+
+ private RequestLine(final HttpMethod httpMethod, final HttpVersion httpVersion, final RequestPath requestPath, final QueryParams queryParams) {
+ this.httpMethod = httpMethod;
+ this.httpVersion = httpVersion;
+ this.requestPath = requestPath;
+ this.queryParams = queryParams;
+ }
+
+ public static RequestLine from(final String requestLine) {
+ final String[] requestLineValues = requestLine.split(REQUEST_LINE_DELIMITER);
+ if (requestLineValues.length != REQUEST_LINE_LENGTH) {
+ throw new CoyoteHttpException("HTTP 요청으로 들어온 값의 첫 번째 라인에 HttpMethod, URI, HttpVersion가 존재해야 합니다.");
+ }
+
+ final HttpMethod httpMethod = HttpMethod.from(requestLineValues[HTTP_METHOD_INDEX]);
+ final HttpVersion httpVersion = HttpVersion.from(requestLineValues[HTTP_VERSION_INDEX]);
+
+ final String requestUri = requestLineValues[HTTP_REQUEST_URI_INDEX];
+ QueryParams queryParams = QueryParams.empty();
+ RequestPath requestPath = new RequestPath(requestUri);
+ if (QUERY_PARAM_PATTERN.matcher(requestUri).matches()) {
+ final int queryParamStartIndex = requestUri.indexOf(QUERY_PARAM_START_DELIMITER);
+ final String requestPathValue = requestUri.substring(0, queryParamStartIndex);
+ final String queryParamsValue = requestUri.substring(queryParamStartIndex);
+
+ requestPath = new RequestPath(requestPathValue);
+ queryParams = QueryParams.from(queryParamsValue);
+ }
+
+ return new RequestLine(httpMethod, httpVersion, requestPath, queryParams);
+ }
+
+ public HttpMethod httpMethod() {
+ return httpMethod;
+ }
+
+ public HttpVersion httpVersion() {
+ return httpVersion;
+ }
+
+ public RequestPath requestPath() {
+ return requestPath;
+ }
+
+ public QueryParams queryParams() {
+ return queryParams;
+ }
+
+ @Override
+ public String toString() {
+ return "RequestLine{" +
+ "httpMethod=" + httpMethod +
+ ", httpVersion=" + httpVersion +
+ ", requestPath=" + requestPath +
+ ", queryParams=" + queryParams +
+ '}';
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/request/RequestPath.java b/tomcat/src/main/java/org/apache/coyote/request/RequestPath.java
new file mode 100644
index 0000000000..f72eddd5b8
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/request/RequestPath.java
@@ -0,0 +1,37 @@
+package org.apache.coyote.request;
+
+import java.util.Objects;
+
+public class RequestPath {
+
+ private final String source;
+
+ public RequestPath(final String source) {
+ if (Objects.isNull(source)) {
+ throw new IllegalArgumentException("RequestPath 에 null 이 들어올 수 없습니다.");
+ }
+ this.source = source;
+ }
+
+ public String source() {
+ return source;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ final RequestPath that = (RequestPath) o;
+ return Objects.equals(source, that.source);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(source);
+ }
+
+ @Override
+ public String toString() {
+ return source;
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/response/HttpResponse.java b/tomcat/src/main/java/org/apache/coyote/response/HttpResponse.java
new file mode 100644
index 0000000000..f02c7579e3
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/response/HttpResponse.java
@@ -0,0 +1,105 @@
+package org.apache.coyote.response;
+
+import org.apache.coyote.common.Headers;
+import org.apache.coyote.common.HttpVersion;
+import org.apache.coyote.session.Cookies;
+
+import static org.apache.coyote.common.CharacterSet.UTF_8;
+
+public class HttpResponse {
+
+ private final HttpVersion httpVersion;
+ private final HttpStatus httpStatus;
+ private final Headers headers;
+ private final ResponseBody responseBody;
+
+ private HttpResponse(final HttpVersion httpVersion, final HttpStatus httpStatus, final Headers headers, final ResponseBody responseBody) {
+ this.httpVersion = httpVersion;
+ this.httpStatus = httpStatus;
+ this.headers = headers;
+ this.responseBody = responseBody;
+ }
+
+ public static HttpResponseBuilder builder() {
+ return new HttpResponseBuilder();
+ }
+
+ public HttpVersion httpVersion() {
+ return httpVersion;
+ }
+
+ public HttpStatus httpStatus() {
+ return httpStatus;
+ }
+
+ public Headers httpHeaders() {
+ return headers;
+ }
+
+ public ResponseBody responseBody() {
+ return responseBody;
+ }
+
+ @Override
+ public String toString() {
+ return "HttpResponse{" + System.lineSeparator() +
+ " httpVersion = " + httpVersion + ", " + System.lineSeparator() +
+ " httpStatus = " + httpStatus + ", " + System.lineSeparator() +
+ " httpHeaders = " + headers + ", " + System.lineSeparator() +
+ " responseBody = " + responseBody + ", " + System.lineSeparator() +
+ '}';
+ }
+
+ public static class HttpResponseBuilder {
+
+ private Headers headers = new Headers();
+ private ResponseBody responseBody = ResponseBody.empty();
+ private HttpVersion httpVersion = HttpVersion.HTTP_1_1;
+ private HttpStatus httpStatus;
+
+ public HttpResponseBuilder setHttpVersion(final HttpVersion httpVersion) {
+ this.httpVersion = httpVersion;
+ return this;
+ }
+
+ public HttpResponseBuilder setHttpStatus(final HttpStatus httpStatus) {
+ this.httpStatus = httpStatus;
+ return this;
+ }
+
+ public HttpResponseBuilder setContentType(final String contentType) {
+ this.headers.setContentType(contentType + ";" + UTF_8.source());
+ return this;
+ }
+
+ public HttpResponseBuilder setContentLength(final int contentLength) {
+ this.headers.setContentLength(contentLength);
+ return this;
+ }
+
+ public HttpResponseBuilder sendRedirect(final String uri) {
+ this.httpStatus = HttpStatus.FOUND;
+ this.headers.setLocation(uri);
+ return this;
+ }
+
+ public HttpResponseBuilder setCookies(final Cookies cookies) {
+ this.headers.setCookies(cookies);
+ return this;
+ }
+
+ public HttpResponseBuilder addCookie(final String cookieName, final String cookieValue) {
+ this.headers.addCookie(cookieName, cookieValue);
+ return this;
+ }
+
+ public HttpResponseBuilder setResponseBody(final ResponseBody responseBody) {
+ this.responseBody = responseBody;
+ return this;
+ }
+
+ public HttpResponse build() {
+ return new HttpResponse(httpVersion, httpStatus, headers, responseBody);
+ }
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/response/HttpStatus.java b/tomcat/src/main/java/org/apache/coyote/response/HttpStatus.java
new file mode 100644
index 0000000000..24115ef822
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/response/HttpStatus.java
@@ -0,0 +1,31 @@
+package org.apache.coyote.response;
+
+public enum HttpStatus {
+
+ OK("OK", 200),
+ FOUND("FOUND", 302);
+
+ private final String statusName;
+ private final int statusCode;
+
+ HttpStatus(final String statusName, final int statusCode) {
+ this.statusName = statusName;
+ this.statusCode = statusCode;
+ }
+
+ public String statusName() {
+ return statusName;
+ }
+
+ public int statusCode() {
+ return statusCode;
+ }
+
+ @Override
+ public String toString() {
+ return "HttpStatus{" +
+ "statusName='" + statusName + '\'' +
+ ", statusCode=" + statusCode +
+ '}';
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/response/ResponseBody.java b/tomcat/src/main/java/org/apache/coyote/response/ResponseBody.java
new file mode 100644
index 0000000000..2966837c93
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/response/ResponseBody.java
@@ -0,0 +1,37 @@
+package org.apache.coyote.response;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+public class ResponseBody {
+
+ private static final String EMPTY_BODY = "";
+
+ private final String source;
+
+ public ResponseBody(final String source) {
+ this.source = source;
+ }
+
+ public static ResponseBody empty() {
+ return new ResponseBody(EMPTY_BODY);
+ }
+
+ public byte[] bytes() {
+ return source.getBytes(UTF_8);
+ }
+
+ public int length() {
+ return bytes().length;
+ }
+
+ public String source() {
+ return source;
+ }
+
+ @Override
+ public String toString() {
+ return "ResponseBody.Length{" +
+ "source='" + source.length() + '\'' +
+ '}';
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/session/Cookies.java b/tomcat/src/main/java/org/apache/coyote/session/Cookies.java
new file mode 100644
index 0000000000..19e1448468
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/session/Cookies.java
@@ -0,0 +1,80 @@
+package org.apache.coyote.session;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+public class Cookies {
+
+ private static final String COOKIE_HEADER_DELIMITER = ";";
+ private static final String COOKIE_VALUE_DELIMITER = "=";
+ private static final int COOKIE_KEY_INDEX = 0;
+ private static final int COOKIE_VALUE_INDEX = 1;
+
+ private final Map cookie;
+
+ private Cookies(final Map cookie) {
+ this.cookie = cookie;
+ }
+
+ public static Cookies from(final String cookieValues) {
+ if (Objects.isNull(cookieValues) || cookieValues.isBlank()) {
+ return empty();
+ }
+
+ return new Cookies(collectCookieMapping(cookieValues));
+ }
+
+ private static Map collectCookieMapping(final String cookieValues) {
+ return Arrays.stream(cookieValues.split(COOKIE_HEADER_DELIMITER))
+ .map(cookieEntry -> cookieEntry.split(COOKIE_VALUE_DELIMITER))
+ .collect(Collectors.toMap(
+ cookieEntry -> cookieEntry[COOKIE_KEY_INDEX].trim(),
+ cookieEntry -> cookieEntry[COOKIE_VALUE_INDEX].trim()
+ ));
+ }
+
+ public static Cookies ofJSessionId(final String sessionId) {
+ return new Cookies(Map.of("JSESSIONID", sessionId));
+ }
+
+ public static Cookies empty() {
+ return new Cookies(new HashMap<>());
+ }
+
+ public void addCookie(final Cookies other) {
+ cookie.putAll(other.cookie);
+ }
+
+ public List cookieNames() {
+ return new ArrayList<>(cookie.keySet());
+ }
+
+ public String getCookieValue(final String cookieName) {
+ return cookie.getOrDefault(cookieName, null);
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ final Cookies cookies = (Cookies) o;
+ return Objects.equals(cookie, cookies.cookie);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(cookie);
+ }
+
+ @Override
+ public String toString() {
+ return "Cookies{" +
+ "cookie=" + cookie +
+ '}';
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/session/Session.java b/tomcat/src/main/java/org/apache/coyote/session/Session.java
new file mode 100644
index 0000000000..541f50e46e
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/session/Session.java
@@ -0,0 +1,56 @@
+package org.apache.coyote.session;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+public class Session {
+
+ private final String id;
+ private final Map values = new HashMap<>();
+
+ public Session(final String id) {
+ this.id = id;
+ }
+
+ public static Session empty() {
+ return new Session(null);
+ }
+
+ public Object getAttribute(final String name) {
+ return values.getOrDefault(name, null);
+ }
+
+ public void setAttribute(final String name, final String value) {
+ values.put(name, value);
+ }
+
+ public void removeAttribute(final String name) {
+ values.remove(name);
+ }
+
+ public String id() {
+ return id;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ final Session session = (Session) o;
+ return Objects.equals(id, session.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id);
+ }
+
+ @Override
+ public String toString() {
+ return "Session{" +
+ "id='" + id + '\'' +
+ ", values=" + values +
+ '}';
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/session/SessionManager.java b/tomcat/src/main/java/org/apache/coyote/session/SessionManager.java
new file mode 100644
index 0000000000..b45ae3352e
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/session/SessionManager.java
@@ -0,0 +1,24 @@
+package org.apache.coyote.session;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class SessionManager {
+
+ private static final Map SESSIONS = new HashMap<>();
+
+ private SessionManager() {
+ }
+
+ public static void add(final Session session) {
+ SESSIONS.put(session.id(), session);
+ }
+
+ public static Session findSession(final String id) {
+ return SESSIONS.getOrDefault(id, null);
+ }
+
+ public void remove(final Session session) {
+ SESSIONS.remove(session.id());
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/util/ResourceReader.java b/tomcat/src/main/java/org/apache/coyote/util/ResourceReader.java
new file mode 100644
index 0000000000..b55e001802
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/util/ResourceReader.java
@@ -0,0 +1,28 @@
+package org.apache.coyote.util;
+
+import org.apache.coyote.exception.CoyoteIOException;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+public class ResourceReader {
+
+ private ResourceReader() {
+ }
+
+ private static final String RESOURCE = "static";
+
+ public static String read(final String resourceUri) {
+ try {
+ final URL resourceUrl = ResourceReader.class.getClassLoader().getResource(RESOURCE + resourceUri);
+ final Path resourcePath = new File(resourceUrl.getFile()).toPath();
+
+ return Files.readString(resourcePath);
+ } catch (NullPointerException | IOException e) {
+ throw new CoyoteIOException("정적 파일을 읽는 도중에 오류가 발생하였습니다.");
+ }
+ }
+}
diff --git a/tomcat/src/main/resources/static/login.html b/tomcat/src/main/resources/static/login.html
index f4ed9de875..e0d3139b7b 100644
--- a/tomcat/src/main/resources/static/login.html
+++ b/tomcat/src/main/resources/static/login.html
@@ -22,7 +22,7 @@