Skip to content

Commit

Permalink
[톰캣 구현하기 - 3, 4단계] 준팍(박준현) 미션 제출합니다. (#407)
Browse files Browse the repository at this point in the history
* test: IOStreamTest 완료

* test: FileTest 완료

* test: FileTest path에서 getResource로 방법 변경

* docs : 요구사항 정리

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

* feat: css 지원 기능 구현

* refactor: 클래스 분리

* feat: QueryString 파싱 기능 구현

* refactor: FileExtension 클래스 분리

* refactor: HttpStatus 클래스 분리

* refactor: RequestHandler 클래스 분리

* refactor: RequestHandler Path 로직 수정

* feat: favicon 추가

* feat: HTTP 302 기능 구현

* refactor: 개행 문자 lineSeparator()

* refactor: 클래스 분리

* feat: Post method 기능 구현

* feat: 회원 가입 후 리다이렉트 완료

* refacotr: lineSeperator() "\r\n"로 수정

* test: cache 학습테스트 완료

* test: cache 학습테스트 완료

* refactor: 피드백 반영

* refactor: QueryStrings 일급 컬렉션 구현

* feat: HttpCookie 구현

* refactor: 네이밍 변경

* chore: 패키지 분리

* feat: RequestHeaders에 HttpCookie 추가

* refactor: 메서드 분리

* refactor: HandlerMapping 상수화

* feat: 쿠키 값을 스트링으로 반환하는 메서드 구현

* refactor: 메서드 이동

* refactor: 클래스별 역할과 책임 재분배

* refactor: Handler 추가

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

* feat: Session 및 SessionManager 객체 구현

* feat: Session 기능 구현 완료

* feat: User ID AutoIncrement 기능 구현

* fix: 로그인 실패시 401.html 리다이렉트

* refactor: SessionManager 싱글턴 적용

* fix: 401 리다이렉트 오류 수정

* refactor: SessionManager Enum Singleton 적용

* chore: 코드 커버리지 확인을 위한 build.yml 수정

* refactor: 네이밍 변경, 싱글턴 적용, collection 변경

* fix: GET 요청 시 Login 및 Register 기능 삭제

* refactor: 불필요 메서드 삭제

* refactor: statusCode 타입 변경

* refactor: 불필요 객체 삭제

* refactor: ResponseEntity 빌더패턴 적용

* feat: 동시성 확장하기

* test: 학습테스트 Thread 학습 테스트 완료

* feat: Connector Thread pool 적용

* chore: 코드 커버리지 체크 제거

* refactor: ResponseEntity 제거 및 Controller 인터페이스 service 메서드 void 적용, 싱글턴 제거

* feat: HttpResponseParser 객체 구현

* refactor: 코드 스멜 제거

* refactor: 피드백 반영

* refactor: 피드백 반영

* fix: 파일을 찾을 수 없을 시 NotFound 연결
  • Loading branch information
junpakPark authored Sep 11, 2023
1 parent 3453b3d commit 94761d0
Show file tree
Hide file tree
Showing 51 changed files with 667 additions and 624 deletions.
34 changes: 11 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,14 @@
# 톰캣 구현하기

## 1단계 - HTTP 서버 구현하기
- [x] GET /index.html 응답하기
- [x] CSS 지원하기
- [x] Query String 파싱
## 3단계 - 리팩터링
- [x] HttpRequest 클래스 구현하기
- [x] HttpResponse 클래스 구현하기
- [x] Controller 인터페이스 추가하기
- [x] AbstractController를 상속한 구현체 만들기

## 2단계 - 로그인 구현하기
- [x] HTTP Status Code 302
- [x] 로그인 성공 시, 응답 헤더에 http status code를 302로 반환하고 `/index.html`로 리다이렉트
- [x] 로그인 실패 시, `401.html`로 리다이렉트
- [x] POST 방식으로 회원가입
- [x] `http://localhost:8080/register` 으로 접속하면 회원가입 페이지(`register.html`)를 보여준다.
- [x] 회원가입 페이지를 보여줄 때는 GET을 사용한다.
- [x] 회원가입을 버튼을 누르면 HTTP method를 GET이 아닌 POST를 사용한다.
- [x] 회원가입을 완료하면 `index.html`로 리다이렉트한다.
- [x] 로그인 페이지도 버튼을 눌렀을 때 GET 방식에서 POST 방식으로 전송하도록 변경하자.
- [x] Cookie에 JSESSIONID 값 저장하기
- [x] 서버에서 HTTP 응답을 전달할 때 응답 헤더에 `Set-Cookie`를 추가하고 `JSESSIONID=656cef62-e3c4-40bc-a8df-94732920ed46` 형태로 값을 전달하면
클라이언트 요청 헤더의 `Cookie` 필드에 값이 추가된다.
- [x] Cookie 클래스를 추가하고 HTTP Request Header의 Cookie에 `JSESSIONID`가 없으면 HTTP Response Header에 `Set-Cookie`를 반환해주는 기능을
구현한다.
- [x] Session 구현하기
- [x] 쿠키에서 전달 받은 JSESSIONID의 값으로 로그인 여부를 체크할 수 있어야 한다.
- [x] 로그인에 성공하면 Session 객체의 값으로 User 객체를 저장해보자.
- [x] 로그인 상태에서 `/login` 페이지에 HTTP GET method로 접근하면 `index.html` 페이지로 리다이렉트 처리한다
## 4단계 - 동시성 확장하기
- [x] Executors로 Thread Pool 적용하기
- [x] Connector 클래스에서 Executors 클래스를 사용해서 ExecutorService 객체 생성하기
- [x] maxThreads라는 변수로 스레드 갯수 지정하기
- [x] 동시성 컬렉션 사용하기
- [x] SessionManager 클래스에서 Session 컬렉션에 동시성 컬렉션 적용하기
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
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) {
CacheControl cacheControl = CacheControl
.noCache()
.cachePrivate();

WebContentInterceptor webContentInterceptor = new WebContentInterceptor();
webContentInterceptor.addCacheMapping(cacheControl, "/");

registry.addInterceptor(webContentInterceptor);
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
package cache.com.example.etag;

import cache.com.example.version.CacheBustingWebConfig;
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> filterRegistrationBean = new FilterRegistrationBean<>(
new ShallowEtagHeaderFilter());
filterRegistrationBean.addUrlPatterns(
"/etag",
CacheBustingWebConfig.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 @@ -20,6 +22,13 @@ public CacheBustingWebConfig(ResourceVersion version) {
@Override
public void addResourceHandlers(final ResourceHandlerRegistry registry) {
registry.addResourceHandler(PREFIX_STATIC_RESOURCES + "/" + version.getVersion() + "/**")
.addResourceLocations("classpath:/static/");
.addResourceLocations("classpath:/static/")
.setUseLastModified(true)
.setCacheControl(
CacheControl.maxAge(Duration.ofDays(365))
.cachePublic()
)
;
}

}
10 changes: 7 additions & 3 deletions study/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ handlebars:

server:
tomcat:
accept-count: 1
max-connections: 1
accept-count: 8 # 연결된 요청이 max-connections에 도달했을 경우, 추가로 연결 대기할 수 있는 요청의 갯수
max-connections: 2 # 서버가 동시에 처리할 수 있는 클라이언트 연결의 최대 개수,
#max-connections이 thread의 최대 갯수보다 많으면 추가 연결이 처리 대기열에 배치된다.
threads:
max: 2
max: 2 # 요청을 처리하는 Thread의 최대 갯수, AppTest의 count.intValue()는 해당 값을 의미한다.
compression:
enabled: true
min-response-size: 10
2 changes: 1 addition & 1 deletion study/src/test/java/thread/stage0/SynchronizationTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ private static final class SynchronizedMethods {

private int sum = 0;

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

Expand Down
6 changes: 3 additions & 3 deletions study/src/test/java/thread/stage0/ThreadPoolsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,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 +46,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 Down
8 changes: 4 additions & 4 deletions study/src/test/java/thread/stage0/ThreadTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ void testExtendedThread() throws InterruptedException {
Thread thread = new ExtendedThread("hello thread");

// 생성한 thread 객체를 시작한다.
thread.start();
thread.start();

// thread의 작업이 완료될 때까지 기다린다.
thread.join();
thread.join();
}

/**
Expand All @@ -46,10 +46,10 @@ void testRunnableThread() throws InterruptedException {
Thread thread = new Thread(new RunnableThread("hello thread"));

// 생성한 thread 객체를 시작한다.
thread.start();
thread.start();

// thread의 작업이 완료될 때까지 기다린다.
thread.join();
thread.join();
}

private static final class ExtendedThread extends Thread {
Expand Down
7 changes: 6 additions & 1 deletion study/src/test/java/thread/stage2/AppTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,25 +24,30 @@ class AppTest {
@Test
void test() throws Exception {
final var NUMBER_OF_THREAD = 10;
var threads = new Thread[NUMBER_OF_THREAD];

// Thread 객체 배열 생성 및 10개의 Thread 객체 생성하여 배열에 할당한다.
var threads = new Thread[NUMBER_OF_THREAD];
for (int i = 0; i < NUMBER_OF_THREAD; i++) {
threads[i] = new Thread(() -> incrementIfOk(TestHttpUtils.send("/test")));
}

// Thread 객체의 start() 메서드를 호출하면, Thread를 생성하고, 해당 스레드에서 run()메서드를 실행한다.
for (final var thread : threads) {
thread.start();
Thread.sleep(50);
}

// join() 메서드를 호출한 스레드가 join() 메서드를 실행한 스레드의 종료를 기다린다.
for (final var thread : threads) {
thread.join();
}

// count.intValue()는 실제 생성된 스레드의 갯수이다.
assertThat(count.intValue()).isEqualTo(2);
}

private static void incrementIfOk(final HttpResponse<String> response) {
// 요청이 200일때 AtomicInteger의 수량 증가
if (response.statusCode() == 200) {
count.incrementAndGet();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package nextstep.jwp.controller;

import org.apache.coyote.http11.common.HttpStatus;
import org.apache.coyote.http11.controller.Controller;
import org.apache.coyote.http11.request.HttpRequest;
import org.apache.coyote.http11.response.HttpResponse;

public class DefaultController implements Controller {

@Override
public void service(HttpRequest httpRequest, HttpResponse httpResponse) {
httpResponse.setHttpStatus(HttpStatus.OK)
.setPath("/default");
}

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

import nextstep.jwp.model.User;
import nextstep.jwp.service.UserService;
import org.apache.coyote.http11.common.HttpCookie;
import org.apache.coyote.http11.common.HttpStatus;
import org.apache.coyote.http11.controller.AbstractController;
import org.apache.coyote.http11.request.HttpRequest;
import org.apache.coyote.http11.request.RequestBody;
import org.apache.coyote.http11.request.RequestLine;
import org.apache.coyote.http11.response.HttpResponse;
import org.apache.coyote.http11.session.HttpSession;

public class LoginController extends AbstractController<UserService> {

public LoginController(UserService userService) {
super(userService);
}

@Override
protected void doGet(HttpRequest httpRequest, HttpResponse httpResponse) {
RequestLine requestLine = httpRequest.getRequestLine();
HttpSession session = httpRequest.getSession(false);

if (session != null && session.getAttribute("user") != null) {
httpResponse.setHttpStatus(HttpStatus.FOUND)
.setPath("/index");
return;
}

httpResponse.setHttpStatus(HttpStatus.OK)
.setPath(requestLine.getPath());
}

@Override
protected void doPost(HttpRequest httpRequest, HttpResponse httpResponse) {
RequestBody requestBody = httpRequest.getRequestBody();

String account = requestBody.get("account");
String password = requestBody.get("password");

try {
User user = service.login(account, password);

HttpSession httpSession = httpRequest.getSession(true);
httpSession.setAttribute("user", user);

httpRequest.addSession(httpSession);

HttpCookie httpCookie = HttpCookie.create();
httpCookie.putJSessionId(httpSession.getId());

httpResponse.setHttpStatus(HttpStatus.FOUND)
.setPath("/index")
.setCookie(httpCookie);
} catch (IllegalArgumentException e) {
httpResponse.setHttpStatus(HttpStatus.FOUND)
.setPath("/401");
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package nextstep.jwp.controller;

import org.apache.coyote.http11.common.HttpStatus;
import org.apache.coyote.http11.controller.Controller;
import org.apache.coyote.http11.request.HttpRequest;
import org.apache.coyote.http11.response.HttpResponse;

public class NotFoundController implements Controller {

@Override
public void service(HttpRequest httpRequest, HttpResponse httpResponse) {
httpResponse.setHttpStatus(HttpStatus.NOT_FOUND)
.setPath("/404");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package nextstep.jwp.controller;

import nextstep.jwp.service.UserService;
import org.apache.coyote.http11.common.HttpStatus;
import org.apache.coyote.http11.controller.AbstractController;
import org.apache.coyote.http11.request.HttpRequest;
import org.apache.coyote.http11.request.RequestBody;
import org.apache.coyote.http11.request.RequestLine;
import org.apache.coyote.http11.response.HttpResponse;

public class RegisterController extends AbstractController<UserService> {

public RegisterController(UserService userService) {
super(userService);
}

@Override
protected void doGet(HttpRequest httpRequest, HttpResponse httpResponse) {
RequestLine requestLine = httpRequest.getRequestLine();

httpResponse.setHttpStatus(HttpStatus.OK)
.setPath(requestLine.getPath());
}

@Override
protected void doPost(HttpRequest httpRequest, HttpResponse httpResponse) {
RequestBody requestBody = httpRequest.getRequestBody();

String account = requestBody.get("account");
String password = requestBody.get("password");
String email = requestBody.get("email");

service.save(account, password, email);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package nextstep.jwp.controller;

import org.apache.coyote.http11.common.HttpStatus;
import org.apache.coyote.http11.controller.Controller;
import org.apache.coyote.http11.request.HttpRequest;
import org.apache.coyote.http11.response.HttpResponse;

public class UnauthorizedController implements Controller {

@Override
public void service(HttpRequest httpRequest, HttpResponse httpResponse) {
httpResponse.setHttpStatus(HttpStatus.UNAUTHORIZED)
.setPath("/401");
}

}
26 changes: 0 additions & 26 deletions tomcat/src/main/java/nextstep/jwp/controller/UserController.java

This file was deleted.

Loading

0 comments on commit 94761d0

Please sign in to comment.