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단계] 우르(김현우) 미션 제출합니다. #481

Merged
merged 30 commits into from
Sep 13, 2023
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
3af2ce9
feat : 학습 테스트
java-saeng Sep 6, 2023
1b3c576
feat : Cookie 클래스 생성
java-saeng Sep 6, 2023
f15ec86
feat : Session 클래스 생성
java-saeng Sep 6, 2023
865345e
feat : HTTP 요청을 파싱하는 유틸리티 클래스 생성
java-saeng Sep 6, 2023
c3d4faa
feat : HttpRequestLine 클래스 추가
java-saeng Sep 6, 2023
67473e0
feat : RequestBody 클래스 추가
java-saeng Sep 6, 2023
a04d297
feat : QueryString 클래스 추가
java-saeng Sep 6, 2023
1aa6444
refactor : queryString, body, cookie, httpRequestLine 추가
java-saeng Sep 6, 2023
066f291
feat : 회원 가입 및 로그인 기능 추가
java-saeng Sep 6, 2023
da41b0d
refactor : path -> uri 로 변수 변경
java-saeng Sep 6, 2023
d1d8c11
feat : handler 추가
java-saeng Sep 6, 2023
91dceb5
refactor : handle 반환값 void 로 변경
java-saeng Sep 7, 2023
728620e
refactor : handler composite 반환값 변경
java-saeng Sep 7, 2023
d096920
feat : Http 응답 관련 header, body, status line 객체 추가
java-saeng Sep 7, 2023
ecd6ddd
feat : Handler 반환값을 void로 변경하여 response에 값을 넣도록 수정
java-saeng Sep 7, 2023
f24fd29
feat : HttpRequest, HttpResponse 공통적으로 Cookie 사용하도록 수정
java-saeng Sep 8, 2023
e7b66dd
feat : 매 요청마다 세션이 있으면 세션 매니저에 넣어주고, 없으면 생성하여 넣어주기
java-saeng Sep 8, 2023
8e403ca
feat : 로그인 관련 로직 수정
java-saeng Sep 8, 2023
c9ebbb1
refactor : 세션 추가할 때 기존 세션이 가지고 있는 id로 저장
java-saeng Sep 8, 2023
2184f8d
refactor : LoginPageHandler는 정적 Handler 로 수정
java-saeng Sep 8, 2023
fb55927
refactor : Http11Processor의 HandlerComposite 타입을 Handler 인터페이스로 수정
java-saeng Sep 8, 2023
d792f64
refactor : 동시성 보장을 위한 ConcurrentHashMap 사용
java-saeng Sep 11, 2023
fa4598e
refactor : max thread 설정
java-saeng Sep 11, 2023
656e694
test : 테스트를 위한 getter 추가
java-saeng Sep 11, 2023
843184c
feat : 학습 테스트
java-saeng Sep 11, 2023
22ce601
refactor : 공백 추가
java-saeng Sep 11, 2023
84c9d52
refactor : 코드 스멜 제거
java-saeng Sep 11, 2023
d60c205
refactor : redirect URL 상대 경로로 설정
java-saeng Sep 12, 2023
e8ec3a5
refactor : HttpResponse에 redirect 행위 캡술화
java-saeng Sep 12, 2023
c8f3e61
refactor : 핸들러가 존재하지 않을 때 exception 추가
java-saeng 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
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
package cache.com.example.cachecontrol;

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) {
final WebContentInterceptor interceptor = new WebContentInterceptor();
interceptor.addCacheMapping(CacheControl.noCache().cachePrivate(), "/*");
registry.addInterceptor(interceptor);
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,29 @@
package cache.com.example.etag;

import static cache.com.example.version.CacheBustingWebConfig.PREFIX_STATIC_RESOURCES;

import java.util.Collections;
import java.util.List;
import javax.servlet.Filter;
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() {

final FilterRegistrationBean<ShallowEtagHeaderFilter> bean = new FilterRegistrationBean<>();
final ShallowEtagHeaderFilter filter = new ShallowEtagHeaderFilter();
bean.setFilter(filter);
bean.setUrlPatterns(List.of(
"/etag",
PREFIX_STATIC_RESOURCES + "/*"
));

return bean;
}
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,33 @@
package cache.com.example.version;

import java.time.Duration;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.CacheControl;
import org.springframework.web.filter.ShallowEtagHeaderFilter;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CacheBustingWebConfig implements WebMvcConfigurer {

public static final String PREFIX_STATIC_RESOURCES = "/resources";
public static final String PREFIX_STATIC_RESOURCES = "/resources";

private final ResourceVersion version;
private final ResourceVersion version;

@Autowired
public CacheBustingWebConfig(ResourceVersion version) {
this.version = version;
}
@Autowired
public CacheBustingWebConfig(ResourceVersion version) {
this.version = version;
}

@Override
public void addResourceHandlers(final ResourceHandlerRegistry registry) {
registry.addResourceHandler(PREFIX_STATIC_RESOURCES + "/" + version.getVersion() + "/**")
.addResourceLocations("classpath:/static/");
}
@Override
public void addResourceHandlers(final ResourceHandlerRegistry registry) {
registry.addResourceHandler(PREFIX_STATIC_RESOURCES + "/" + version.getVersion() + "/**")
.addResourceLocations("classpath:/static/")
.setUseLastModified(true)
.setCacheControl(CacheControl.maxAge(Duration.ofDays(365)).cachePublic());
}
}
3 changes: 3 additions & 0 deletions study/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ handlebars:
suffix: .html

server:
compression:
enabled: true
min-response-size: 10
tomcat:
accept-count: 1
max-connections: 1
Expand Down
106 changes: 51 additions & 55 deletions study/src/test/java/thread/stage0/ThreadPoolsTest.java
Original file line number Diff line number Diff line change
@@ -1,66 +1,62 @@
package thread.stage0;

import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.assertj.core.api.Assertions.assertThat;

import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* 스레드 풀은 무엇이고 어떻게 동작할까?
* 테스트를 통과시키고 왜 해당 결과가 나왔는지 생각해보자.
*
* Thread Pools
* https://docs.oracle.com/javase/tutorial/essential/concurrency/pools.html
*
* Introduction to Thread Pools in Java
* https://www.baeldung.com/thread-pool-java-and-guava
* 스레드 풀은 무엇이고 어떻게 동작할까? 테스트를 통과시키고 왜 해당 결과가 나왔는지 생각해보자.
* <p>
* Thread Pools https://docs.oracle.com/javase/tutorial/essential/concurrency/pools.html
* <p>
* Introduction to Thread Pools in Java https://www.baeldung.com/thread-pool-java-and-guava
*/
class ThreadPoolsTest {

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

@Test
void testNewFixedThreadPool() {
final var executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(2);
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;

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

@Test
void testNewCachedThreadPool() {
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 expectedQueueSize = 0;

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

private Runnable logWithSleep(final String message) {
return () -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.info(message);
};
}
private static final Logger log = LoggerFactory.getLogger(ThreadPoolsTest.class);

@Test
void testNewFixedThreadPool() {
final ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(2);
executor.submit(logWithSleep("hello fixed thread pools"));
executor.submit(logWithSleep("hello fixed thread pools"));
executor.submit(logWithSleep("hello fixed thread pools"));

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

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

@Test
void testNewCachedThreadPool() {
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 = 3;
final int expectedQueueSize = 0;

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

private Runnable logWithSleep(final String message) {
return () -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.info(message);
};
}
}
28 changes: 14 additions & 14 deletions study/src/test/java/thread/stage0/ThreadTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,20 @@ void testExtendedThread() throws InterruptedException {
thread.join();
}

private static final class ExtendedThread extends Thread {

private String message;

public ExtendedThread(final String message) {
this.message = message;
}

@Override
public void run() {
log.info(message);
}
}

/**
* Runnable 인터페이스를 사용하는 방법도 있다.
* 주석을 참고하여 테스트 코드를 작성하고, 테스트를 실행시켜서 메시지가 잘 나오는지 확인한다.
Expand All @@ -52,20 +66,6 @@ void testRunnableThread() throws InterruptedException {
thread.join();
}

private static final class ExtendedThread extends Thread {

private String message;

public ExtendedThread(final String message) {
this.message = message;
}

@Override
public void run() {
log.info(message);
}
}

private static final class RunnableThread implements Runnable {

private String message;
Expand Down
7 changes: 4 additions & 3 deletions tomcat/src/main/java/org/apache/catalina/Manager.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import jakarta.servlet.http.HttpSession;

import java.io.IOException;
import org.apache.coyote.request.Session;

/**
* A <b>Manager</b> manages the pool of Sessions that are associated with a
Expand All @@ -29,7 +30,7 @@ public interface Manager {
*
* @param session Session to be added
*/
void add(HttpSession session);
void add(Session session);

/**
* Return the active Session, associated with this Manager, with the
Expand All @@ -45,12 +46,12 @@ public interface Manager {
* @return the request session or {@code null} if a session with the
* requested ID could not be found
*/
HttpSession findSession(String id) throws IOException;
Session findSession(String id) throws IOException;

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

import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import org.apache.coyote.handler.Handler;
import org.apache.coyote.handler.HandlerComposite;
import org.apache.coyote.http11.Http11Processor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -17,19 +20,27 @@ public class Connector implements Runnable {

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

private final ServerSocket serverSocket;
private boolean stopped;
private final List<Handler> handlers;
private final Executor executor;

public Connector(final List<Handler> handlers) {
this(DEFAULT_PORT, DEFAULT_ACCEPT_COUNT, handlers);
this(DEFAULT_PORT, DEFAULT_ACCEPT_COUNT, handlers, MAX_THREADS);
}

public Connector(final int port, final int acceptCount, final List<Handler> handlers) {
public Connector(
final int port,
final int acceptCount,
final List<Handler> handlers,
final int maxThreads
) {
this.serverSocket = createServerSocket(port, acceptCount);
this.stopped = false;
this.handlers = handlers;
executor = Executors.newFixedThreadPool(maxThreads);
Copy link

Choose a reason for hiding this comment

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

학습 질문 중 '250개의 쓰레드가 모두 사용중일 때, 최대 100개의 연결을 대기하게 만드려면 어떻게 해야 하는가?' 라는 질문이 있었어요.

우르는 이걸 어떻게 풀어내야 한다고 생각하시나요?

Copy link
Author

Choose a reason for hiding this comment

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

스레드 풀의 maxThread 의 개수를 250개로 설정하고 acceptCount를 100으로 하면 되지 않을까 싶습니다!!!

땡칠은 어떻게 생각하시나요?

Copy link

Choose a reason for hiding this comment

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

저도 같게 생각했습니다.
'100개 연결 대기' 부분이 OS가 관리하는 backlog 부분이고, acceptCount로 조절 가능하다고 생각했어요!

}

private ServerSocket createServerSocket(final int port, final int acceptCount) {
Expand Down Expand Up @@ -70,8 +81,8 @@ private void process(final Socket connection) {
if (connection == null) {
return;
}
var processor = new Http11Processor(connection, handlers);
new Thread(processor).start();
var processor = new Http11Processor(connection, new HandlerComposite(handlers));
executor.execute(processor);
}

public void stop() {
Expand Down
8 changes: 7 additions & 1 deletion tomcat/src/main/java/org/apache/catalina/startup/Tomcat.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
import org.apache.catalina.connector.Connector;
import org.apache.coyote.handler.Handler;
import org.apache.coyote.handler.LoginHandler;
import org.apache.coyote.handler.LoginPageHandler;
import org.apache.coyote.handler.RegisterHandler;
import org.apache.coyote.handler.RegisterPageHandler;
import org.apache.coyote.handler.StaticFileHandler;
import org.apache.coyote.handler.WelcomePageHandler;
import org.slf4j.Logger;
Expand Down Expand Up @@ -34,7 +37,10 @@ public List<Handler> getContext() {
return List.of(
new LoginHandler(),
new StaticFileHandler(),
new WelcomePageHandler()
new LoginPageHandler(),
new WelcomePageHandler(),
new RegisterHandler(),
new RegisterPageHandler()
);
}
}
7 changes: 4 additions & 3 deletions tomcat/src/main/java/org/apache/coyote/handler/Handler.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@

import java.io.IOException;
import org.apache.coyote.request.HttpRequest;
import org.apache.coyote.response.HttpResponse;

public interface Handler {

boolean canHandle(final HttpRequest httpRequest);

String handle(final HttpRequest httpRequest) throws IOException;
void handle(final HttpRequest httpRequest, final HttpResponse httpResponse) throws IOException;
Copy link

Choose a reason for hiding this comment

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

저도 주말에 알게되었는데 Request, Response가 각각 인자로 들어가는게 요구사항이었더라구요!

우르는 왜 이런 디자인을 가져야 한다고 생각하시나요?

Copy link
Author

@java-saeng java-saeng Sep 12, 2023

Choose a reason for hiding this comment

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

request에 맞는 controller를 찾기 전에 response에 관한 전처리를 해줘야하는 경우가 있을까? 에 대해서 생각해봤습니다.

대표적인게 Accept라고 생각이 드는데, 서버에서 지원하는 Content-Type이 없으면 바로 response 406 Not Acceptable 를 응답하는 경우도 있지 않을까라는 생각이 들었습니다.

이게 맞는건지는 모르겠지만,, 톰켓이 설계한 이유 중에 아주 사소한 것 중 하나이지 않을까 싶습니다,,

Copy link

Choose a reason for hiding this comment

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

그러게요 ㅎㅎ 컨트롤러에서 정한 응답 외에 오류 응답등이 있을 수 있겠네요
이런 응답 수정을 언제든지, 쉽게 하려고 이렇게 만들지 않았을까 싶어요.
원래 의도는 톰캣을 만든 본인만 알겠죠!


default String safeHandle(final HttpRequest httpRequest) {
default void safeHandle(final HttpRequest httpRequest, final HttpResponse httpResponse) {
try {
return handle(httpRequest);
handle(httpRequest, httpResponse);
} catch (IOException e) {
throw new IllegalArgumentException("I/O 작업 관련 에러가 발생했습니다.");
}
Expand Down
Loading