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단계] 도이(유도영) 미션 제출합니다. #453

Merged
merged 36 commits into from
Sep 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
92b35c9
test: Cache 학습 테스트 작성
yoondgu Sep 5, 2023
cd9a2f7
style: final 키워드 추가
yoondgu Sep 5, 2023
d7c55aa
style: 타입 추론으로 코드 통일
yoondgu Sep 5, 2023
1e30f69
refactor: 적절하지 않은 정적 팩터리 메서드 네이밍 수정
yoondgu Sep 5, 2023
3dea82f
refactor: 일부 메서드 분리, Optional 로직 보완, 가독성 개선
yoondgu Sep 5, 2023
fd68631
refactor: 쿼리 스트링 파싱 클래스 네이밍 변경 및 범용성 있게 수정
yoondgu Sep 5, 2023
bb4c233
refactor: Session, SessionManager 구조 변경
yoondgu Sep 5, 2023
edfeb63
rename: 요청 핸들러 분기 클래스명 FirstController로 변경
yoondgu Sep 5, 2023
cc56b3a
refactor: 헤더 유형 별로 관리하도록 클래스 정의
yoondgu Sep 6, 2023
8bb0991
refactor: Response Builder 적용
yoondgu Sep 6, 2023
ce2dd69
refactor: uri 기준으로 메서드 매핑하도록 전체 수정, 클래스 네이밍 전반 수정
yoondgu Sep 7, 2023
1ba138f
fix: Enum에 없는 헤더 못읽는 오류 수정
yoondgu Sep 7, 2023
eac7d14
refactor: RequestLine 객체 포장
yoondgu Sep 7, 2023
a86e454
refactor: StatusLine 객체 포장
yoondgu Sep 7, 2023
a9dc6d0
chore: Servlet 관련 클래스들 catalina 패키지로 이동 (3단계 완료)
yoondgu Sep 7, 2023
0133924
refactor: SessionManager가 Manger 인터페이스 구현하도록 변경
yoondgu Sep 7, 2023
f7942be
test: Thread 테스트 작성
yoondgu Sep 7, 2023
dd3e648
rename: MimeType 클래스명 변경
yoondgu Sep 8, 2023
589135d
test: Thread 학습 테스트 2단계 작성
yoondgu Sep 10, 2023
026e4dd
feat: 스레드 풀 적, 세션 매니저 동시성 컬렉션 적용
yoondgu Sep 10, 2023
b0bda83
chore: Session 패키지 coyote/http11로 이동
yoondgu Sep 10, 2023
3fe2930
refactor: HandlerMapping 구조 변경, 구현체 의존성 주입 구현
yoondgu Sep 10, 2023
6c5730f
refactor: 정적 파일 핸들러는 톰캣에서 관리하도록 변경, 어색한 네이밍 수정
yoondgu Sep 10, 2023
73535c5
refactor: 3단계 요구사항에 맞게 컨트롤러(Servlet) 인터페이스 수정
yoondgu Sep 10, 2023
54a9418
refactor: ServletResponse 클래스 분리, 패키지 이동
yoondgu Sep 10, 2023
5bb6dab
refactor: 서블릿 패키지명 및 구조 수정
yoondgu Sep 10, 2023
de048a2
rename: ServletXXX -> HttpServletXXX 으로 네이밍 수정
yoondgu Sep 10, 2023
ee2a088
rename: 정적 파일에 대한 핸들러도 서블릿으로 취급
yoondgu Sep 10, 2023
85a3522
rename: 톰캣에 필요한 객체들 Container로 전달
yoondgu Sep 10, 2023
2da62d8
refactor: HttpServletRequest 클래스 정의
yoondgu Sep 10, 2023
3ff6f80
refactor: session 패키지 catalina로 이동
yoondgu Sep 10, 2023
031c166
refactor: 예외에 따른 응답 반환 로직 Container 로 이동
yoondgu Sep 10, 2023
2745e8c
refactor: 메서드 순서 수정
yoondgu Sep 10, 2023
4e64504
test: 테스트 패키지 수정
yoondgu Sep 10, 2023
a474269
fix: 존재하지 않는 정적 파일 요청 시 404 페이지로 리다이렉트, SVG mimeType 추가
yoondgu Sep 10, 2023
48f7d39
fix: 500 에러 발생 시 500.html 페이지로 리다이렉트하도록 수정
yoondgu Sep 10, 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,19 @@
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) {
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,22 @@
package cache.com.example.etag;

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

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> registration = new FilterRegistrationBean<>();

registration.setFilter(new ShallowEtagHeaderFilter());
registration.addUrlPatterns("/etag", PREFIX_STATIC_RESOURCES + "/*");

return registration;
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package cache.com.example.version;

import java.time.Duration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.CacheControl;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

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

server:
tomcat:
accept-count: 1
max-connections: 1
accept-count: 8
max-connections: 2
threads:
max: 2
compression:
enabled: true
min-response-size: 10
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package cache.com.example;

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

import cache.com.example.version.ResourceVersion;
import java.time.Duration;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -10,10 +13,6 @@
import org.springframework.http.HttpHeaders;
import org.springframework.test.web.reactive.server.WebTestClient;

import java.time.Duration;

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

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class GreetingControllerTest {

Expand All @@ -35,6 +34,7 @@ void testNoCachePrivate() {
.expectHeader().cacheControl(CacheControl.noCache().cachePrivate())
.expectBody(String.class).returnResult();

log.info("{}", response);
log.info("response body\n{}", response.getResponseBody());
}

Expand All @@ -51,6 +51,7 @@ void testCompression() {
.expectHeader().valueEquals(HttpHeaders.TRANSFER_ENCODING, "chunked")
.expectBody(String.class).returnResult();

log.info("{}", response);
log.info("response body\n{}", response.getResponseBody());
}

Expand All @@ -64,14 +65,13 @@ void testETag() {
.expectHeader().exists(HttpHeaders.ETAG)
.expectBody(String.class).returnResult();

log.info("{}", response);
log.info("response body\n{}", response.getResponseBody());
}

/**
* http://localhost:8080/resource-versioning
* 위 url의 html 파일에서 사용하는 js, css와 같은 정적 파일에 캐싱을 적용한다.
* 보통 정적 파일을 캐싱 무효화하기 위해 캐싱과 함께 버전을 적용시킨다.
* 정적 파일에 변경 사항이 생기면 배포할 때 버전을 바꿔주면 적용된 캐싱을 무효화(Caching Busting)할 수 있다.
* http://localhost:8080/resource-versioning 위 url의 html 파일에서 사용하는 js, css와 같은 정적 파일에 캐싱을 적용한다. 보통 정적 파일을 캐싱 무효화하기
* 위해 캐싱과 함께 버전을 적용시킨다. 정적 파일에 변경 사항이 생기면 배포할 때 버전을 바꿔주면 적용된 캐싱을 무효화(Caching Busting)할 수 있다.
*/
@Test
void testCacheBustingOfStaticResources() {
Expand Down
31 changes: 13 additions & 18 deletions study/src/test/java/thread/stage0/SynchronizationTest.java
Original file line number Diff line number Diff line change
@@ -1,34 +1,29 @@
package thread.stage0;

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

import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

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

/**
* 다중 스레드 환경에서 두 개 이상의 스레드가 변경 가능한(mutable) 공유 데이터를 동시에 업데이트하면 경쟁 조건(race condition)이 발생한다.
* 자바는 공유 데이터에 대한 스레드 접근을 동기화(synchronization)하여 경쟁 조건을 방지한다.
* 동기화된 블록은 하나의 스레드만 접근하여 실행할 수 있다.
*
* Synchronization
* https://docs.oracle.com/javase/tutorial/essential/concurrency/sync.html
* 다중 스레드 환경에서 두 개 이상의 스레드가 변경 가능한(mutable) 공유 데이터를 동시에 업데이트하면 경쟁 조건(race condition)이 발생한다. 자바는 공유 데이터에 대한 스레드 접근을
* 동기화(synchronization)하여 경쟁 조건을 방지한다. 동기화된 블록은 하나의 스레드만 접근하여 실행할 수 있다.
* <p>
* Synchronization https://docs.oracle.com/javase/tutorial/essential/concurrency/sync.html
*/
class SynchronizationTest {

/**
* 테스트가 성공하도록 SynchronizedMethods 클래스에 동기화를 적용해보자.
* synchronized 키워드에 대하여 찾아보고 적용하면 된다.
*
* Guide to the Synchronized Keyword in Java
* https://www.baeldung.com/java-synchronized
* 테스트가 성공하도록 SynchronizedMethods 클래스에 동기화를 적용해보자. synchronized 키워드에 대하여 찾아보고 적용하면 된다.
* <p>
* Guide to the Synchronized Keyword in Java https://www.baeldung.com/java-synchronized
*/
@Test
void testSynchronized() throws InterruptedException {
var executorService = Executors.newFixedThreadPool(3);
var synchronizedMethods = new SynchronizedMethods();
final var executorService = Executors.newFixedThreadPool(3);
final var synchronizedMethods = new SynchronizedMethods();

IntStream.range(0, 1000)
.forEach(count -> executorService.submit(synchronizedMethods::calculate));
Expand All @@ -41,15 +36,15 @@ private static final class SynchronizedMethods {

private int sum = 0;

public void calculate() {
public synchronized void calculate() {
setSum(getSum() + 1);
}

public int getSum() {
return sum;
}

public void setSum(int sum) {
public void setSum(final int sum) {
this.sum = sum;
}
}
Expand Down
30 changes: 13 additions & 17 deletions study/src/test/java/thread/stage0/ThreadPoolsTest.java
Original file line number Diff line number Diff line change
@@ -1,23 +1,19 @@
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 {

Expand All @@ -31,8 +27,8 @@ void testNewFixedThreadPool() {
executor.submit(logWithSleep("hello fixed thread pools"));

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

assertThat(expectedPoolSize).isEqualTo(executor.getPoolSize());
assertThat(expectedQueueSize).isEqualTo(executor.getQueue().size());
Expand All @@ -46,7 +42,7 @@ void testNewCachedThreadPool() {
executor.submit(logWithSleep("hello cached thread pools"));

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

assertThat(expectedPoolSize).isEqualTo(executor.getPoolSize());
Expand All @@ -57,7 +53,7 @@ private Runnable logWithSleep(final String message) {
return () -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
} catch (final InterruptedException e) {
throw new RuntimeException(e);
}
log.info(message);
Expand Down
19 changes: 8 additions & 11 deletions study/src/test/java/thread/stage1/ConcurrencyTest.java
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
package thread.stage1;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.Test;

/**
* 스레드를 다룰 때 어떤 상황을 조심해야 할까?
* - 상태를 가진 한 객체를 여러 스레드에서 동시에 접근할 경우
* - static 변수를 가진 객체를 여러 스레드에서 동시에 접근할 경우
*
* 위 경우는 동기화(synchronization)를 적용시키거나 객체가 상태를 갖지 않도록 한다.
* 객체를 불변 객체로 만드는 방법도 있다.
*
* 웹서버는 여러 사용자가 동시에 접속을 시도하기 때문에 동시성 이슈가 생길 수 있다.
* 어떤 사례가 있는지 아래 테스트 코드를 통해 알아보자.
* 스레드를 다룰 때 어떤 상황을 조심해야 할까? - 상태를 가진 한 객체를 여러 스레드에서 동시에 접근할 경우 - static 변수를 가진 객체를 여러 스레드에서 동시에 접근할 경우
* <p>
* 위 경우는 동기화(synchronization)를 적용시키거나 객체가 상태를 갖지 않도록 한다. 객체를 불변 객체로 만드는 방법도 있다.
* <p>
* 웹서버는 여러 사용자가 동시에 접속을 시도하기 때문에 동시성 이슈가 생길 수 있다. 어떤 사례가 있는지 아래 테스트 코드를 통해 알아보자.
*/
class ConcurrencyTest {

Expand All @@ -35,6 +31,7 @@ void test() throws InterruptedException {

// 이미 gugu로 가입한 사용자가 있어서 UserServlet.join() 메서드의 if절 조건은 false가 되고 크기는 1이다.
// 하지만 디버거로 개별 스레드를 일시 중지하면 if절 조건이 true가 되고 크기가 2가 된다. 왜 그럴까?
// A. .add에서 일시중지했을 때, 다른 스레드에서 .join이 먼저 실행되어서
assertThat(userServlet.getUsers()).hasSize(1);
}
}
30 changes: 12 additions & 18 deletions study/src/test/java/thread/stage2/AppTest.java
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
package thread.stage2;

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

import java.net.http.HttpResponse;
import java.util.concurrent.atomic.AtomicInteger;

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

class AppTest {

private static final AtomicInteger count = new AtomicInteger(0);

private static void incrementIfOk(final HttpResponse<String> response) {
if (response.statusCode() == 200) {
count.incrementAndGet();
}
}

/**
* 1. App 클래스의 애플리케이션을 실행시켜 서버를 띄운다.
* 2. 아래 테스트를 실행시킨다.
* 3. AppTest가 아닌 App의 콘솔에서 SampleController가 생성한 http call count 로그를 확인한다.
* 4. application.yml에서 설정값을 변경해보면서 어떤 차이점이 있는지 분석해본다.
* - 로그가 찍힌 시간
* - 스레드명(nio-8080-exec-x)으로 생성된 스레드 갯수를 파악
* - http call count
* - 테스트 결과값
* 1. App 클래스의 애플리케이션을 실행시켜 서버를 띄운다. 2. 아래 테스트를 실행시킨다. 3. AppTest가 아닌 App의 콘솔에서 SampleController가 생성한 http call
* count 로그를 확인한다. 4. application.yml에서 설정값을 변경해보면서 어떤 차이점이 있는지 분석해본다. - 로그가 찍힌 시간 - 스레드명(nio-8080-exec-x)으로 생성된 스레드
* 갯수를 파악 - http call count - 테스트 결과값
*/
@Test
void test() throws Exception {
final var NUMBER_OF_THREAD = 10;
var threads = new Thread[NUMBER_OF_THREAD];
final var threads = new Thread[NUMBER_OF_THREAD];

for (int i = 0; i < NUMBER_OF_THREAD; i++) {
threads[i] = new Thread(() -> incrementIfOk(TestHttpUtils.send("/test")));
Expand All @@ -41,10 +41,4 @@ void test() throws Exception {

assertThat(count.intValue()).isEqualTo(2);
}

private static void incrementIfOk(final HttpResponse<String> response) {
if (response.statusCode() == 200) {
count.incrementAndGet();
}
}
}
7 changes: 5 additions & 2 deletions tomcat/src/main/java/nextstep/Application.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package nextstep;

import java.util.Set;
import nextstep.jwp.servlet.LoginRequestServlet;
import nextstep.jwp.servlet.RegisterRequestServlet;
import org.apache.catalina.startup.Tomcat;

public class Application {

public static void main(String[] args) {
public static void main(final String[] args) {
final var tomcat = new Tomcat();
tomcat.start();
tomcat.start(Set.of(new LoginRequestServlet(), new RegisterRequestServlet()));
Copy link
Member

Choose a reason for hiding this comment

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

Application에서 Servlet을 주입받는군요.

진짜 코드가 맛있네요. 패키지에 신경을 쓰셨다는게 눈에 보여요.

정말 단순한 궁금증인데, Servlet들을 start로 넘기신 이유가 있을까요?

Tomcat 생성자에 넣을 수도 있을 것 같아서요.
(별 의미 없을 수도 있을 것 같은데, 혹시 도이라면 이유가 있을까? 궁금해서 코멘트 남깁니다.)

Copy link
Author

Choose a reason for hiding this comment

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

생성자로 받게 된다면, 서블릿들을 Tomcat의 멤버변수로 가지게 될텐데 (서블릿의 상태를 관리하는 다른 기능이 없기 때문에) 지금으로서는 그럴 이유가 없다고 생각했어요! 그래서 start 메서드를 통해 Http11Processor까지 전달해주기만 했습니다.

}
}
5 changes: 5 additions & 0 deletions tomcat/src/main/java/nextstep/jwp/controller/Controller.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package nextstep.jwp.controller;

public interface Controller {
Copy link
Member

Choose a reason for hiding this comment

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

혹시 Controller를 별도의 인터페이스로 분리하신 이유가 있으신가요?

각각의 Controller에 Implement를 하고 별도로 사용하진 않는 것 같아서요.
(따로 추상 메서드가 있는 것도 아니고요.)

Copy link
Author

@yoondgu yoondgu Sep 13, 2023

Choose a reason for hiding this comment

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

지금은 catalina에서 도메인 컨트롤러의 static 메서드를 호출하는 방식을 사용하고 있는데,
이 컨트롤러들도 인스턴스화해서 주입하는 방식으로 바꿀 필요가 있다고 생각하면서 일단 인터페이스만 만들어놨는데요. 해당 부분까지 진행하면 너무 커질 것 같아서 내버려뒀습니다... ㅎㅎㅎ

여기에 쪼금 더 합리화하자면 Spring에서 @Service 어노테이션이 지금은 특별한 기능이 없어도 사용되는 것처럼, 기능이 없는 인터페이스도 추후 변경 시 같이 변경되어야 할 레이어를 지정하는 역할을 할 수 있다고 생각했어요.


}
Loading