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단계] 리오(오영택) 미션 제출합니다. #496

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
201e52f
feat: 캐시 학습 테스트 구현
Jaeyoung22 Sep 5, 2023
a3a8543
feat: 쓰레드 학습 테스트 구현
Jaeyoung22 Sep 7, 2023
b089e99
feat: 3,4 단계 구현
Jaeyoung22 Sep 11, 2023
e71df35
fix: 에러 수정
Jaeyoung22 Sep 11, 2023
c8dfb84
refactor: 패키지 간 의존성 분리
Jaeyoung22 Sep 11, 2023
9f091bb
refactor: 매직넘버 정리 및 Http Status 추가
Jaeyoung22 Sep 11, 2023
e10d7fa
refactor: 응답 헤더에 값 설정하는 로직 변경
Jaeyoung22 Sep 11, 2023
da9fff9
refactor: 메서드 중복 제거
Jaeyoung22 Sep 11, 2023
bed381f
feat: 409 에러 시 보여줄 html 파일 생성
Jaeyoung22 Sep 11, 2023
aa96ef7
refactor: 메서드 정리
Jaeyoung22 Sep 11, 2023
695e7d8
refactor: 메서드 정리
Jaeyoung22 Sep 11, 2023
eb069cc
Merge remote-tracking branch 'origin/step3and4' into step3and4
Jaeyoung22 Sep 11, 2023
04cc270
test: 테스트 코드 추가
Jaeyoung22 Sep 11, 2023
bc072b1
docs: README 변경
Jaeyoung22 Sep 13, 2023
5e48432
refactor: Constants 클래스 패키지 변경
Jaeyoung22 Sep 13, 2023
cf1e1e6
fix: 요청에 해당하는 메서드를 호출하는 경우 예외를 발생
Jaeyoung22 Sep 13, 2023
163f07a
refactor: toString 메서드 네이밍 변경
Jaeyoung22 Sep 13, 2023
f5f027d
refactor: 누락된 선언 추가
Jaeyoung22 Sep 13, 2023
4333099
refactor: 메서드명 변경
Jaeyoung22 Sep 13, 2023
816870e
refactor: 추상화에 맞게 변경
Jaeyoung22 Sep 13, 2023
9d67479
refactor: 싱글턴 패턴 사용
Jaeyoung22 Sep 13, 2023
29da1dc
refactor: override 어노테이션 적용
Jaeyoung22 Sep 13, 2023
1833e9f
chore: 로깅 구현
Jaeyoung22 Sep 13, 2023
2f7ee65
feat: 예외 처리 기능 구현
Jaeyoung22 Sep 14, 2023
5e82471
docs: README 수정
Jaeyoung22 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
14 changes: 11 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,18 @@
-[x] Cookie에 JSESSIONID 값 저장하기
-[x] Session 구현하기

## 3단계 - 리팩터링
-[x] HttpRequest 클래스 구현
-[x] HttpResponse 클래스 구현
-[x] Controller 인터페이스 구현
-[x] AbstractController 추상 클래스 구현
-[x] RequestMapping 클래스 구현

### TODO
-[x] 예외 처리
-[x] 매직 넘버 정리
-[x] 리팩터링(클래스 분리, 패키지 정리)
-[ ] 상태코드, http 메서드 종류 추가
-[ ] 테스트 - 회원가입, 정적리소스
-[ ] 로깅
-[x] 상태코드, http 메서드 종류 추가
-[x] 테스트 - 회원가입, 정적리소스
-[x] 로깅
-[X] 예외 처리
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
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) {
WebContentInterceptor webContentInterceptor = new WebContentInterceptor();
Copy link
Member

Choose a reason for hiding this comment

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

WebContentInterceptor는 어떤 역할을 하나요?

Copy link
Author

Choose a reason for hiding this comment

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

학습테스트에서 캐시를 설정해주는 역할입니다!

webContentInterceptor.addCacheMapping(CacheControl.noCache().cachePrivate(), "/**");
registry.addInterceptor(webContentInterceptor);
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
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() {
ShallowEtagHeaderFilter shallowEtagHeaderFilter = new ShallowEtagHeaderFilter();
FilterRegistrationBean<ShallowEtagHeaderFilter> filterRegistrationBean = new FilterRegistrationBean<>(shallowEtagHeaderFilter);
filterRegistrationBean.addUrlPatterns("/etag", "/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.util.concurrent.TimeUnit;

@Configuration
public class CacheBustingWebConfig implements WebMvcConfigurer {

Expand All @@ -20,6 +23,8 @@ public CacheBustingWebConfig(ResourceVersion version) {
@Override
public void addResourceHandlers(final ResourceHandlerRegistry registry) {
registry.addResourceHandler(PREFIX_STATIC_RESOURCES + "/" + version.getVersion() + "/**")
.setCacheControl(CacheControl.maxAge(365L, TimeUnit.DAYS).cachePublic())
.setUseLastModified(true)
.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:
min-response-size: 10
enabled: true
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
50 changes: 50 additions & 0 deletions tomcat/src/main/java/common/http/AbstractController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package common.http;

import java.util.EnumMap;
import java.util.Map;
import java.util.function.BiConsumer;

import static common.http.HttpMethod.DELETE;
import static common.http.HttpMethod.GET;
import static common.http.HttpMethod.PATCH;
import static common.http.HttpMethod.POST;
import static common.http.HttpMethod.PUT;

public abstract class AbstractController implements Controller {

public static final String EXCEPTION_MESSAGE_WHEN_CALL_NOT_DECLARED_METHOD = "요청에 해당하는 메서드가 없습니다.";
private final Map<HttpMethod, BiConsumer<Request, Response>> methodMapping = new EnumMap<>(HttpMethod.class);

protected AbstractController() {
methodMapping.put(GET, this::doGet);
methodMapping.put(POST, this::doPost);
methodMapping.put(PUT, this::doPut);
methodMapping.put(PATCH, this::doPatch);
methodMapping.put(DELETE, this::doDelete);
}

@Override
public void service(Request request, Response response) {
methodMapping.get(request.getHttpMethod()).accept(request, response);
}

protected void doGet(Request request, Response response) {
throw new IllegalArgumentException(EXCEPTION_MESSAGE_WHEN_CALL_NOT_DECLARED_METHOD);
}

protected void doPost(Request request, Response response) {
throw new IllegalArgumentException(EXCEPTION_MESSAGE_WHEN_CALL_NOT_DECLARED_METHOD);
}

protected void doPut(Request request, Response response) {
throw new IllegalArgumentException(EXCEPTION_MESSAGE_WHEN_CALL_NOT_DECLARED_METHOD);
}

protected void doPatch(Request request, Response response) {
throw new IllegalArgumentException(EXCEPTION_MESSAGE_WHEN_CALL_NOT_DECLARED_METHOD);
}

protected void doDelete(Request request, Response response) {
throw new IllegalArgumentException(EXCEPTION_MESSAGE_WHEN_CALL_NOT_DECLARED_METHOD);
}
}
42 changes: 42 additions & 0 deletions tomcat/src/main/java/common/http/ContentType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package common.http;

import java.util.Arrays;

public enum ContentType {
HTML("text/html", "html"),
CSS("text/css", "css"),
JS("text/javascript", "js"),
ICO("image/ico", "ico"),
;

public static final String DELIMITER_FOR_EXTENSION = ".";

private final String type;
private final String extension;

ContentType(String type, String extension) {
this.type = type;
this.extension = extension;
}

public static ContentType findByExtension(String extension) {
return Arrays.stream(ContentType.values())
.filter(contentType -> contentType.extension.equals(extension))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("유효하지 않은 확장자명 입니다."));
}

public static ContentType findByPath(String path) {
int indexBeforeExtension = path.lastIndexOf(DELIMITER_FOR_EXTENSION);
if (indexBeforeExtension == -1) {
throw new IllegalArgumentException("파일의 확장자명이 없습니다.");
}

String extension = path.substring(indexBeforeExtension + 1);
return findByExtension(extension);
}

public String getType() {
return type;
}
}
5 changes: 5 additions & 0 deletions tomcat/src/main/java/common/http/Controller.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package common.http;

public interface Controller {
void service(Request request, Response response);
}
8 changes: 8 additions & 0 deletions tomcat/src/main/java/common/http/ControllerManager.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package common.http;

public interface ControllerManager {

void add(String path, Controller controller);

void service(Request request, Response response);
}
57 changes: 57 additions & 0 deletions tomcat/src/main/java/common/http/Cookie.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package common.http;

import java.util.HashMap;
import java.util.Map;

public class Cookie {

private static final String SEPARATOR = "; ";
public static final String DELIMITER = "=";

private static final int ATTRIBUTE_INDEX = 0;
private static final int VALUE_INDEX = 1;

private final Map<String, String> items;

private Cookie(Map<String, String> items) {
this.items = items;
}

public static Cookie from(String cookieHeader) {
if (cookieHeader == null || cookieHeader.isEmpty()) {
return new Cookie(new HashMap<>());
}
return new Cookie(parse(cookieHeader));
}

private static Map<String, String> parse(String values) {
Map<String, String> items = new HashMap<>();
String[] attributesAndValues = values.split(SEPARATOR);
for (String cookie : attributesAndValues) {
String[] attributeAndValue = cookie.split(DELIMITER);
items.put(attributeAndValue[ATTRIBUTE_INDEX], attributeAndValue[VALUE_INDEX].trim());
}
return items;
}

public void addAttribute(String attribute, String value) {
items.put(attribute, value);
}

boolean hasAttribute(String attribute) {
return items.containsKey(attribute);
}

String getAttribute(String attribute) {
return items.get(attribute);
}

public String getValue() {
StringBuilder stringBuilder = new StringBuilder();
for (Map.Entry<String, String> item : items.entrySet()) {
stringBuilder.append(item.getKey()).append(DELIMITER).append(item.getValue()).append(SEPARATOR);
}
String cookie = stringBuilder.toString();
return cookie.substring(0, cookie.length()-2);
}
}
19 changes: 19 additions & 0 deletions tomcat/src/main/java/common/http/Cookies.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package common.http;

public class Cookies {
Copy link
Member

Choose a reason for hiding this comment

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

일급컬렉션 같은 이름이지만 CookieManager에 가까운 성격을 가지고 있네요.
Cookie 클래스에서 이루어져도 괜찮을 로직들이라 생각하는데 Cookies로 분리한 이유가 있을까요? 리오가 생성한 Cookies 클래스의 역할은 무엇인가요?

Copy link
Author

Choose a reason for hiding this comment

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

CookieManager와 같다고 생각하고 클래스를 만들었습니다!
다만 네이밍의 경우 구구가 참고하라고 주신 코드와 맞추기 위해서 이렇게 지었습니다...!
처음에는 일급 컬렉션이라고 생각하고 해당 클래스에서 발행된 모든 쿠키를 가지고 있게 했었는데요,
생각해보니 쿠키들을 굳이 보관하고 있을 필요는 없을 것 같아서 제외했습니다.
로이스의 생각은 어떠신가요?

Copy link
Member

Choose a reason for hiding this comment

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

쿠키들을 보관하지 않다보니 Cookie의 일급컬렉션 같은 느낌이 들지 않아 질문 남겼었습니다.
요청에 따라 관리 되는 쿠키들을 보관하기엔 정적 메서드들 뿐이라 위험 할 것 같네요.
구구가 참고하라고 주신 코드가 어떤건진 모르지만..

지금 역할은 CookieManager라는 네이밍이 어울린다고 생각합니다.
아니면, 요청에 따라 Cookie -> Cookies를 생성하고 이를 Headers에서 관리하는 구조는 어떠신가요?

public Cookie {}

public Cookies {
    List<Cookie> cookies;
}

public HttpRequestHeaders {
    Cookies cookies;
}

public HttpRequest {
    HttpRequestHeaders headers;
}

---
doGet(HttpRequest request, ...) {
    request.getCookie("")
}

저는 Cookie가 HTTP 요청에 꽤나 의존적(?)이라(요청에 담겨서 오고 이를 꺼내서 활용 ≈ 헤더) 생각해서요.
적용하실 필요는 없고, 제 생각을 잘 전달하고자 적은 내용들입니다!


private static final String JSESSIONID = "JSESSIONID";

private Cookies() {}

public static Cookie ofJSessionId(String id) {
return Cookie.from(JSESSIONID + Cookie.DELIMITER + id);
}

public static String getJsessionid(Cookie cookie) {
if (cookie.hasAttribute(JSESSIONID)) {
return cookie.getAttribute(JSESSIONID);
}
throw new IllegalArgumentException("쿠키에 세션 아이디가 없습니다.");
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.apache.coyote.http11;
package common.http;

public enum HttpMethod {
GET,
Expand Down
33 changes: 33 additions & 0 deletions tomcat/src/main/java/common/http/HttpStatus.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package common.http;

public enum HttpStatus {
OK("OK", 200),
CREATED("Created", 201),
ACCEPTED("Accepted", 202),
NO_CONTENT("No Content", 204),
MOVED_PERMANENTLY("Moved Permanently", 301),
FOUND("Found", 302),
PERMANENT_REDIRECT("Permanent Redirect", 308),
BAD_REQUEST("Bad Request", 400),
UNAUTHORIZED("Unauthorized", 401),
FORBIDDEN("Forbidden", 403),
NOT_FOUND("Not Found", 404),
CONFLICT("Conflict", 409),
INTERNAL_SERVER_ERROR("Internal Server Error", 500);

private final String statusMessage;
private final int statusCode;

HttpStatus(String statusMessage, int statusCode) {
this.statusMessage = statusMessage;
this.statusCode = statusCode;
}

public String getStatusMessage() {
return statusMessage;
}

public int getStatusCode() {
return statusCode;
}
}
28 changes: 28 additions & 0 deletions tomcat/src/main/java/common/http/Request.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package common.http;

public interface Request {
Copy link
Member

Choose a reason for hiding this comment

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

Request를 추상화한 이유가 무엇인가요?!
어떠한 추가적인 상황을 예상하셨나요?

Copy link
Author

Choose a reason for hiding this comment

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

HttpRequest와 HttpResponse 모두 원래 coyote에 있었던 걸로 기억합니다!
그런데 jwp 내에서도 그렇고 catalina 에서도 해당 클래스를 사용하더라구요!
그래서 패키지간 의존성을 분리하기 위해서 common 패키지를 만들어서 catalina와 coyote 모두 common을 바라보게 했습니다!

프로토콜이 변경됐을때도 사용할 수 있도록 하고 싶은데 이건 조금 더 추상화를 해봐야겠네요..

Copy link
Member

Choose a reason for hiding this comment

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

그런 의도셨군요. 패키지간의 응집도와 역할, 의존도를 엄청 신경 쓰신게 느껴졌습니다.👍👍
common의 이름도 이제 이해가 됩니다.


HttpMethod getHttpMethod();

String getVersionOfTheProtocol();

String getAccount();

String getPassword();

Session getSession(boolean create);

Session getSession();

String getCookie();

String getPath();

boolean hasValidSession();

String getEmail();

void addSession(Session session);

boolean hasStaticResourcePath();
}
Loading