Skip to content

Commit

Permalink
[톰캣 구현하기 - 1, 2단계] 허브(방대의) 미션 제출합니다. (#302)
Browse files Browse the repository at this point in the history
* test: step1을 위한 학습 테스트 추가

* feat: 정적 리소스에 대한 응답 기능 추가

* feat: css 지원 기능 추가

* feat: Query String 파싱 기능 구현

* docs: 요구사항 추가

* refactor: QueryString을 위한 Util 클래스 제거 및 QueryString 일급 컬렉션 추가

* feat: 빈 favicon 추가

* feat: Http 요청의 첫 라인을 의미하는 RequestLine, HttpMethod enum 추가

* feat: RequestLine 클래스 queryString 파싱하는 기능 추가

* refactor: process 메서드 flow 개선

* refactor: 기존의 util 클래스 제거하고 HttpResponseGenerator 추가

* feat: RequestHeader 클래스 추가

* feat: RequestBody 클래스 추가

* feat: Found Redirect 기능 추가

* feat: 회원가입 기능 추가

* refactor: 패키지 분리

* feat: 쿠키값을 가지고 있는 httpCookie 추가

* feat: 쿠키 세팅하는 기능 추가

* feat: Session, SessionManager 추가

* feat: 로그인시 세션에 사용자 저장 기능 추가

* refactor: 불필요한 static 메서드 제거

* fix: 자동 정렬된 index.html 수정

* fix: 코드 스멜 제거

* refactor: JSESSIONID, account, passoword 상수화

* refactor: final 빠진 부분 추가

* refactor: 외부에서 사용하지 않는 생성자 private으로 변경

* feat: HttpExtensionType Enum 추가
  • Loading branch information
greeng00se authored Sep 4, 2023
1 parent 68db530 commit 187f9c2
Show file tree
Hide file tree
Showing 36 changed files with 1,638 additions and 223 deletions.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,17 @@
# 톰캣 구현하기

- [x] GET /index.html 요청에 응답한다.
- [x] CSS 형식의 정적 파일 요청도 지원한다.
- [x] QueryString 파싱 기능을 추가한다.
- [x] 로그인 기능(/login)을 구현한다.
- [x] 로그인의 성공하면 http status 302를 반환한다.
- [x] 로그인에 성공하면 세션에 사용자를 저장한다.
- [x] 로그인에 성공하면 JSESSIONID를 Set-Cookie 헤더에 담아 보낸다.
- [x] 로그인에 성공한 상태에서 로그인 페이지로 접근하면 index.html로 리다이렉트한다.
- [x] location header에 리다이렉트할 url을 추가한다.
- [x] 로그인에 실패하는 경우 401.html로 리다이렉트한다.
- [x] 회원가입 기능(/register)을 구현한다.
- [x] 회원가입 페이지의 경우 GET을 사용하여 보여준다.
- [x] 회원가입의 경우 POST를 사용한다.
- [x] 회원가입을 완료하는 경우 index.html로 리다이렉트한다.

50 changes: 27 additions & 23 deletions study/src/test/java/study/FileTest.java
Original file line number Diff line number Diff line change
@@ -1,53 +1,57 @@
package study;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;

import java.nio.file.Path;
import java.util.Collections;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.DisplayNameGenerator;
import org.junit.jupiter.api.Test;

/**
* 웹서버는 사용자가 요청한 html 파일을 제공 할 수 있어야 한다.
* File 클래스를 사용해서 파일을 읽어오고, 사용자에게 전달한다.
* 웹서버는 사용자가 요청한 html 파일을 제공 할 수 있어야 한다. File 클래스를 사용해서 파일을 읽어오고, 사용자에게 전달한다.
*/
@SuppressWarnings("NonAsciiCharacters")
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
@DisplayName("File 클래스 학습 테스트")
class FileTest {

/**
* resource 디렉터리 경로 찾기
*
* File 객체를 생성하려면 파일의 경로를 알아야 한다.
* 자바 애플리케이션은 resource 디렉터리에 HTML, CSS 같은 정적 파일을 저장한다.
* resource 디렉터리의 경로는 어떻게 알아낼 수 있을까?
* <p>
* File 객체를 생성하려면 파일의 경로를 알아야 한다. 자바 애플리케이션은 resource 디렉터리에 HTML, CSS 같은 정적 파일을 저장한다. resource 디렉터리의 경로는 어떻게 알아낼 수
* 있을까?
*/
@Test
void resource_디렉터리에_있는_파일의_경로를_찾는다() {
final String fileName = "nextstep.txt";
final ClassLoader classLoader = ClassLoader.getSystemClassLoader();
final URL resource = classLoader.getResource(fileName);

// todo
final String actual = "";
final String actual = resource.getFile();

assertThat(actual).endsWith(fileName);
}

/**
* 파일 내용 읽기
*
* 읽어온 파일의 내용을 I/O Stream을 사용해서 사용자에게 전달 해야 한다.
* File, Files 클래스를 사용하여 파일의 내용을 읽어보자.
* <p>
* 읽어온 파일의 내용을 I/O Stream을 사용해서 사용자에게 전달 해야 한다. File, Files 클래스를 사용하여 파일의 내용을 읽어보자.
*/
@Test
void 파일의_내용을_읽는다() {
void 파일의_내용을_읽는다() throws IOException {
final String fileName = "nextstep.txt";
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
URL resource = classLoader.getResource(fileName);
String filePath = resource.getFile();
File file = new File(filePath);

// todo
final Path path = null;

// todo
final List<String> actual = Collections.emptyList();
final List<String> actual = Files.readAllLines(file.toPath(), StandardCharsets.UTF_8);

assertThat(actual).containsOnly("nextstep");
}
Expand Down
133 changes: 70 additions & 63 deletions study/src/test/java/study/IOStreamTest.java
Original file line number Diff line number Diff line change
@@ -1,45 +1,54 @@
package study;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

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 org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.DisplayNameGenerator;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

import java.io.*;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;

/**
* 자바는 스트림(Stream)으로부터 I/O를 사용한다.
* 입출력(I/O)은 하나의 시스템에서 다른 시스템으로 데이터를 이동 시킬 때 사용한다.
*
* InputStream은 데이터를 읽고, OutputStream은 데이터를 쓴다.
* FilterStream은 InputStream이나 OutputStream에 연결될 수 있다.
* FilterStream은 읽거나 쓰는 데이터를 수정할 때 사용한다. (e.g. 암호화, 압축, 포맷 변환)
*
* Stream은 데이터를 바이트로 읽고 쓴다.
* 바이트가 아닌 텍스트(문자)를 읽고 쓰려면 Reader와 Writer 클래스를 연결한다.
* Reader, Writer는 다양한 문자 인코딩(e.g. UTF-8)을 처리할 수 있다.
* 자바는 스트림(Stream)으로부터 I/O를 사용한다. 입출력(I/O)은 하나의 시스템에서 다른 시스템으로 데이터를 이동 시킬 때 사용한다.
* <p>
* InputStream은 데이터를 읽고, OutputStream은 데이터를 쓴다. FilterStream은 InputStream이나 OutputStream에 연결될 수 있다. FilterStream은 읽거나 쓰는
* 데이터를 수정할 때 사용한다. (e.g. 암호화, 압축, 포맷 변환)
* <p>
* Stream은 데이터를 바이트로 읽고 쓴다. 바이트가 아닌 텍스트(문자)를 읽고 쓰려면 Reader와 Writer 클래스를 연결한다. Reader, Writer는 다양한 문자 인코딩(e.g. UTF-8)을
* 처리할 수 있다.
*/
@SuppressWarnings("NonAsciiCharacters")
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
@DisplayName("Java I/O Stream 클래스 학습 테스트")
class IOStreamTest {

/**
* OutputStream 학습하기
*
* 자바의 기본 출력 클래스는 java.io.OutputStream이다.
* OutputStream의 write(int b) 메서드는 기반 메서드이다.
* <p>
* 자바의 기본 출력 클래스는 java.io.OutputStream이다. OutputStream의 write(int b) 메서드는 기반 메서드이다.
* <code>public abstract void write(int b) throws IOException;</code>
*/
@Nested
class OutputStream_학습_테스트 {

/**
* OutputStream은 다른 매체에 바이트로 데이터를 쓸 때 사용한다.
* OutputStream의 서브 클래스(subclass)는 특정 매체에 데이터를 쓰기 위해 write(int b) 메서드를 사용한다.
* 예를 들어, FilterOutputStream은 파일로 데이터를 쓸 때,
* 또는 DataOutputStream은 자바의 primitive type data를 다른 매체로 데이터를 쓸 때 사용한다.
*
* OutputStream은 다른 매체에 바이트로 데이터를 쓸 때 사용한다. OutputStream의 서브 클래스(subclass)는 특정 매체에 데이터를 쓰기 위해 write(int b) 메서드를
* 사용한다. 예를 들어, FilterOutputStream은 파일로 데이터를 쓸 때, 또는 DataOutputStream은 자바의 primitive type data를 다른 매체로 데이터를 쓸 때
* 사용한다.
* <p>
* write 메서드는 데이터를 바이트로 출력하기 때문에 비효율적이다.
* <code>write(byte[] data)</code>와 <code>write(byte b[], int off, int len)</code> 메서드는
* 1바이트 이상을 한 번에 전송 할 수 있어 훨씬 효율적이다.
Expand All @@ -48,26 +57,24 @@ class OutputStream_학습_테스트 {
void OutputStream_데이터를_바이트로_처리한다() throws IOException {
final byte[] bytes = {110, 101, 120, 116, 115, 116, 101, 112};
final OutputStream outputStream = new ByteArrayOutputStream(bytes.length);
final String input = "nextstep";

/**
* todo
* OutputStream 객체의 write 메서드를 사용해서 테스트를 통과시킨다
*/

outputStream.write(input.getBytes(), 0, input.length());
final String actual = outputStream.toString();

assertThat(actual).isEqualTo("nextstep");
outputStream.close();
}

/**
* 효율적인 전송을 위해 스트림에서 버퍼링을 사용 할 수 있다.
* BufferedOutputStream 필터를 연결하면 버퍼링이 가능하다.
*
* 버퍼링을 사용하면 OutputStream을 사용할 때 flush를 사용하자.
* flush() 메서드는 버퍼가 아직 가득 차지 않은 상황에서 강제로 버퍼의 내용을 전송한다.
* Stream은 동기(synchronous)로 동작하기 때문에 버퍼가 찰 때까지 기다리면
* 데드락(deadlock) 상태가 되기 때문에 flush로 해제해야 한다.
* 효율적인 전송을 위해 스트림에서 버퍼링을 사용 할 수 있다. BufferedOutputStream 필터를 연결하면 버퍼링이 가능하다.
* <p>
* 버퍼링을 사용하면 OutputStream을 사용할 때 flush를 사용하자. flush() 메서드는 버퍼가 아직 가득 차지 않은 상황에서 강제로 버퍼의 내용을 전송한다. Stream은
* 동기(synchronous)로 동작하기 때문에 버퍼가 찰 때까지 기다리면 데드락(deadlock) 상태가 되기 때문에 flush로 해제해야 한다.
*/
@Test
void BufferedOutputStream_사용하면_버퍼링이_가능하다() throws IOException {
Expand All @@ -78,45 +85,44 @@ class OutputStream_학습_테스트 {
* flush를 사용해서 테스트를 통과시킨다.
* ByteArrayOutputStream과 어떤 차이가 있을까?
*/
outputStream.flush();

verify(outputStream, atLeastOnce()).flush();
outputStream.close();
}

/**
* 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다.
* 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다.
* 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다. 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다.
*/
@Test
void OutputStream_사용하고_나서_close_처리를_해준다() throws IOException {
final OutputStream outputStream = mock(OutputStream.class);

/**
* todo
* try-with-resources를 사용한다.
* java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다.
*/
try (outputStream) {
}

verify(outputStream, atLeastOnce()).close();
}
}

/**
* InputStream 학습하기
*
* 자바의 기본 입력 클래스는 java.io.InputStream이다.
* InputStream은 다른 매체로부터 바이트로 데이터를 읽을 때 사용한다.
* InputStream의 read() 메서드는 기반 메서드이다.
* <p>
* 자바의 기본 입력 클래스는 java.io.InputStream이다. InputStream은 다른 매체로부터 바이트로 데이터를 읽을 때 사용한다. InputStream의 read() 메서드는 기반
* 메서드이다.
* <code>public abstract int read() throws IOException;</code>
*
* <p>
* InputStream의 서브 클래스(subclass)는 특정 매체에 데이터를 읽기 위해 read() 메서드를 사용한다.
*/
@Nested
class InputStream_학습_테스트 {

/**
* read() 메서드는 매체로부터 단일 바이트를 읽는데, 0부터 255 사이의 값을 int 타입으로 반환한다.
* int 값을 byte 타입으로 변환하면 -128부터 127 사이의 값으로 변환된다.
* read() 메서드는 매체로부터 단일 바이트를 읽는데, 0부터 255 사이의 값을 int 타입으로 반환한다. int 값을 byte 타입으로 변환하면 -128부터 127 사이의 값으로 변환된다.
* 그리고 Stream 끝에 도달하면 -1을 반환한다.
*/
@Test
Expand All @@ -128,16 +134,15 @@ class InputStream_학습_테스트 {
* todo
* inputStream에서 바이트로 반환한 값을 문자열로 어떻게 바꿀까?
*/
final String actual = "";
final String actual = new String(inputStream.readAllBytes());

assertThat(actual).isEqualTo("🤩");
assertThat(inputStream.read()).isEqualTo(-1);
inputStream.close();
}

/**
* 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다.
* 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다.
* 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다. 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다.
*/
@Test
void InputStream_사용하고_나서_close_처리를_해준다() throws IOException {
Expand All @@ -148,64 +153,66 @@ class InputStream_학습_테스트 {
* try-with-resources를 사용한다.
* java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다.
*/
try (inputStream) {
}

verify(inputStream, atLeastOnce()).close();
}
}

/**
* FilterStream 학습하기
*
* 필터는 필터 스트림, reader, writer로 나뉜다.
* 필터는 바이트를 다른 데이터 형식으로 변환 할 때 사용한다.
* reader, writer는 UTF-8, ISO 8859-1 같은 형식으로 인코딩된 텍스트를 처리하는 데 사용된다.
* <p>
* 필터는 필터 스트림, reader, writer로 나뉜다. 필터는 바이트를 다른 데이터 형식으로 변환 할 때 사용한다. reader, writer는 UTF-8, ISO 8859-1 같은 형식으로 인코딩된
* 텍스트를 처리하는 데 사용된다.
*/
@Nested
class FilterStream_학습_테스트 {

/**
* BufferedInputStream은 데이터 처리 속도를 높이기 위해 데이터를 버퍼에 저장한다.
* InputStream 객체를 생성하고 필터 생성자에 전달하면 필터에 연결된다.
* 버퍼 크기를 지정하지 않으면 버퍼의 기본 사이즈는 얼마일까?
* BufferedInputStream은 데이터 처리 속도를 높이기 위해 데이터를 버퍼에 저장한다. InputStream 객체를 생성하고 필터 생성자에 전달하면 필터에 연결된다. 버퍼 크기를 지정하지
* 않으면 버퍼의 기본 사이즈는 얼마일까? -> 8192
*/
@Test
void 필터인_BufferedInputStream_사용해보자() {
void 필터인_BufferedInputStream_사용해보자() throws IOException {
final String text = "필터에 연결해보자.";
final InputStream inputStream = new ByteArrayInputStream(text.getBytes());
final InputStream bufferedInputStream = null;
final InputStream bufferedInputStream = new BufferedInputStream(inputStream);

final byte[] actual = new byte[0];
final byte[] actual = bufferedInputStream.readAllBytes();

assertThat(bufferedInputStream).isInstanceOf(FilterInputStream.class);
assertThat(actual).isEqualTo("필터에 연결해보자.".getBytes());
}
}

/**
* 자바의 기본 문자열은 UTF-16 유니코드 인코딩을 사용한다.
* 문자열이 아닌 바이트 단위로 처리하려니 불편하다.
* 그리고 바이트를 문자(char)로 처리하려면 인코딩을 신경 써야 한다.
* reader, writer를 사용하면 입출력 스트림을 바이트가 아닌 문자 단위로 데이터를 처리하게 된다.
* 그리고 InputStreamReader를 사용하면 지정된 인코딩에 따라 유니코드 문자로 변환할 수 있다.
* 자바의 기본 문자열은 UTF-16 유니코드 인코딩을 사용한다. 문자열이 아닌 바이트 단위로 처리하려니 불편하다. 그리고 바이트를 문자(char)로 처리하려면 인코딩을 신경 써야 한다. reader,
* writer를 사용하면 입출력 스트림을 바이트가 아닌 문자 단위로 데이터를 처리하게 된다. 그리고 InputStreamReader를 사용하면 지정된 인코딩에 따라 유니코드 문자로 변환할 수 있다.
*/
@Nested
class InputStreamReader_학습_테스트 {

/**
* InputStreamReader를 사용해서 바이트를 문자(char)로 읽어온다.
* 읽어온 문자(char)를 문자열(String)로 처리하자.
* 필터인 BufferedReader를 사용하면 readLine 메서드를 사용해서 문자열(String)을 한 줄 씩 읽어올 수 있다.
* InputStreamReader를 사용해서 바이트를 문자(char)로 읽어온다. 읽어온 문자(char)를 문자열(String)로 처리하자. 필터인 BufferedReader를 사용하면
* readLine 메서드를 사용해서 문자열(String)을 한 줄 씩 읽어올 수 있다.
*/
@Test
void BufferedReader_사용하여_문자열을_읽어온다() {
void BufferedReader_사용하여_문자열을_읽어온다() throws IOException {
final String emoji = String.join("\r\n",
"😀😃😄😁😆😅😂🤣🥲☺️😊",
"😇🙂🙃😉😌😍🥰😘😗😙😚",
"😋😛😝😜🤪🤨🧐🤓😎🥸🤩",
"");
final InputStream inputStream = new ByteArrayInputStream(emoji.getBytes());
final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));

final StringBuilder actual = new StringBuilder();
String line = bufferedReader.readLine();
while (line != null) {
actual.append(line + "\r\n");
line = bufferedReader.readLine();
}

assertThat(actual).hasToString(emoji);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
package org.apache.catalina.connector;

import org.apache.coyote.http11.Http11Processor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.ServerSocket;
import java.net.Socket;
import org.apache.coyote.http11.Http11Processor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Connector implements Runnable {

Expand Down
Loading

0 comments on commit 187f9c2

Please sign in to comment.