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단계] 에단(김석호) 미션 제출합니다. #422

Merged
merged 43 commits into from
Sep 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
5f53414
feat: CacheControl 인터셉터 추가
cookienc Sep 5, 2023
818588c
feat: 압축 설정 추가
cookienc Sep 5, 2023
b205cc0
feat: Etag 설정 추가
cookienc Sep 5, 2023
e42bd2a
feat: Version 및 캐싱 기능 구현
cookienc Sep 5, 2023
815ecb5
test: 쓰레드 stage0 학습 테스트 진행
cookienc Sep 7, 2023
35d7e57
test: 쓰레드 stage1 학습 테스트 진행
cookienc Sep 7, 2023
c714b06
test: 쓰레드 stage2 학습 테스트 진행
cookienc Sep 7, 2023
4fd6b34
refactor: 패키지 변경
cookienc Sep 7, 2023
7c08deb
refactor: HttpHeaders 의 key 를 enummap으로 변경
cookienc Sep 7, 2023
8618726
refactor: 정적파일의 확장자를 contenttype으로 관리
cookienc Sep 7, 2023
0b1b0e6
refactor: 상수의 접근 제한자를 private으로 변경
cookienc Sep 7, 2023
2d9e91c
refactor: HomePageMapping 에서 경로 상수화
cookienc Sep 7, 2023
8a1647e
refactor: 페이지 이름을 관리하는 Path 추가
cookienc Sep 7, 2023
10bb2f2
refactor: requestLine 빈 줄 제거
cookienc Sep 7, 2023
2f774c9
refactor: QueryString 으로 리팩토링
cookienc Sep 7, 2023
f7d6fc2
refactor: keySet()을 entrySet()으로 변경
cookienc Sep 7, 2023
da45451
feat: Controller 추가
cookienc Sep 7, 2023
23216a7
refactor: AbstractController 로 리팩토링
cookienc Sep 7, 2023
4180472
refactor: 필요없는 파일 제거
cookienc Sep 7, 2023
0e0cb5b
refactor: Mapping 을 Controller 로 이름 변경
cookienc Sep 7, 2023
83a2309
refactor: Login 관련 컨트롤러를 하나로 합침
cookienc Sep 7, 2023
3beba19
refactor: Register 관련 파일을 하나로 합침
cookienc Sep 7, 2023
708cad5
refactor: 패키지 변경
cookienc Sep 7, 2023
6d188b4
refactor: example 패키지 CacheBustingWebConfig의 상수의 접근제한자 변경
cookienc Sep 7, 2023
937e0f7
refactor: 의존성 끊기 위한 패키지 분리 및 인스턴스 변수 추가
cookienc Sep 8, 2023
4a2e985
refactor: FrontController 를 FrontHandler 로 리팩토링
cookienc Sep 8, 2023
cabedba
refactor: HttpResponseBuilder 제거
cookienc Sep 8, 2023
cbc9455
refactor: 사용하지 않는 메서드 제거
cookienc Sep 8, 2023
954862a
test: 테스트를 HttpRequest, HttpResponse 로 리팩토링
cookienc Sep 8, 2023
694cca8
test: StubSockect 으로 serialize 코드 옮김
cookienc Sep 8, 2023
0ecf383
refactor: 로그인 테스트 하기 쉬운 구조로 변경
cookienc Sep 8, 2023
1994cc3
test: E2E 테스트 작성
cookienc Sep 8, 2023
f3cfa28
test: Processor 테스트 분리
cookienc Sep 8, 2023
c91cd6d
fix: 자원 공유 문제 해결
cookienc Sep 8, 2023
8ff9c92
refactor: LoginController 메서드 분리
cookienc Sep 8, 2023
86b6d32
feat: Default Max Thread Size 적용
cookienc Sep 8, 2023
207e696
refactor: SessionManager에 ConcurrentHashMap 적용
cookienc Sep 8, 2023
73966b1
refactor: code smell 제거
cookienc Sep 8, 2023
ed006cb
refactor: 필요없는 static 제거
cookienc Sep 12, 2023
93513c9
refactor: 메서드명 변경
cookienc Sep 12, 2023
f3c9cb7
refactor: HttpResponse serialize 수정
cookienc Sep 12, 2023
5f735eb
refactor: 분기문을 stream 으로 정리
cookienc Sep 12, 2023
e2b96bb
test: 잘못된 테스트 수정
cookienc Sep 13, 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
@@ -0,0 +1,21 @@
package cache.com.example.cachecontrol;

import com.google.common.net.HttpHeaders;
import org.springframework.http.CacheControl;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class CacheInterceptor implements HandlerInterceptor {

@Override
public void postHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler, final ModelAndView modelAndView) throws Exception {
final String cacheControlValue = CacheControl.noCache()
.cachePrivate()
.getHeaderValue();

response.addHeader(HttpHeaders.CACHE_CONTROL, cacheControlValue);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,7 @@ public class CacheWebConfig implements WebMvcConfigurer {

@Override
public void addInterceptors(final InterceptorRegistry registry) {
registry.addInterceptor(new CacheInterceptor())
.excludePathPatterns("/resources/**");
}
}
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;

@Configuration
public class EtagFilterConfiguration {

// @Bean
// public FilterRegistrationBean<ShallowEtagHeaderFilter> shallowEtagHeaderFilter() {
// return null;
// }
@Bean
public FilterRegistrationBean<ShallowEtagHeaderFilter> shallowEtagHeaderFilter() {
final FilterRegistrationBean<ShallowEtagHeaderFilter> shallowEtagHeaderFilterFilterRegistrationBean
= new FilterRegistrationBean<>(new ShallowEtagHeaderFilter());
shallowEtagHeaderFilterFilterRegistrationBean.addUrlPatterns("/etag", "/resources/*");
shallowEtagHeaderFilterFilterRegistrationBean.setName("etagFilter");
return shallowEtagHeaderFilterFilterRegistrationBean;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,28 @@

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.util.concurrent.TimeUnit;

@Configuration
public class CacheBustingWebConfig implements WebMvcConfigurer {

public static final String PREFIX_STATIC_RESOURCES = "/resources";
private static final Long MAX_AGE = 365L;

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/");
.setCacheControl(CacheControl.maxAge(MAX_AGE, TimeUnit.DAYS).cachePublic()).addResourceLocations("classpath:/static/");
}
}
6 changes: 5 additions & 1 deletion study/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ handlebars:
server:
tomcat:
accept-count: 1
max-connections: 1
max-connections: 2
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
2 changes: 1 addition & 1 deletion study/src/test/java/thread/stage1/UserServlet.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public void service(final User user) {
join(user);
}

private void join(final User user) {
private synchronized void join(final User user) {
if (!users.contains(user)) {
users.add(user);
}
Expand Down
6 changes: 4 additions & 2 deletions tomcat/src/main/java/nextstep/Application.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package nextstep;

import nextstep.jwp.controller.FrontHandler;
import org.apache.catalina.startup.Tomcat;
import org.apache.coyote.http.session.SessionManager;

public class Application {

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

import org.apache.coyote.controller.Controller;
import org.apache.coyote.controller.RequestHandler;
import org.apache.coyote.http.LoginManager;
import org.apache.coyote.http.request.HttpRequest;
import org.apache.coyote.http.response.HttpResponse;

import java.io.IOException;
import java.util.HashSet;
import java.util.Set;

import static nextstep.jwp.controller.Path.NOT_FOUND;

public class FrontHandler implements RequestHandler {

private final Set<Controller> requestControllers = new HashSet<>();

public FrontHandler(final LoginManager loginManager) {
requestControllers.add(new HomePageController());
requestControllers.add(new StaticFileController());
requestControllers.add(new LoginController(loginManager));
requestControllers.add(new RegisterController());
}

@Override
public void handle(final HttpRequest request, final HttpResponse response) throws IOException {
for (final Controller controller : requestControllers) {
if (controller.supports(request)) {
controller.service(request, response);
return;
}
}

response.mapToRedirect(NOT_FOUND.getValue());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package nextstep.jwp.controller;

import org.apache.coyote.http.common.ContentType;
import org.apache.coyote.http.common.HttpBody;
import org.apache.coyote.http.request.HttpRequest;
import org.apache.coyote.http.response.HttpResponse;
import org.apache.coyote.http.response.StatusCode;
import org.apache.coyote.http.response.StatusLine;

import static org.apache.coyote.http.common.HttpHeader.CONTENT_TYPE;

public class HomePageController extends RequestController {

private static final String HOME_MESSAGE = "Hello world!";
private static final String TARGET_URI = "/";

@Override
public boolean supports(final HttpRequest httpRequest) {
return TARGET_URI.equals(httpRequest.getRequestUri().getRequestUri());
}

@Override
protected void doPost(final HttpRequest request, final HttpResponse response) {
throw new UnsupportedOperationException();
}

@Override
protected void doGet(final HttpRequest request, final HttpResponse response) {
response.changeStatusLine(StatusLine.from(StatusCode.OK));
response.addHeader(CONTENT_TYPE, ContentType.HTML.getValue());
response.changeBody(new HttpBody(HOME_MESSAGE));
Comment on lines +29 to +31
Copy link
Member

Choose a reason for hiding this comment

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

statusLine은 정적팩토리메서드로 HttpBody는 생성자로 생성하고 있는데 혹시 기준이 있나요?

Copy link
Author

Choose a reason for hiding this comment

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

특별한 로직 같은게 있으면 정팩메를 씁니다! StatusLine에 있는 from 메서드는 Protocol이 기본으로 들어가 있어서 로직으로 봤습니다.

}
}
171 changes: 171 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,171 @@
package nextstep.jwp.controller;

import nextstep.jwp.db.InMemoryUserRepository;
import nextstep.jwp.model.User;
import org.apache.coyote.http.LoginManager;
import org.apache.coyote.http.common.ContentType;
import org.apache.coyote.http.common.HttpBody;
import org.apache.coyote.http.common.HttpHeader;
import org.apache.coyote.http.request.HttpCookie;
import org.apache.coyote.http.request.HttpRequest;
import org.apache.coyote.http.request.QueryString;
import org.apache.coyote.http.response.HttpResponse;
import org.apache.coyote.http.response.StatusCode;
import org.apache.coyote.http.response.StatusLine;
import org.apache.coyote.http.session.Session;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

import static nextstep.jwp.controller.Path.LOGIN;
import static nextstep.jwp.controller.Path.MAIN;
import static nextstep.jwp.controller.Path.UNAUTHORIZED;
import static org.apache.coyote.http.common.HttpHeader.CONTENT_TYPE;
import static org.apache.coyote.http.common.HttpHeader.COOKIE;

public class LoginController extends RequestController {

private static final String TARGET_URI = "login";
private static final Logger log = LoggerFactory.getLogger(LoginController.class);
private static final String CREDENTIALS = "account";
private static final String PASSWORD = "password";

private final LoginManager loginManager;

public LoginController(final LoginManager loginManager) {
this.loginManager = loginManager;
}

@Override
public boolean supports(final HttpRequest httpRequest) {
return httpRequest.containsRequestUri(TARGET_URI);
}

@Override
protected void doPost(final HttpRequest request, final HttpResponse response) {
try {
final User user = getValidateUser(request);

log.info("로그인 성공! user = {}", user);
final UUID uuid = createSessionToUser(user);

setCookieAndRedirectToMain(response, uuid);
} catch (final IllegalArgumentException e) {
log.warn("login error = {}", e.getMessage());
redirectToUnAuthorized(response);
}
}

private User getValidateUser(final HttpRequest request) {
final Map<String, String> bodyParams = request.getParsedBody();
final String account = bodyParams.get(CREDENTIALS);
final String password = bodyParams.get(PASSWORD);

final User user = InMemoryUserRepository.findByAccount(account)
.orElseThrow(() -> new IllegalArgumentException("잘못된 계정입니다. 다시 입력해주세요."));

if (!user.checkPassword(password)) {
throw new IllegalArgumentException("잘못된 비밀번호입니다. 다시 입력해주세요.");
}

return user;
}

private UUID createSessionToUser(final User user) {
final UUID uuid = UUID.randomUUID();

final Map<String, String> sessionData = Map.of(CREDENTIALS, user.getAccount());
final Session session = new Session(uuid.toString());
for (final Map.Entry<String, String> entry : sessionData.entrySet()) {
session.add(entry.getKey(), entry.getValue());
}

loginManager.add(session);

return uuid;
}

private static void setCookieAndRedirectToMain(final HttpResponse response, final UUID uuid) {
response.changeStatusLine(StatusLine.from(StatusCode.FOUND));
response.addHeader(HttpHeader.LOCATION, MAIN.getValue());
response.addHeader(HttpHeader.SET_COOKIE, "JSESSIONID=" + uuid);
response.changeBody(HttpBody.empty());
}

@Override
protected void doGet(final HttpRequest request, final HttpResponse response) throws IOException {
if (isAlreadyLogined(request)) {
redirectToMain(response);
return;
}

if (wantToLogin(request)) {
doLoginProcess(request, response);
return;
}

redirectToLoginPage(response);
}

private boolean isAlreadyLogined(final HttpRequest request) {
if (!request.containsHeader(COOKIE)) {
return false;
}

final HttpCookie cookies = HttpCookie.from(request.getHeader(COOKIE));

return checkLogin(cookies.get("JSESSIONID"));
}

private void redirectToMain(final HttpResponse response) {
response.mapToRedirect(MAIN.getValue());
}

private boolean wantToLogin(final HttpRequest request) {
return request.hasQueryString();
}

private void doLoginProcess(final HttpRequest request, final HttpResponse response) {
final QueryString queryString = request.getQueryString();

final String account = queryString.get(CREDENTIALS);
final String password = queryString.get(PASSWORD);

try {
validateUserCredentials(account, password);
log.info("로그인 성공! account = {}", account);
} catch (final IllegalArgumentException e) {
log.warn("login error = {}", e.getMessage());
redirectToUnAuthorized(response);
return;
}

redirectToMain(response);
}

private void redirectToUnAuthorized(final HttpResponse response) {
response.mapToRedirect(UNAUTHORIZED.getValue());
}

private void validateUserCredentials(final String account, final String password) {
final User user = InMemoryUserRepository.findByAccount(account)
.orElseThrow(() -> new IllegalArgumentException("잘못된 계정입니다. 다시 입력해주세요."));

if (!user.checkPassword(password)) {
throw new IllegalArgumentException("잘못된 비밀번호입니다. 다시 입력해주세요.");
}
}

private boolean checkLogin(final String jSessionId) {
return loginManager.isAlreadyLogined(jSessionId);
}

private void redirectToLoginPage(final HttpResponse response) throws IOException {
response.changeStatusLine(StatusLine.from(StatusCode.OK));
response.addHeader(CONTENT_TYPE, ContentType.HTML.getValue());
response.changeBody(HttpBody.file(LOGIN.getValue()));
}
}
Loading