Skip to content

[톰캣 구현하기 - 3, 4단계] 아마란스(송세연) 미션 제출합니다. #440

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 40 commits into from
Sep 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
c4839eb
refactor: 코드스타일 indent를 4로 맞춤
Sep 7, 2023
f20b3fb
refactor: Request 파싱 작업을 Processor 클래스로 이동
Sep 7, 2023
df6a578
refactor: Http Cookie 생성자를 정적 팩토리 메서드로 변경
Sep 7, 2023
530d19f
refactor: HttpResponse에 protocol version 필드 추가
Sep 7, 2023
0afe5e0
refactor: HttpRequest 필드를 원시값 포장
Sep 7, 2023
3fa0e76
refactor: HttpRequest 관련 클래스 이름 통일 및 경로 이동
Sep 7, 2023
a1b06b2
refactor: Http Response 관련 클래스 경로 이동
Sep 7, 2023
bcff3dd
refactor: Controller 클래스 분리
Sep 8, 2023
7d5e3b9
refactor: TODO 주석 제거
Sep 9, 2023
1135fcb
chore: parameterize 테스트를 위한 의존성 추가
Sep 9, 2023
96c2aaf
test: 스레드 학습 테스트 작성
Sep 9, 2023
392810f
feat: 스레드 풀 적용
Sep 9, 2023
28b6dbc
feat: 세션 컬렉션을 Concurrent Collection으로 변경
Sep 9, 2023
105aa98
test: GET 요청 시 반환 페이지 검증 테스트
Sep 9, 2023
6fef3cf
test: POST 요청 시 테스트 작성
Sep 9, 2023
1f05cdd
fix: 로그인 후 /login 페이지 접근 시 302 응답을 반환하도록 수정
Sep 9, 2023
35cb32f
test: 로그인 후 로그인 화면 접속 시 index.html로 리다이렉트되는 기능 테스트 작성
Sep 9, 2023
997a73e
test: 회원가입 후 로그인 시 로그인이 성공하는 테스트 추가
Sep 9, 2023
09371b0
test: public 메서드의 유효성 검증 테스트 작성
Sep 9, 2023
ac561ce
fix: NPE 방지
Sep 9, 2023
ee5ef18
chore: 휴리스틱 캐싱 제거
Sep 11, 2023
d4eb5c9
chore: http 응답 압축 적용
Sep 11, 2023
c91f454
chore: /etag 요청에 대한 응답에 ETag가 추가되도록 구현
Sep 11, 2023
f665ef6
chore: 캐시 무효화 학습테스트 해결
Sep 11, 2023
c97b2aa
refactor: 잘못된 케이스 수정
Sep 11, 2023
55beb87
refactor: getOrDefault() 메서드를 사용하도록 리팩토링
Sep 11, 2023
fefe0c2
refactor: 불필요한 공백 제거
Sep 11, 2023
186f4ea
refactor: null 반환 구문 제거
Sep 11, 2023
f4b112a
refactor: HttpRequest 객체 생성을 정적 팩토리 메서드에서 수행하도록 수정
Sep 11, 2023
5536e40
refactor: Request 입력 및 파싱 역할을 각 객체에 적절하게 분담
Sep 11, 2023
f1542e3
refactor: Controller 코드가 response를 파라미터로 받도록 수정
Sep 11, 2023
6377c26
refactor: 메서드명 수정 및 파일 끝 줄바꿈 추정
Sep 11, 2023
1f7dd01
refactor: 응답 헤더 생성에 빌더 패턴 적용
Sep 11, 2023
fdf8396
refactor: 2뎁스를 1뎁스로 리팩토링
Sep 11, 2023
65b1508
refactor: POST 요청의 Request body 처리를 Request 객체로 이동
Sep 11, 2023
6b595a9
refactor: Controller 클래스들을 nextstep.jwp패키지로 이동
Sep 11, 2023
3ae799c
refactor: 스레드 대기시간을 0.3초로 수정
Sep 11, 2023
e8afc39
test: 깨지는 테스트 해결 및 HttpRequestBody parse() 테스트 추가
Sep 11, 2023
befdb89
test: 깨지는 테스트 해결
Sep 11, 2023
c982fc6
refactor: sonarLint 통과하도록 수정
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
20 changes: 18 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,22 @@

- [x] Request, Response 객체 분리
- [x] Http11Processor의 메서드 분리
- [ ] Controller 분리
- [x] Controller 분리
- [x] 테스트 작성하기
- [x] / 페이지 리다이렉트 확인
- [x] /index.html 페이지 리다이렉트 확인
- [x] /login 페이지 리다이렉트 확인
- [x] 로그인 성공 시 index.html 리다이렉트 및 set-cookie 설정 여부 확인
- [x] 로그인 실패 시 401.html 파일 리다이렉트 확인
- [x] 로그인 후 /login 페이지 접근 시 index.html 리다이렉트 확인
- [x] /register 페이지 리다이렉트 확인
- [x] 회원가입 시 로그인 가능해지는지 확인
- [x] 회원가입 후 index.html 리다이렉트 확인
- [x] 404 페이지 리다이렉트 확인
- [x] 유효성 검증 테스트

## 4단계 - 동시성 확장하기
## 4단계 - 동시성 확장하기

- [x] 학습테스트 마무리
- [x] Connector의 동작을 ThreadPool에서 관리하도록 구현
- [x] 세션을 저장하는 객체를 Concurrent Collection으로 변
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
package cache.com.example.cachecontrol;

import cache.com.example.version.ResourceVersion;
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;

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

@Configuration
public class CacheWebConfig implements WebMvcConfigurer {
private final ResourceVersion version;

public CacheWebConfig(final ResourceVersion version) {
this.version = version;
}

@Override
public void addInterceptors(final InterceptorRegistry registry) {
WebContentInterceptor interceptor = new WebContentInterceptor();
interceptor.addCacheMapping(CacheControl.noCache().cachePrivate(), "/*");
registry.addInterceptor(interceptor)
.excludePathPatterns(PREFIX_STATIC_RESOURCES + "/" + version.getVersion() + "/**");
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,27 @@
package cache.com.example.etag;

import cache.com.example.version.ResourceVersion;
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;
// }
private final ResourceVersion version;

public EtagFilterConfiguration(final ResourceVersion version) {
this.version = version;
}

@Bean
public FilterRegistrationBean<ShallowEtagHeaderFilter> shallowEtagHeaderFilter() {
final FilterRegistrationBean<ShallowEtagHeaderFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new ShallowEtagHeaderFilter());
registration.addUrlPatterns("/etag", PREFIX_STATIC_RESOURCES + "/" + version.getVersion() + "/*");
return registration;
}
}
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
6 changes: 3 additions & 3 deletions study/src/test/java/thread/stage0/SynchronizationTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
* 다중 스레드 환경에서 두 개 이상의 스레드가 변경 가능한(mutable) 공유 데이터를 동시에 업데이트하면 경쟁 조건(race condition)이 발생한다.
* 자바는 공유 데이터에 대한 스레드 접근을 동기화(synchronization)하여 경쟁 조건을 방지한다.
* 동기화된 블록은 하나의 스레드만 접근하여 실행할 수 있다.
*
* <p>
* Synchronization
* https://docs.oracle.com/javase/tutorial/essential/concurrency/sync.html
*/
Expand All @@ -21,7 +21,7 @@ class SynchronizationTest {
/**
* 테스트가 성공하도록 SynchronizedMethods 클래스에 동기화를 적용해보자.
* synchronized 키워드에 대하여 찾아보고 적용하면 된다.
*
* <p>
* Guide to the Synchronized Keyword in Java
* https://www.baeldung.com/java-synchronized
*/
Expand All @@ -41,7 +41,7 @@ private static final class SynchronizedMethods {

private int sum = 0;

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

Expand Down
10 changes: 5 additions & 5 deletions study/src/test/java/thread/stage0/ThreadPoolsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@
/**
* 스레드 풀은 무엇이고 어떻게 동작할까?
* 테스트를 통과시키고 왜 해당 결과가 나왔는지 생각해보자.
*
* <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
*/
Expand All @@ -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
1 change: 1 addition & 0 deletions tomcat/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ dependencies {

testImplementation "org.assertj:assertj-core:3.24.2"
testImplementation "org.mockito:mockito-core:5.4.0"
testImplementation "org.junit.jupiter:junit-jupiter-params:5.7.0"
testImplementation "org.junit.jupiter:junit-jupiter-api:5.7.2"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.7.2"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package nextstep.jwp.controller;

import org.apache.coyote.http11.controller.AbstractController;
import org.apache.coyote.http11.request.HttpRequest;
import org.apache.coyote.http11.response.HttpResponse;
import org.apache.coyote.http11.response.HttpResponseHeader;
import org.apache.coyote.http11.response.HttpResponseStatus;

public class DefaultGetController extends AbstractController {


@Override
public boolean isSupported(HttpRequest request) {
return request.isGET() && request.isSamePath("/");
}

@Override
protected void doGet(HttpRequest request, HttpResponse response) {
String responseBody = "Hello world!";

HttpResponseHeader responseHeader = new HttpResponseHeader.Builder(
readContentType(request.getAccept(), request.getPath()),
String.valueOf(responseBody.getBytes().length))
.build();
response.updateResponse(HttpResponseStatus.OK, responseHeader, responseBody);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package nextstep.jwp.controller;

import org.apache.catalina.SessionManager;
import org.apache.coyote.http11.controller.AbstractController;
import org.apache.coyote.http11.request.HttpCookie;
import org.apache.coyote.http11.request.HttpRequest;
import org.apache.coyote.http11.response.HttpResponse;
import org.apache.coyote.http11.response.HttpResponseHeader;
import org.apache.coyote.http11.response.HttpResponseStatus;

import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;

public class LoginGetController extends AbstractController {
public static final String JSESSIONID = "JSESSIONID";

@Override
public boolean isSupported(HttpRequest request) {
return request.isGET() && request.isSamePath("/login");
}


@Override
protected void doGet(HttpRequest request, HttpResponse response) throws URISyntaxException, IOException {
final HttpCookie cookie = request.getCookie();
URL filePathUrl;
if (isLogin(cookie)) {
HttpResponseHeader responseHeader = new HttpResponseHeader.Builder(
readContentType(request.getAccept(), request.getPath()),
String.valueOf(0))
.addLocation("/index.html")
.build();
response.updateResponse(HttpResponseStatus.FOUND, responseHeader, "");
return;
}
filePathUrl = getClass().getResource("/static/login.html");
String responseBody = readHtmlFile(filePathUrl);

HttpResponseHeader responseHeader = new HttpResponseHeader.Builder(
readContentType(request.getAccept(), request.getPath()),
String.valueOf(responseBody.getBytes().length))
.build();
response.updateResponse(HttpResponseStatus.OK, responseHeader, responseBody);
}

private boolean isLogin(HttpCookie cookie) {
return cookie.isExist(JSESSIONID)
&& SessionManager.getInstance().findSession(cookie.findCookie(JSESSIONID)) != null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package nextstep.jwp.controller;

import nextstep.jwp.db.InMemoryUserRepository;
import nextstep.jwp.model.User;
import org.apache.catalina.SessionManager;
import org.apache.coyote.http11.controller.AbstractController;
import org.apache.coyote.http11.exception.UnauthorizeException;
import org.apache.coyote.http11.request.HttpCookie;
import org.apache.coyote.http11.request.HttpRequest;
import org.apache.coyote.http11.response.HttpResponse;
import org.apache.coyote.http11.response.HttpResponseHeader;
import org.apache.coyote.http11.response.HttpResponseStatus;

import java.util.Map;
import java.util.UUID;

public class LoginPostController extends AbstractController {
public static final String JSESSIONID = "JSESSIONID";

@Override
public boolean isSupported(HttpRequest request) {
return request.isPOST() && request.isSamePath("/login");
}

@Override
protected void doPost(HttpRequest request, HttpResponse response) {
if (request.isNotExistBody()) {
throw new IllegalArgumentException("로그인 정보가 입력되지 않았습니다.");
}
final HttpCookie cookie = request.getCookie();
Map<String, String> parsedRequestBody = request.parseBody();
User user = InMemoryUserRepository.findByAccount(parsedRequestBody.get("account"))
.orElseThrow(() -> new UnauthorizeException("입력한 회원 ID가 존재하지 않습니다."));
if (isLoginFail(user, parsedRequestBody)) {
throw new UnauthorizeException("입력한 회원 ID가 존재하지 않습니다.");
}
if (!cookie.isExist(JSESSIONID)) {
String jSessionId = String.valueOf(UUID.randomUUID());
String setCookie = JSESSIONID + "=" + jSessionId;
SessionManager.getInstance().addLoginSession(jSessionId, user);
HttpResponseHeader responseHeader = new HttpResponseHeader.Builder(
readContentType(request.getAccept(), request.getPath()), String.valueOf(0))
.addLocation("/index.html")
.addSetCookie(setCookie)
.build();
response.updateResponse(HttpResponseStatus.FOUND, responseHeader, "");
return;
}
HttpResponseHeader responseHeader = new HttpResponseHeader.Builder(
readContentType(request.getAccept(), request.getPath()), String.valueOf(0))
.addLocation("/index.html")
.build();
response.updateResponse(HttpResponseStatus.FOUND, responseHeader, "");

}

private boolean isLoginFail(User user, Map<String, String> parsedRequestBody) {
return !user.checkPassword(parsedRequestBody.get("password"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package nextstep.jwp.controller;

import org.apache.coyote.http11.controller.AbstractController;
import org.apache.coyote.http11.request.HttpRequest;
import org.apache.coyote.http11.response.HttpResponse;
import org.apache.coyote.http11.response.HttpResponseHeader;
import org.apache.coyote.http11.response.HttpResponseStatus;

import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;

public class RegisterGetController extends AbstractController {


@Override
public boolean isSupported(HttpRequest request) {
return request.isGET() && request.isSamePath("/register");
}

@Override
protected void doGet(HttpRequest request, HttpResponse response) throws URISyntaxException, IOException {
URL filePathUrl = getClass().getResource("/static/register.html");
String responseBody = readHtmlFile(filePathUrl);

HttpResponseHeader responseHeader = new HttpResponseHeader.Builder(
readContentType(request.getAccept(), request.getPath()),
String.valueOf(responseBody.getBytes().length))
.build();
response.updateResponse(HttpResponseStatus.OK, responseHeader, responseBody);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package nextstep.jwp.controller;

import nextstep.jwp.db.InMemoryUserRepository;
import nextstep.jwp.model.User;
import org.apache.coyote.http11.controller.AbstractController;
import org.apache.coyote.http11.request.HttpRequest;
import org.apache.coyote.http11.response.HttpResponse;
import org.apache.coyote.http11.response.HttpResponseHeader;
import org.apache.coyote.http11.response.HttpResponseStatus;

import java.util.Map;

public class RegisterPostController extends AbstractController {

@Override
public boolean isSupported(HttpRequest request) {
return request.isPOST() && request.isSamePath("/register");
}


@Override
protected void doPost(HttpRequest request, HttpResponse response) {
if (request.isNotExistBody()) {
throw new IllegalArgumentException("회원가입 정보가 입력되지 않았습니다.");
}
Map<String, String> parsedRequestBody = request.parseBody();
InMemoryUserRepository.save(new User(
Long.getLong(parsedRequestBody.get("id")),
parsedRequestBody.get("account"),
parsedRequestBody.get("password"),
parsedRequestBody.get("email")
));
HttpResponseHeader responseHeader = new HttpResponseHeader.Builder(
readContentType(request.getAccept(), request.getPath()),
String.valueOf(0))
.addLocation("/index.html")
.build();
response.updateResponse(HttpResponseStatus.FOUND, responseHeader, "");
}
}
Loading