Skip to content

[톰캣 구현하기 - 3, 4단계] 준팍(박준현) 미션 제출합니다. #407

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

Merged
merged 60 commits into from
Sep 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
757aa2a
test: IOStreamTest 완료
junpakPark Sep 1, 2023
15b7437
test: FileTest 완료
junpakPark Sep 2, 2023
4cb9a8c
test: FileTest path에서 getResource로 방법 변경
junpakPark Sep 2, 2023
08af0cb
docs : 요구사항 정리
junpakPark Sep 2, 2023
aa667c6
feat: GET /index.html 응답 기능 구현
junpakPark Sep 2, 2023
a721f0b
feat: css 지원 기능 구현
junpakPark Sep 2, 2023
791fd98
refactor: 클래스 분리
junpakPark Sep 3, 2023
41582a5
feat: QueryString 파싱 기능 구현
junpakPark Sep 3, 2023
782c35b
refactor: FileExtension 클래스 분리
junpakPark Sep 3, 2023
4a06f65
refactor: HttpStatus 클래스 분리
junpakPark Sep 3, 2023
af1bc42
refactor: RequestHandler 클래스 분리
junpakPark Sep 3, 2023
03a14a7
refactor: RequestHandler Path 로직 수정
junpakPark Sep 3, 2023
8be2ead
feat: favicon 추가
junpakPark Sep 3, 2023
030dd63
feat: HTTP 302 기능 구현
junpakPark Sep 4, 2023
aa6b77c
refactor: 개행 문자 lineSeparator()
junpakPark Sep 4, 2023
b7a3fda
refactor: 클래스 분리
junpakPark Sep 4, 2023
a8228d2
feat: Post method 기능 구현
junpakPark Sep 4, 2023
08ab904
feat: 회원 가입 후 리다이렉트 완료
junpakPark Sep 4, 2023
dd29b24
refacotr: lineSeperator() "\r\n"로 수정
junpakPark Sep 4, 2023
fda4afa
test: cache 학습테스트 완료
junpakPark Sep 5, 2023
b4c483d
test: cache 학습테스트 완료
junpakPark Sep 5, 2023
727135f
refactor: 피드백 반영
junpakPark Sep 5, 2023
04f7da6
refactor: QueryStrings 일급 컬렉션 구현
junpakPark Sep 5, 2023
7fe6019
feat: HttpCookie 구현
junpakPark Sep 5, 2023
08b0e2f
refactor: 네이밍 변경
junpakPark Sep 5, 2023
7b7d82a
chore: 패키지 분리
junpakPark Sep 5, 2023
a21062c
feat: RequestHeaders에 HttpCookie 추가
junpakPark Sep 5, 2023
ce3b483
refactor: 메서드 분리
junpakPark Sep 5, 2023
9cf7608
refactor: HandlerMapping 상수화
junpakPark Sep 5, 2023
7d8ddae
feat: 쿠키 값을 스트링으로 반환하는 메서드 구현
junpakPark Sep 5, 2023
09d4a9c
refactor: 메서드 이동
junpakPark Sep 6, 2023
b78dbdb
refactor: 클래스별 역할과 책임 재분배
junpakPark Sep 6, 2023
1bd761c
refactor: Handler 추가
junpakPark Sep 6, 2023
8dea78c
feat: Cookie에 JSESSIONID 값 저장 기능 구현
junpakPark Sep 7, 2023
61a7a60
feat: Session 및 SessionManager 객체 구현
junpakPark Sep 7, 2023
4a830e2
feat: Session 기능 구현 완료
junpakPark Sep 7, 2023
5479d54
feat: User ID AutoIncrement 기능 구현
junpakPark Sep 7, 2023
e1e9eb9
test: cache 학습테스트
junpakPark Sep 7, 2023
2407c89
fix: 로그인 실패시 401.html 리다이렉트
junpakPark Sep 7, 2023
56ee2a9
refactor: SessionManager 싱글턴 적용
junpakPark Sep 7, 2023
10c25e9
test: cache 학습 테스트
junpakPark Sep 7, 2023
fb494d3
fix: 401 리다이렉트 오류 수정
junpakPark Sep 7, 2023
a29c816
refactor: SessionManager Enum Singleton 적용
junpakPark Sep 7, 2023
42f9516
chore: 코드 커버리지 확인을 위한 build.yml 수정
junpakPark Sep 7, 2023
189b083
refactor: 네이밍 변경, 싱글턴 적용, collection 변경
junpakPark Sep 8, 2023
fec6b10
fix: GET 요청 시 Login 및 Register 기능 삭제
junpakPark Sep 8, 2023
ecf241a
refactor: 불필요 메서드 삭제
junpakPark Sep 8, 2023
c09714c
refactor: statusCode 타입 변경
junpakPark Sep 8, 2023
d8463a2
refactor: 불필요 객체 삭제
junpakPark Sep 8, 2023
c0ba83b
refactor: ResponseEntity 빌더패턴 적용
junpakPark Sep 8, 2023
07d5d93
feat: 동시성 확장하기
junpakPark Sep 8, 2023
2691b61
test: 학습테스트 Thread 학습 테스트 완료
junpakPark Sep 9, 2023
a1a5dc5
feat: Connector Thread pool 적용
junpakPark Sep 9, 2023
4b1cfd0
chore: 코드 커버리지 체크 제거
junpakPark Sep 9, 2023
265a9e7
refactor: ResponseEntity 제거 및 Controller 인터페이스 service 메서드 void 적용, 싱…
junpakPark Sep 10, 2023
e7ac707
feat: HttpResponseParser 객체 구현
junpakPark Sep 10, 2023
fe30e41
refactor: 코드 스멜 제거
junpakPark Sep 10, 2023
45cb835
refactor: 피드백 반영
junpakPark Sep 11, 2023
8f06001
refactor: 피드백 반영
junpakPark Sep 11, 2023
ba6a311
fix: 파일을 찾을 수 없을 시 NotFound 연결
junpakPark Sep 11, 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
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 {

Choose a reason for hiding this comment

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

Controller를 구현한 클래스와, AbstractController를 구현한 클래스를 어떤 기준으로 나누셨는지 궁금해요 !

정적 파일을 서빙하는 컨트롤러는 Controller를 구현한게 맞나요 ? (뷰 리졸버 느끼이이임?)
(ViewResolver가 삭제된 이력이 있어서 유추해봤습니다 ㅋ.ㅋ.ㅋ)

Copy link
Member Author

@junpakPark junpakPark Sep 11, 2023

Choose a reason for hiding this comment

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

  1. 일단 제가 Controller와 AbstractController를 나눈 기준은 비즈니스 로직이 필요한지 여부 입니당.
    단순히 파일만 서빙하는 경우는 doGet()이나 doPost()를 사용할 필요가 없기 때문에 Controller를 구현했고,
    비즈니스 로직이 필요한 경우 (get과 post의 분기가 필요한 경우)에는 AbstractController로 구현했습니다.

  2. 정적 파일을 서빙하는 컨트롤러는 ResourceController라는 네이밍으로 coyote 패키지 내부에 위치하고 있습니다.
    뷰 리졸버는 Tomcat 미션에 적절하지 않은 네이밍이라고 생각하여 Renderer라는
    Renderer라는 네이밍으로 변경하고, 내부에 로직을 더 정리했습니다.
    ResourceController라는 네이밍은 좀 더 명시적으로 수정하겠습니당


@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) {

Choose a reason for hiding this comment

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

QueryString으로 로그인 요청하는 것에 대한 처리 로직이 누락 된 것 같아요.

의도하신 걸까요 ?

Copy link
Member Author

@junpakPark junpakPark Sep 11, 2023

Choose a reason for hiding this comment

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

#379 (comment)

저번 리뷰에서 지우겠다고 말씀드렸었는데... 초큼 섭섭할지도..?ㅠ

  1. 로그인이나 회원 가입 시 정보(특히 비밀번호)가 URL에 그대로 노출된다는 점
  2. HTML이 이미 POST를 지원한다는 점

위 두 가지 이유로 인해 해당 로직 삭제했습니다!

Choose a reason for hiding this comment

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

Sorry bro...

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