Skip to content

Commit

Permalink
[톰캣 구현하기 1, 2단계] 트레 미션 제출합니다. (#510)
Browse files Browse the repository at this point in the history
* chore: .gitignore 학습 테스트를 버전 관리하지 않도록 수정

* feat: /index.html 응답하기 구현

* refactor: 200 응답 문자열 생성하는 부분을 메서드로 분리

* refactor: index.html 이외 다른 파일명이 들어와도 리소스 반환하도록 수정

* refactor: 메서드명 간결화

* feat: 리소스 존재하지 않는 경우 404.html 반환

* feat: Query String 파싱 구현

* fix: 올바른 Content-Type 지정

* fix: id, pw 존재하지 않아도 예외 발생하지 않도록 수정, /login으로 로그인 페이지 접속 가능하도록 수정

* refactor: Response Message를 생성하는 메서드에서 상태 코드를 인자로 받도록 수정

* feat: 로그인 시 리다이렉트 구현

* feat: 로그인을 POST 메서드로 변경

* feat: 회원가입 구현

* fix: .js 파일의 Content-Type 지정

* feat: HttpCookie 클래스 생성

* feat: Cookie에 JSESSIONID 값 저장하기 구현

* refactor: 상수 사용하도록 일부 수정

* fix: jakarta의 HttpSession을 사용하지 않도록 수정

* feat: 세션 구현

* refactor: Request 관련 로직을 별도의 클래스로 분리

* refactor: 헤더 읽기, body 읽기 로직을 메서드로 분리

* refactor: 쿠키 관련 로직을 분리

* refactor: HTTP 응답 로직 분리

* refactor: 메서드명 및 시그니처명 변경

* refactor: inline variable 적용

* refactor: 핸들러 메서드 분리

* refactor: 중복 제거

* refactor: 메서드명 변경

* refactor: 불필요한 if문 depth 제거

* chore: 패키지 구조 변경

* refactor: set-cookie 하는 부분 메서드 분리

* fix: Location 헤더로 올바르게 리다이렉션 하도록 수정

* refactor: 메서드명 수정

* fix: request line 마지막에 공백 추가

* refactor: 단순 텍스트를 응답하는 경우에 대한 응답 메서드 작성

* test: 깨지는 테스트 수정

* refactor: 메서드명 변경

* feat: 존재하지 않는 리소스에 접근하는 경우 404 페이지를 보여줌

* refactor: Location 헤더를 enum으로 관리

* chore: print문 삭제

* refactor: Set-Cookie 헤더를 상수가 아닌 enum으로 변경

* test: FileTest 작성

* test: IOStreamTest 작성

* chore: 학습 테스트를 다시 버전관리 하도록 수정

* fix: remove implementation logback-classic on gradle (#501)

(cherry picked from commit fed02f6)

* fix: add threads min-spare configuration on properties (#502)

(cherry picked from commit 7e91356)

* chore: Thymeleaf 의존성 추가

* feat: 휴리스틱 캐싱 제거

* chore: HTTP 압축 설정

* feat: ETag 추가

* feat: .js와 .css 파일은 1년 동안 캐시 적용

* refactor: interceptor 구현체를 지우고 WebContentIntercepor 사용

* refactor: 헤더의 이름을 enum에서 꺼내오도록 수정

* refactor: 미사용 메서드 제거

* refactor: depth 개선

* refactor: request에서 cookie 가져오는 로직 메서드로 분리

* refactor: HttpCookie에 세션 관련 메서드 생성

* refactor: Set-Cookie 관련 로직 개선

* refactor: if문 개선

* refactor: HttpHeaders -> HttpHeaderType

* refactor: 반복문을 스트림으로 개선

* refactor: 각종 delimiters 상수화

* refactor: 중복되는 build() 로직 개선

* refactor: CRLF 상수화
  • Loading branch information
takoyakimchi authored Sep 11, 2024
1 parent 0b698a2 commit 28e85b8
Show file tree
Hide file tree
Showing 29 changed files with 999 additions and 99 deletions.
2 changes: 1 addition & 1 deletion study/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ repositories {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'ch.qos.logback:logback-classic:1.5.7'
implementation 'org.apache.commons:commons-lang3:3.14.0'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1'
implementation 'pl.allegro.tech.boot:handlebars-spring-boot-starter:0.4.1'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.assertj:assertj-core:3.26.0'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
package cache.com.example.cachecontrol;

import static java.util.concurrent.TimeUnit.DAYS;

import org.springframework.context.annotation.Configuration;
import org.springframework.http.CacheControl;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.mvc.WebContentInterceptor;

@Configuration
public class CacheWebConfig implements WebMvcConfigurer {

@Override
public void addInterceptors(final InterceptorRegistry registry) {
WebContentInterceptor noCacheIntercepter = new WebContentInterceptor();
noCacheIntercepter.addCacheMapping(CacheControl.noCache().cachePrivate(), "/**");
registry.addInterceptor(noCacheIntercepter)
.excludePathPatterns("/**/*.js", "/**/*.css");

WebContentInterceptor cacheIntercepter = new WebContentInterceptor();
cacheIntercepter.addCacheMapping(CacheControl.maxAge(365, DAYS).cachePublic(), "/**");
registry.addInterceptor(cacheIntercepter)
.addPathPatterns("/**/*.js", "/**/*.css");
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
package cache.com.example.etag;

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.ShallowEtagHeaderFilter;

@Configuration
public class EtagFilterConfiguration {

// @Bean
// public FilterRegistrationBean<ShallowEtagHeaderFilter> shallowEtagHeaderFilter() {
// return null;
// }
@Bean
public FilterRegistrationBean<ShallowEtagHeaderFilter> shallowEtagHeaderFilter() {
FilterRegistrationBean<ShallowEtagHeaderFilter> filter
= new FilterRegistrationBean<>(new ShallowEtagHeaderFilter());
filter.addUrlPatterns("/etag", "*.js", "*.css");
return filter;
}
}
4 changes: 4 additions & 0 deletions study/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,8 @@ server:
accept-count: 1
max-connections: 1
threads:
min-spare: 2
max: 2
compression:
enabled: true
min-response-size: 10
42 changes: 20 additions & 22 deletions study/src/test/java/study/FileTest.java
Original file line number Diff line number Diff line change
@@ -1,53 +1,51 @@
package study;

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

import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.nio.file.Paths;
import java.util.List;

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

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

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

// todo
final String actual = "";
URL url = getClass().getClassLoader().getResource(fileName);
final String actual = url.toString();

assertThat(actual).endsWith(fileName);
}

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

// todo
final Path path = null;

// todo
final List<String> actual = Collections.emptyList();
URL url = getClass().getClassLoader().getResource(fileName);
final Path path = Paths.get(url.toURI());
final List<String> actual = Files.readAllLines(path);

assertThat(actual).containsOnly("nextstep");
}
Expand Down
27 changes: 14 additions & 13 deletions study/src/test/java/study/IOStreamTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,7 @@ class OutputStream_학습_테스트 {
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 메서드를 사용해서 테스트를 통과시킨다
*/
outputStream.write(bytes);

final String actual = outputStream.toString();

Expand All @@ -72,13 +68,12 @@ class OutputStream_학습_테스트 {
@Test
void BufferedOutputStream을_사용하면_버퍼링이_가능하다() throws IOException {
final OutputStream outputStream = mock(BufferedOutputStream.class);

/**
* todo
* flush를 사용해서 테스트를 통과시킨다.
* ByteArrayOutputStream과 어떤 차이가 있을까?
*/

outputStream.flush();
verify(outputStream, atLeastOnce()).flush();
outputStream.close();
}
Expand All @@ -96,6 +91,7 @@ class OutputStream_학습_테스트 {
* try-with-resources를 사용한다.
* java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다.
*/
outputStream.close();

verify(outputStream, atLeastOnce()).close();
}
Expand Down Expand Up @@ -128,7 +124,8 @@ class InputStream_학습_테스트 {
* todo
* inputStream에서 바이트로 반환한 값을 문자열로 어떻게 바꿀까?
*/
final String actual = "";
final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
final String actual = bufferedReader.readLine();

assertThat(actual).isEqualTo("🤩");
assertThat(inputStream.read()).isEqualTo(-1);
Expand All @@ -148,7 +145,7 @@ class InputStream_학습_테스트 {
* try-with-resources를 사용한다.
* java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다.
*/

inputStream.close();
verify(inputStream, atLeastOnce()).close();
}
}
Expand All @@ -169,12 +166,12 @@ class FilterStream_학습_테스트 {
* 버퍼 크기를 지정하지 않으면 버퍼의 기본 사이즈는 얼마일까?
*/
@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());
Expand All @@ -197,7 +194,7 @@ class InputStreamReader_학습_테스트 {
* 필터인 BufferedReader를 사용하면 readLine 메서드를 사용해서 문자열(String)을 한 줄 씩 읽어올 수 있다.
*/
@Test
void BufferedReader를_사용하여_문자열을_읽어온다() {
void BufferedReader를_사용하여_문자열을_읽어온다() throws IOException {
final String emoji = String.join("\r\n",
"😀😃😄😁😆😅😂🤣🥲☺️😊",
"😇🙂🙃😉😌😍🥰😘😗😙😚",
Expand All @@ -206,6 +203,10 @@ class InputStreamReader_학습_테스트 {
final InputStream inputStream = new ByteArrayInputStream(emoji.getBytes());

final StringBuilder actual = new StringBuilder();
final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
while (bufferedReader.ready()) {
actual.append(bufferedReader.readLine()).append("\r\n");
}

assertThat(actual).hasToString(emoji);
}
Expand Down
22 changes: 17 additions & 5 deletions tomcat/src/main/java/com/techcourse/db/InMemoryUserRepository.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
package com.techcourse.db;

import com.techcourse.exception.UncheckedServletException;
import com.techcourse.model.User;

import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;

public class InMemoryUserRepository {
Expand All @@ -19,9 +18,22 @@ public static void save(User user) {
database.put(user.getAccount(), user);
}

public static Optional<User> findByAccount(String account) {
return Optional.ofNullable(database.get(account));
public static boolean exists(String account, String password) {
return database.containsKey(account)
&& database.get(account).checkPassword(password);
}

public static User getByAccount(String account) {
if (database.containsKey(account)) {
return database.get(account);
}
throw new UncheckedServletException("유저가 존재하지 않습니다.");
}

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

private InMemoryUserRepository() {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,8 @@ public class UncheckedServletException extends RuntimeException {
public UncheckedServletException(Exception e) {
super(e);
}

public UncheckedServletException(String message) {
super(message);
}
}
40 changes: 17 additions & 23 deletions tomcat/src/main/java/org/apache/catalina/Manager.java
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
package org.apache.catalina;

import jakarta.servlet.http.HttpSession;

import java.io.IOException;
import java.util.Optional;
import org.apache.catalina.session.Session;

/**
* A <b>Manager</b> manages the pool of Sessions that are associated with a
* particular Container. Different Manager implementations may support
* value-added features such as the persistent storage of session data,
* as well as migrating sessions for distributable web applications.
* A <b>Manager</b> manages the pool of Sessions that are associated with a particular Container. Different Manager
* implementations may support value-added features such as the persistent storage of session data, as well as migrating
* sessions for distributable web applications.
* <p>
* In order for a <code>Manager</code> implementation to successfully operate
* with a <code>Context</code> implementation that implements reloading, it
* must obey the following constraints:
* In order for a <code>Manager</code> implementation to successfully operate with a <code>Context</code> implementation
* that implements reloading, it must obey the following constraints:
* <ul>
* <li>Must implement <code>Lifecycle</code> so that the Context can indicate
* that a restart is required.
Expand All @@ -29,28 +27,24 @@ public interface Manager {
*
* @param session Session to be added
*/
void add(HttpSession session);
void add(String id, Session session);

/**
* Return the active Session, associated with this Manager, with the
* specified session id (if any); otherwise return <code>null</code>.
* Return the active Session, associated with this Manager, with the specified session id (if any); otherwise
* return
* <code>null</code>.
*
* @param id The session id for the session to be returned
*
* @exception IllegalStateException if a new session cannot be
* instantiated for any reason
* @exception IOException if an input/output error occurs while
* processing this request
*
* @return the request session or {@code null} if a session with the
* requested ID could not be found
* @return the request session or {@code null} if a session with the requested ID could not be found
* @throws IllegalStateException if a new session cannot be instantiated for any reason
* @throws IOException if an input/output error occurs while processing this request
*/
HttpSession findSession(String id) throws IOException;
Optional<Session> findSession(String id) throws IOException;

/**
* Remove this Session from the active Sessions for this Manager.
*
* @param session Session to be removed
* @param id Session to be removed
*/
void remove(HttpSession session);
void remove(String id);
}
21 changes: 21 additions & 0 deletions tomcat/src/main/java/org/apache/catalina/session/Session.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.apache.catalina.session;

import com.techcourse.model.User;
import java.util.HashMap;
import java.util.Map;

public class Session {

private static final String USER_SESSION_NAME = "user";

private final Map<String, Object> values = new HashMap<>();

private Session() {
}

public static Session ofUser(User user) {
Session session = new Session();
session.values.put(USER_SESSION_NAME, user);
return session;
}
}
Loading

0 comments on commit 28e85b8

Please sign in to comment.