Skip to content

Commit

Permalink
[톰캣 구현하기 - 2, 3, 4단계] 밀리(김미성) 미션 제출합니다. (#464)
Browse files Browse the repository at this point in the history
* feat: cookie, session 구현

* refactor: Http Request 파싱 로직 리팩토링

* refactor: Controller 인터페이스 리팩토링

* refactor: HttpResponseGenerator로 응답 메세지 생성하도록 수정

* refactor: FrontController 도입하여 양방향 의존성 제거

* test: cache 학습 테스트 작성

* test: thread 학습 테스트 작성

* feat: Executors로 Thread Pool 적용

* feat: 동시성 컬렉션으로 변경

* refactor: RequestBody 파싱 타입 변경

* docs: 기능 요구사항 작성

* refactor: content-length 헤더 세팅 컨트롤러에서 하도록 수정

* refactor: 코드 가독성 개선

* refactor: static resource 요청을 처리하는 StaticController 구현

* refactor: 세션 책임 분리

* refactor: FrontController를 RequestMapping으로 변경
  • Loading branch information
miseongk authored Sep 13, 2023
1 parent 21135a4 commit 20faf64
Show file tree
Hide file tree
Showing 36 changed files with 557 additions and 269 deletions.
19 changes: 15 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,18 @@
- [x] 회원가입 페이지는 GET으로 요청한다.
- [x] 회원가입을 완료하면 index.html로 리다이렉트한다.
- [x] 로그인 페이지도 버튼을 눌렀을 때 POST 방식으로 전송하도록 변경한다.
- [ ] 서버에서 HTTP 응답을 전달할 때 응답 헤더에 Set-Cookie를 추가한다.
- [ ] Cookie에 JSESSIONID가 없으면 응답 헤더에 Set-Cookie를 반환해준다.
- [ ] 쿠키에서 전달 받은 JSESSIONID의 값으로 로그인 여부를 체크한다.
- [ ] 로그인된 상태에서 /login 페이지에 접근하면 index.html 페이지로 리다이렉트 처리한다.
- [x] 서버에서 HTTP 응답을 전달할 때 응답 헤더에 Set-Cookie를 추가한다.
- [x] Cookie에 JSESSIONID가 없으면 응답 헤더에 Set-Cookie를 반환해준다.
- [x] 쿠키에서 전달 받은 JSESSIONID의 값으로 로그인 여부를 체크한다.
- [x] 로그인된 상태에서 /login 페이지에 접근하면 index.html 페이지로 리다이렉트 처리한다.

### 3단계

- [x] HttpRequest 클래스 구현하기
- [x] HttpResponse 클래스 구현하기
- [x] Controller 인터페이스 추가하기

### 4단계

- [x] Executors로 Thread Pool 적용
- [x] 동시성 컬렉션 사용하기
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,23 @@
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() {
final FilterRegistrationBean<ShallowEtagHeaderFilter> filterRegistrationBean = new FilterRegistrationBean<>(
new ShallowEtagHeaderFilter());
filterRegistrationBean.addUrlPatterns(
"/etag",
PREFIX_STATIC_RESOURCES + "/*"
);
return filterRegistrationBean;
}
}
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 @@ -13,13 +15,14 @@ public class CacheBustingWebConfig implements WebMvcConfigurer {
private final ResourceVersion version;

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

@Override
public void addResourceHandlers(final ResourceHandlerRegistry registry) {
registry.addResourceHandler(PREFIX_STATIC_RESOURCES + "/" + version.getVersion() + "/**")
.addResourceLocations("classpath:/static/");
.addResourceLocations("classpath:/static/")
.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 @@ -7,3 +7,6 @@ server:
max-connections: 1
threads:
max: 2
compression:
enabled: true
min-response-size: 10
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
6 changes: 0 additions & 6 deletions tomcat/src/main/java/handler/RequestHandler.java

This file was deleted.

23 changes: 0 additions & 23 deletions tomcat/src/main/java/handler/RequestHandlerMapping.java

This file was deleted.

8 changes: 6 additions & 2 deletions tomcat/src/main/java/nextstep/Application.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package nextstep;

import nextstep.jwp.JwpRequestMapping;
import org.apache.catalina.startup.Tomcat;
import org.apache.coyote.handler.RequestMapping;

public class Application {

public static void main(String[] args) {
final var tomcat = new Tomcat();
private static final RequestMapping requestMapping = new JwpRequestMapping();

public static void main(final String[] args) {
final var tomcat = new Tomcat(requestMapping);
tomcat.start();
}
}
37 changes: 37 additions & 0 deletions tomcat/src/main/java/nextstep/jwp/JwpRequestMapping.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package nextstep.jwp;

import java.util.HashMap;
import java.util.Map;
import nextstep.jwp.controller.IndexController;
import nextstep.jwp.controller.LoginController;
import nextstep.jwp.controller.RegisterController;
import nextstep.jwp.controller.StaticController;
import org.apache.coyote.handler.Controller;
import org.apache.coyote.handler.RequestMapping;

public class JwpRequestMapping implements RequestMapping {

private static final String STATIC_RESOURCE_KEY = "static";
private static final String EXTENSION_DELIMITER = ".";

private final Map<String, Controller> handlerMapping = new HashMap<>();

public JwpRequestMapping() {
handlerMapping.put(STATIC_RESOURCE_KEY, new StaticController());
handlerMapping.put("/", new IndexController());
handlerMapping.put("/login", new LoginController());
handlerMapping.put("/register", new RegisterController());
}

@Override
public Controller getHandler(final String requestUri) {
if (isFileRequest(requestUri)) {
return handlerMapping.get(STATIC_RESOURCE_KEY);
}
return handlerMapping.get(requestUri);
}

private boolean isFileRequest(final String requestUri) {
return requestUri.contains(EXTENSION_DELIMITER);
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
package nextstep.jwp.controller;

import handler.Controller;
import org.apache.coyote.handler.AbstractController;
import org.apache.coyote.http11.HttpRequest;
import org.apache.coyote.http11.HttpResponse;
import org.apache.coyote.http11.HttpStatusCode;
import org.apache.coyote.http11.ViewResolver;

public class IndexController implements Controller {
public class IndexController extends AbstractController {

@Override
public String run(final HttpRequest httpRequest, final HttpResponse httpResponse) {
httpResponse.setStatusCode(HttpStatusCode.OK);
return "/index.html";
public void doGet(final HttpRequest request, final HttpResponse response) {
final String responseBody = ViewResolver.read("/index.html");
response.setResponseBody(responseBody);
response.setContentLength();
response.setStatusCode(HttpStatusCode.OK);
}
}
Loading

0 comments on commit 20faf64

Please sign in to comment.