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단계] 망고(고재철) 미션 제출합니다. #460

Merged
merged 25 commits into from
Sep 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
ff07df4
docs: 3, 4단계 요구사항 정리
Go-Jaecheol Sep 7, 2023
0e15705
refactor: RequestLine 객체 생성
Go-Jaecheol Sep 8, 2023
b042c08
refactor: RequestHeader 객체 생성
Go-Jaecheol Sep 8, 2023
d20cedc
refactor: HttpRequest 객체 분리
Go-Jaecheol Sep 8, 2023
6f254a0
refactor: StatusLine 객체 생성
Go-Jaecheol Sep 8, 2023
ab51a09
refactor: ResponseHeader 객체 생성
Go-Jaecheol Sep 8, 2023
e29572f
refactor: HttpResponse 객체 분리
Go-Jaecheol Sep 8, 2023
9c8a923
feat: ResponseBody 객체 추가
Go-Jaecheol Sep 10, 2023
930fb10
test: HttpResponse toString 테스트 추가
Go-Jaecheol Sep 10, 2023
8214f8a
feat: HttpResponse 빈 객체 생성자 추가
Go-Jaecheol Sep 10, 2023
cd9bd02
feat: Controller 인터페이스 추가
Go-Jaecheol Sep 10, 2023
1dca664
feat: HomeController 구현
Go-Jaecheol Sep 10, 2023
2cc4449
feat: ResourceController 구현
Go-Jaecheol Sep 10, 2023
38f1da8
feat: RegisterController 구현
Go-Jaecheol Sep 10, 2023
78cf2d5
feat: LoginController 구현
Go-Jaecheol Sep 10, 2023
6ed87da
refactor: RequestMapping 객체 추가
Go-Jaecheol Sep 10, 2023
51529d5
test: 캐시 학습 테스트 완료
Go-Jaecheol Sep 10, 2023
4b8aefc
test: Thread 학습 테스트 완료
Go-Jaecheol Sep 10, 2023
7081ed7
feat: Thread Pool 적용
Go-Jaecheol Sep 10, 2023
baf5476
refactor: SessionManager 동시성 컬렉션 적용
Go-Jaecheol Sep 10, 2023
c6e0955
refactor: 405 Method Not Allowed 반영
Go-Jaecheol Sep 14, 2023
8cbc245
refactor: HttpMethod enum 생성
Go-Jaecheol Sep 14, 2023
a386e73
refactor: Request Parameter 구하는 책임 변경
Go-Jaecheol Sep 14, 2023
aafb47e
refactor: HttpRequest에서 Cookie 값 반환하도록 책임 변경
Go-Jaecheol Sep 14, 2023
37a4aaa
refactor: httpRequest, httpResponse 변수명 통일
Go-Jaecheol Sep 14, 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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,12 @@
- [x] POST 방식으로 회원가입
- [x] Cookie에 JSESSIONID 값 저장하기
- [x] Session 구현하기

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

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;

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

@Configuration
public class EtagFilterConfiguration {

// @Bean
// public FilterRegistrationBean<ShallowEtagHeaderFilter> shallowEtagHeaderFilter() {
// return null;
// }
@Bean
public FilterRegistrationBean<ShallowEtagHeaderFilter> shallowEtagHeaderFilter() {
final var filterRegistrationBean = new FilterRegistrationBean<>(new ShallowEtagHeaderFilter());
filterRegistrationBean.addUrlPatterns("/etag", PREFIX_STATIC_RESOURCES + "/*");
return filterRegistrationBean;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

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;

import java.time.Duration;

@Configuration
public class CacheBustingWebConfig implements WebMvcConfigurer {

Expand All @@ -20,6 +23,7 @@ public CacheBustingWebConfig(ResourceVersion 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
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package nextstep.jwp.controller;

import nextstep.jwp.exception.HttpRequestException;
import org.apache.coyote.http11.request.HttpRequest;
import org.apache.coyote.http11.response.HttpResponse;

import java.io.IOException;

public abstract class AbstractController implements Controller {

protected static final String TEXT_HTML = "text/html;charset=utf-8";
protected static final String TEXT_CSS = "text/css;";
protected static final String INDEX_PAGE = "/index.html";
protected static final String UNAUTHORIZED_PAGE = "/401.html";
protected static final String HEADER_LOCATION = "Location";
protected static final String HEADER_SET_COOKIE = "Set-Cookie";
protected static final String HEADER_CONTENT_TYPE = "Content-Type";
protected static final String HEADER_CONTENT_LENGTH = "Content-Length";
protected static final String HTTP_METHOD_EXCEPTION_MESSAGE = "올바르지 않은 HTTP Method 입니다.";

@Override
public void service(final HttpRequest httpRequest, final HttpResponse httpResponse) throws IOException {
if (httpRequest.getRequestLine().getMethod().equals("POST")) {
doPost(httpRequest, httpResponse);
return;
}
if (httpRequest.getRequestLine().getMethod().equals("GET")) {
doGet(httpRequest, httpResponse);
return;
}
throw new HttpRequestException(HTTP_METHOD_EXCEPTION_MESSAGE);
}

protected abstract void doPost(final HttpRequest httpRequest, final HttpResponse httpResponse) throws IOException;

protected abstract void doGet(final HttpRequest httpRequest, final HttpResponse httpResponse) throws IOException;
}
11 changes: 11 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,11 @@
package nextstep.jwp.controller;
Copy link

@dooboocookie dooboocookie Sep 12, 2023

Choose a reason for hiding this comment

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

위 의존성 순환에 대한 언급을 했던 것에 연장선일 수 있는데요.

이 Controller는 인터페이스인데요.
이 인터페이스는 Tomcat을 개발하는 우리가 이 톰캣을 사용해서 웹 개발을 할 유저에게 어떤 요청을 핸들링할 때는 이 컨트롤러와 Abstract컨트롤러를 구현해서 사용하라는 일종의 설명서 같은 것일텐데요.

그렇다는 것은 jwp(유저의 영역)와 koyote(톰캣 영역) 둘중 어디에 있는 것이 적절할까요?

Copy link
Author

Choose a reason for hiding this comment

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

의존성... 어렵네요...

제가 이해한 바로는 그렇다면 Controller 인터페이스는 coyote에 두고, Controller 구현체는 jwp에 두고, RequestMapping도 jwp에 두도록 수정하는 게 맞을까요...?

의존성.. 패키지 구조.. 국어 지문처럼 사람마다 해석하는 게 다를 수 있다고 생각해서 너무 어려워요 😢😢😢


import org.apache.coyote.http11.request.HttpRequest;
import org.apache.coyote.http11.response.HttpResponse;

import java.io.IOException;

public interface Controller {

void service(final HttpRequest request, final HttpResponse httpResponse) throws IOException;

Choose a reason for hiding this comment

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

파라미터 네이밍을 httpRequest나 response로 통일 시키면 좋을 것 가타요!

Copy link
Author

Choose a reason for hiding this comment

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

요구사항 템플릿 그대로 했더니..!
수정했습니당~

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

import org.apache.coyote.http11.request.HttpRequest;
import org.apache.coyote.http11.response.HttpResponse;
import org.apache.coyote.http11.response.HttpStatus;
import org.apache.coyote.http11.response.ResponseBody;
import org.apache.coyote.http11.response.StatusLine;

public class HomeController extends AbstractController {

@Override
protected void doPost(final HttpRequest httpRequest, final HttpResponse httpResponse) {
final var statusLine = StatusLine.of(httpRequest.getRequestLine().getProtocol(), HttpStatus.METHOD_NOT_ALLOWED);
httpResponse.setStatusLine(statusLine);
}

@Override
protected void doGet(final HttpRequest httpRequest, final HttpResponse httpResponse) {
final var statusLine = StatusLine.of(httpRequest.getRequestLine().getProtocol(), HttpStatus.OK);
final var responseBody = ResponseBody.fromText("Hello world!");
httpResponse.setStatusLine(statusLine);
httpResponse.addResponseHeader("Content-Type", TEXT_HTML);
httpResponse.addResponseHeader("Content-Length", String.valueOf(responseBody.getBody().getBytes().length));
httpResponse.setResponseBody(responseBody);
}
}
78 changes: 78 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,78 @@
package nextstep.jwp.controller;

import nextstep.jwp.db.InMemoryUserRepository;
import nextstep.jwp.model.User;
import org.apache.coyote.http11.HttpCookie;
import org.apache.coyote.http11.Session;
import org.apache.coyote.http11.SessionManager;
import org.apache.coyote.http11.request.HttpRequest;
import org.apache.coyote.http11.response.HttpResponse;
import org.apache.coyote.http11.response.HttpStatus;
import org.apache.coyote.http11.response.ResponseBody;
import org.apache.coyote.http11.response.StatusLine;

import java.io.IOException;
import java.util.Map;
import java.util.Optional;

public class LoginController extends AbstractController {

private static final SessionManager SESSION_MANAGER = new SessionManager();

@Override
protected void doPost(final HttpRequest httpRequest, final HttpResponse httpResponse) {
final HttpCookie cookie = httpRequest.getCookie();
final User user = findUserBySessionId(cookie.getJSessionId());
final var statusLine = StatusLine.of(httpRequest.getRequestLine().getProtocol(), HttpStatus.FOUND);
httpResponse.setStatusLine(statusLine);
if (user == null) {
handleFirstLogin(httpRequest, httpResponse);
return;
}
httpResponse.addResponseHeader(HEADER_LOCATION, INDEX_PAGE);
}

private void handleFirstLogin(final HttpRequest httpRequest, final HttpResponse httpResponse) {
final Map<String, String> requestBodyValues = httpRequest.getRequestParameters();
final Optional<User> user = InMemoryUserRepository.findByAccount(requestBodyValues.get("account"));
if (user.isEmpty() || !user.get().checkPassword(requestBodyValues.get("password"))) {
httpResponse.addResponseHeader(HEADER_LOCATION, UNAUTHORIZED_PAGE);
return;
}
final String sessionId = addSession(user.get());
httpResponse.addResponseHeader(HEADER_LOCATION, INDEX_PAGE);
httpResponse.addResponseHeader(HEADER_SET_COOKIE, "JSESSIONID=" + sessionId);
}

@Override
protected void doGet(final HttpRequest httpRequest, final HttpResponse httpResponse) throws IOException {
final HttpCookie cookie = httpRequest.getCookie();
final User user = findUserBySessionId(cookie.getJSessionId());
if (user != null) {
doPost(httpRequest, httpResponse);
return;
}
final var statusLine = StatusLine.of(httpRequest.getRequestLine().getProtocol(), HttpStatus.OK);
final var responseBody = ResponseBody.fromUri("/login.html");
httpResponse.setStatusLine(statusLine);
httpResponse.addResponseHeader(HEADER_CONTENT_TYPE, TEXT_HTML);
httpResponse.addResponseHeader(HEADER_CONTENT_LENGTH, String.valueOf(responseBody.getBody().getBytes().length));
httpResponse.setResponseBody(responseBody);
}

private User findUserBySessionId(final String sessionId) {
if (sessionId == null) {
return null;
}
final Session session = SESSION_MANAGER.findSession(sessionId)
.orElseGet(Session::create);
return (User) session.getAttribute("user");
Comment on lines +67 to +69

Choose a reason for hiding this comment

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

Session은 위에서 언급한 Cookie와 다른 역할을 하고 있지만, 무상태성인 HTTP 통신을 하다가 서버가 특정 브라우저(클라이언트) 임을 알기 위해서 저장하고 그에 대한 아이디를 쿠키에 넘겨주는 형식으로 사용이 되는데요.

Cookie와 마찬가지로 어떤 요청이 오면 그 클라이언트가 Session이 이미 저장이 되어있는 유저라면 Cookie 헤더의 SessionId를 통해서 알 수 있을거에요.

그렇다면 이 책임 또한 Request같은 객체로 옮길 수 있지 않을까요?

만약 그렇다면 어떤 방식으로 Response에게 Set-Cookie 헤더를 명시하라고 지시할 수 있을까요?

Choose a reason for hiding this comment

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

사실 유저 입장에서는 Session Manager가 무엇인지 인지하거나 그것을 통해서 세션을 저장하는 행위가 매우 어려울 수 있다고 생각합니다.

우리 톰캣을 사용하는 유저는

  • 요청과 매핑된 컨트롤러를 구현하고
  • 그 컨트롤러의 로직을 구현하고
  • 그 때 파라미터로 넘어온 request와 response를 조작해서 요청을 분석하고 응답을 설정한다

위에서 나온 객체들로 거의 대부분의 일을 해내야될 수 있다고 생각합니다.

Copy link
Author

@Go-Jaecheol Go-Jaecheol Sep 14, 2023

Choose a reason for hiding this comment

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

Session이 이미 저장이 되어있는 유저라면 Cookie 헤더의 SessionId를 통해서 알 수 있을거에요.

이 부분은 위 피드백을 반영하면서 Cookie 값을 가져오는 책임을 HttpRequest 객체로 옮겨주는 방식으로 수정했습니다.

상위 객체가 너무 많은 책임을 가지기보다는 각각 객체마다 가장 어울리는 책임을 담당하도록 하는게 좋다고 생각해서, JSESSIONID를 받아오는 책임은 HttpCookie에게 그대로 두도록 했습니다.
이 부분도 HttpRequest 객체에서 바로 받아오도록 수정하는 게 더 어울릴까요??

Choose a reason for hiding this comment

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

제 생각은 Session과 Cookie의 값들은 어떤 요청에 의한 값이므로 그에 대한 접근 정보 권한도 Request에 있어야된다고 생각했습니다!!

그래서 유저가 Request의 어떤 값으로 Session 저장소에서 Session이 어떤 컬렉션에 담겨있을테니 어떻게 가져와야지 이런식으로 복잡한 과정을 생각하는 것 보다는

Session Cookie RequestParameter같은 값은 손쉽게 Request에서 값을 뽑아낼 수 있어야된다고 생각했고 Request가 저 위에 값들을 필드로 가지고 있어야한다고 생각했습니다!

}

private String addSession(final User user) {
final var session = Session.create();
session.setAttribute("user", user);
SESSION_MANAGER.add(session);
return session.getId();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package nextstep.jwp.controller;

import nextstep.jwp.db.InMemoryUserRepository;
import nextstep.jwp.model.User;
import org.apache.coyote.http11.request.HttpRequest;
import org.apache.coyote.http11.response.HttpResponse;
import org.apache.coyote.http11.response.HttpStatus;
import org.apache.coyote.http11.response.ResponseBody;
import org.apache.coyote.http11.response.StatusLine;

import java.io.IOException;
import java.util.Map;

public class RegisterController extends AbstractController {

@Override
protected void doPost(final HttpRequest httpRequest, final HttpResponse httpResponse) {
final Map<String, String> requestBodyValues = httpRequest.getRequestParameters();
final var user = new User(requestBodyValues.get("account"), requestBodyValues.get("password"),
requestBodyValues.get("email"));
InMemoryUserRepository.save(user);

final var statusLine = StatusLine.of(httpRequest.getRequestLine().getProtocol(), HttpStatus.FOUND);
httpResponse.setStatusLine(statusLine);
httpResponse.addResponseHeader("Location", INDEX_PAGE);
}

@Override
protected void doGet(final HttpRequest httpRequest, final HttpResponse httpResponse) throws IOException {
final var statusLine = StatusLine.of(httpRequest.getRequestLine().getProtocol(), HttpStatus.OK);
final var responseBody = ResponseBody.fromUri("/register.html");
httpResponse.setStatusLine(statusLine);
httpResponse.addResponseHeader("Content-Type", TEXT_HTML);
httpResponse.addResponseHeader("Content-Length", String.valueOf(responseBody.getBody().getBytes().length));
httpResponse.setResponseBody(responseBody);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package nextstep.jwp.controller;

import org.apache.coyote.http11.request.HttpRequest;
import org.apache.coyote.http11.response.HttpResponse;
import org.apache.coyote.http11.response.HttpStatus;
import org.apache.coyote.http11.response.ResponseBody;
import org.apache.coyote.http11.response.StatusLine;

import java.io.IOException;

public class ResourceController extends AbstractController {

@Override
protected void doPost(final HttpRequest httpRequest, final HttpResponse httpResponse) {
final var statusLine = StatusLine.of(httpRequest.getRequestLine().getProtocol(), HttpStatus.METHOD_NOT_ALLOWED);
httpResponse.setStatusLine(statusLine);
}

@Override
protected void doGet(final HttpRequest httpRequest, final HttpResponse httpResponse) throws IOException {
final var statusLine = StatusLine.of(httpRequest.getRequestLine().getProtocol(), HttpStatus.OK);
final String uri = httpRequest.getRequestLine().getPath().split("\\?")[0];
final var responseBody = ResponseBody.fromUri(uri);
httpResponse.setStatusLine(statusLine);
httpResponse.addResponseHeader("Content-Type", getContentType(uri));
httpResponse.addResponseHeader("Content-Length", String.valueOf(responseBody.getBody().getBytes().length));
httpResponse.setResponseBody(responseBody);
}

private String getContentType(final String uri) {
if (uri.endsWith(".css")) {
return TEXT_CSS;
}
return TEXT_HTML;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package nextstep.jwp.exception;

public class HttpRequestException extends RuntimeException {

public HttpRequestException(final String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,29 @@
import java.io.UncheckedIOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Connector implements Runnable {

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

private static final int DEFAULT_PORT = 8080;
private static final int DEFAULT_ACCEPT_COUNT = 100;
private static final int MAX_THREAD_POOL_SIZE = 250;

private final ExecutorService executorService;
private final ServerSocket serverSocket;
private boolean stopped;

public Connector() {
this(DEFAULT_PORT, DEFAULT_ACCEPT_COUNT);
this(DEFAULT_PORT, DEFAULT_ACCEPT_COUNT, MAX_THREAD_POOL_SIZE);
}

public Connector(final int port, final int acceptCount) {
public Connector(final int port, final int acceptCount, final int maxThreads) {
this.serverSocket = createServerSocket(port, acceptCount);
this.stopped = false;
this.executorService = Executors.newFixedThreadPool(maxThreads);
}

private ServerSocket createServerSocket(final int port, final int acceptCount) {
Expand Down Expand Up @@ -67,7 +72,7 @@ private void process(final Socket connection) {
return;
}
var processor = new Http11Processor(connection);
new Thread(processor).start();
executorService.execute(processor);
}

public void stop() {
Expand Down
Loading