From 68bceee361f7d0bb28aa431f92190bffda2c455a Mon Sep 17 00:00:00 2001 From: ashsty Date: Thu, 5 Sep 2024 15:38:17 +0900 Subject: [PATCH 01/11] =?UTF-8?q?chore:=20=EC=84=A4=EC=A0=95=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- study/src/main/java/cache/com/example/GreetingController.java | 2 +- .../main/java/cache/com/example/version/ResourceVersion.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/study/src/main/java/cache/com/example/GreetingController.java b/study/src/main/java/cache/com/example/GreetingController.java index 978eefdc34..c0053cda42 100644 --- a/study/src/main/java/cache/com/example/GreetingController.java +++ b/study/src/main/java/cache/com/example/GreetingController.java @@ -5,7 +5,7 @@ import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletResponse; @Controller public class GreetingController { diff --git a/study/src/main/java/cache/com/example/version/ResourceVersion.java b/study/src/main/java/cache/com/example/version/ResourceVersion.java index 7049b3d82a..27a2f22813 100644 --- a/study/src/main/java/cache/com/example/version/ResourceVersion.java +++ b/study/src/main/java/cache/com/example/version/ResourceVersion.java @@ -2,7 +2,7 @@ import org.springframework.stereotype.Component; -import javax.annotation.PostConstruct; +import jakarta.annotation.PostConstruct; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; From bc922fd15b210c9424692208e5ca39e2c524792d Mon Sep 17 00:00:00 2001 From: ashsty Date: Thu, 5 Sep 2024 15:39:30 +0900 Subject: [PATCH 02/11] =?UTF-8?q?test:=20=ED=95=99=EC=8A=B5=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=A7=84=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- study/src/test/java/study/FileTest.java | 37 +++++++++++----- study/src/test/java/study/IOStreamTest.java | 49 +++++++++++++++------ 2 files changed, 61 insertions(+), 25 deletions(-) diff --git a/study/src/test/java/study/FileTest.java b/study/src/test/java/study/FileTest.java index e1b6cca042..50a2d0c335 100644 --- a/study/src/test/java/study/FileTest.java +++ b/study/src/test/java/study/FileTest.java @@ -3,8 +3,11 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; import java.nio.file.Path; -import java.util.Collections; +import java.nio.file.Paths; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -18,36 +21,48 @@ class FileTest { /** * resource 디렉터리 경로 찾기 - * + *

* File 객체를 생성하려면 파일의 경로를 알아야 한다. * 자바 애플리케이션은 resource 디렉터리에 HTML, CSS 같은 정적 파일을 저장한다. * resource 디렉터리의 경로는 어떻게 알아낼 수 있을까? */ @Test - void resource_디렉터리에_있는_파일의_경로를_찾는다() { + void resource_디렉터리에_있는_파일의_경로를_찾는다() throws URISyntaxException { final String fileName = "nextstep.txt"; + /* - // todo - final String actual = ""; + 1. URL에서 경로 별도 분리 + // 리소스 파일의 URL을 얻어옴 + final URL resource = getClass().getClassLoader().getResource(fileName); + + // URL에서 경로를 추출함 + final String actual = resource.getPath();*/ + + // 2. 정적 경로 고정 + //Path path = Paths.get("src/test/resources", fileName); + + // 3. 동적 경로 할당 + final Path path = Paths.get(getClass().getClassLoader().getResource(fileName).toURI()); + System.out.println(path.toAbsolutePath()); + + final String actual = path.toString(); assertThat(actual).endsWith(fileName); } /** * 파일 내용 읽기 - * + *

* 읽어온 파일의 내용을 I/O Stream을 사용해서 사용자에게 전달 해야 한다. * File, Files 클래스를 사용하여 파일의 내용을 읽어보자. */ @Test - void 파일의_내용을_읽는다() { + void 파일의_내용을_읽는다() throws IOException, URISyntaxException { final String fileName = "nextstep.txt"; - // todo - final Path path = null; + final Path path = Paths.get(getClass().getClassLoader().getResource(fileName).toURI()); - // todo - final List actual = Collections.emptyList(); + final List actual = Files.readAllLines(path); assertThat(actual).containsOnly("nextstep"); } diff --git a/study/src/test/java/study/IOStreamTest.java b/study/src/test/java/study/IOStreamTest.java index 47a79356b6..e83174637f 100644 --- a/study/src/test/java/study/IOStreamTest.java +++ b/study/src/test/java/study/IOStreamTest.java @@ -12,11 +12,11 @@ /** * 자바는 스트림(Stream)으로부터 I/O를 사용한다. * 입출력(I/O)은 하나의 시스템에서 다른 시스템으로 데이터를 이동 시킬 때 사용한다. - * + *

* InputStream은 데이터를 읽고, OutputStream은 데이터를 쓴다. * FilterStream은 InputStream이나 OutputStream에 연결될 수 있다. * FilterStream은 읽거나 쓰는 데이터를 수정할 때 사용한다. (e.g. 암호화, 압축, 포맷 변환) - * + *

* Stream은 데이터를 바이트로 읽고 쓴다. * 바이트가 아닌 텍스트(문자)를 읽고 쓰려면 Reader와 Writer 클래스를 연결한다. * Reader, Writer는 다양한 문자 인코딩(e.g. UTF-8)을 처리할 수 있다. @@ -26,7 +26,7 @@ class IOStreamTest { /** * OutputStream 학습하기 - * + *

* 자바의 기본 출력 클래스는 java.io.OutputStream이다. * OutputStream의 write(int b) 메서드는 기반 메서드이다. * public abstract void write(int b) throws IOException; @@ -39,7 +39,7 @@ class OutputStream_학습_테스트 { * OutputStream의 서브 클래스(subclass)는 특정 매체에 데이터를 쓰기 위해 write(int b) 메서드를 사용한다. * 예를 들어, FilterOutputStream은 파일로 데이터를 쓸 때, * 또는 DataOutputStream은 자바의 primitive type data를 다른 매체로 데이터를 쓸 때 사용한다. - * + *

* write 메서드는 데이터를 바이트로 출력하기 때문에 비효율적이다. * write(byte[] data)write(byte b[], int off, int len) 메서드는 * 1바이트 이상을 한 번에 전송 할 수 있어 훨씬 효율적이다. @@ -53,7 +53,7 @@ class OutputStream_학습_테스트 { * todo * OutputStream 객체의 write 메서드를 사용해서 테스트를 통과시킨다 */ - + outputStream.write(bytes); final String actual = outputStream.toString(); assertThat(actual).isEqualTo("nextstep"); @@ -63,7 +63,7 @@ class OutputStream_학습_테스트 { /** * 효율적인 전송을 위해 스트림에서 버퍼링을 사용 할 수 있다. * BufferedOutputStream 필터를 연결하면 버퍼링이 가능하다. - * + *

* 버퍼링을 사용하면 OutputStream을 사용할 때 flush를 사용하자. * flush() 메서드는 버퍼가 아직 가득 차지 않은 상황에서 강제로 버퍼의 내용을 전송한다. * Stream은 동기(synchronous)로 동작하기 때문에 버퍼가 찰 때까지 기다리면 @@ -79,6 +79,7 @@ class OutputStream_학습_테스트 { * ByteArrayOutputStream과 어떤 차이가 있을까? */ + outputStream.flush(); verify(outputStream, atLeastOnce()).flush(); outputStream.close(); } @@ -96,6 +97,13 @@ class OutputStream_학습_테스트 { * try-with-resources를 사용한다. * java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다. */ + // 1. close + // outputStream.close(); + + // 2. try-with-resources + try (outputStream) { + + } verify(outputStream, atLeastOnce()).close(); } @@ -103,12 +111,12 @@ class OutputStream_학습_테스트 { /** * InputStream 학습하기 - * + *

* 자바의 기본 입력 클래스는 java.io.InputStream이다. * InputStream은 다른 매체로부터 바이트로 데이터를 읽을 때 사용한다. * InputStream의 read() 메서드는 기반 메서드이다. * public abstract int read() throws IOException; - * + *

* InputStream의 서브 클래스(subclass)는 특정 매체에 데이터를 읽기 위해 read() 메서드를 사용한다. */ @Nested @@ -128,7 +136,9 @@ class InputStream_학습_테스트 { * todo * inputStream에서 바이트로 반환한 값을 문자열로 어떻게 바꿀까? */ - final String actual = ""; + + inputStream.read(bytes); + final String actual = new String(bytes, "UTF-8"); assertThat(actual).isEqualTo("🤩"); assertThat(inputStream.read()).isEqualTo(-1); @@ -149,13 +159,14 @@ class InputStream_학습_테스트 { * java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다. */ + inputStream.close(); verify(inputStream, atLeastOnce()).close(); } } /** * FilterStream 학습하기 - * + *

* 필터는 필터 스트림, reader, writer로 나뉜다. * 필터는 바이트를 다른 데이터 형식으로 변환 할 때 사용한다. * reader, writer는 UTF-8, ISO 8859-1 같은 형식으로 인코딩된 텍스트를 처리하는 데 사용된다. @@ -169,12 +180,13 @@ class FilterStream_학습_테스트 { * 버퍼 크기를 지정하지 않으면 버퍼의 기본 사이즈는 얼마일까? */ @Test - void 필터인_BufferedInputStream를_사용해보자() { + void 필터인_BufferedInputStream를_사용해보자() throws IOException { final String text = "필터에 연결해보자."; final InputStream inputStream = new ByteArrayInputStream(text.getBytes()); - final InputStream bufferedInputStream = null; + final InputStream bufferedInputStream = new BufferedInputStream(inputStream); - final byte[] actual = new byte[0]; + final byte[] actual = new byte[text.getBytes().length]; + bufferedInputStream.read(actual); assertThat(bufferedInputStream).isInstanceOf(FilterInputStream.class); assertThat(actual).isEqualTo("필터에 연결해보자.".getBytes()); @@ -197,16 +209,25 @@ class InputStreamReader_학습_테스트 { * 필터인 BufferedReader를 사용하면 readLine 메서드를 사용해서 문자열(String)을 한 줄 씩 읽어올 수 있다. */ @Test - void BufferedReader를_사용하여_문자열을_읽어온다() { + void BufferedReader를_사용하여_문자열을_읽어온다() throws IOException { final String emoji = String.join("\r\n", "😀😃😄😁😆😅😂🤣🥲☺️😊", "😇🙂🙃😉😌😍🥰😘😗😙😚", "😋😛😝😜🤪🤨🧐🤓😎🥸🤩", ""); final InputStream inputStream = new ByteArrayInputStream(emoji.getBytes()); + InputStreamReader inputStreamReader = new InputStreamReader(inputStream); + BufferedReader bufferedReader = new BufferedReader(inputStreamReader); final StringBuilder actual = new StringBuilder(); + String line; + + // BufferedReader에서 문자열을 읽어서 StringBuilder에 추가 + while ((line = bufferedReader.readLine()) != null) { + actual.append(line).append("\r\n"); + } + assertThat(actual).hasToString(emoji); } } From 78bf0865e41978b4ca8b2a8dea3c9904696e1f7e Mon Sep 17 00:00:00 2001 From: ashsty Date: Thu, 5 Sep 2024 15:40:23 +0900 Subject: [PATCH 03/11] =?UTF-8?q?feat:=20=EA=B8=B0=EB=B3=B8=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=EC=97=90=EC=84=9C=20"Hello=20World!"=20?= =?UTF-8?q?=EC=B6=9C=EB=A0=A5,=20=EC=9D=B8=EB=8D=B1=EC=8A=A4=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=EC=97=90=EC=84=9C=20index.html=20=EB=B0=98?= =?UTF-8?q?=ED=99=98=ED=95=98=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../apache/coyote/http11/Http11Processor.java | 42 +++++++++++++++++-- .../coyote/http11/Http11ProcessorTest.java | 4 +- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java index bb14184757..b6c938e6f3 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -5,8 +5,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.BufferedReader; +import java.io.File; import java.io.IOException; +import java.io.InputStreamReader; import java.net.Socket; +import java.net.URL; +import java.nio.file.Files; public class Http11Processor implements Runnable, Processor { @@ -27,19 +32,50 @@ public void run() { @Override public void process(final Socket connection) { try (final var inputStream = connection.getInputStream(); - final var outputStream = connection.getOutputStream()) { + final var outputStream = connection.getOutputStream(); + final InputStreamReader inputStreamReader = new InputStreamReader(inputStream); + final BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) { - final var responseBody = "Hello world!"; + final String line = bufferedReader.readLine(); + + final String[] httpRequestLine = line.split(" "); + final String method = httpRequestLine[0]; + final String requestURL = httpRequestLine[1]; + + String contentType = "text/html;charset=utf-8 "; + String responseBody = ""; + + if (requestURL.endsWith(".css")) { + contentType = "text/css"; + } + + if (method.equals("GET") && !requestURL.equals("/")) { + URL resource = getClass().getResource("/static" + requestURL); + + if (resource == null) { + responseBody = "404 Not Found"; + contentType = "text/plain"; + resource = getClass().getResource("/static/404.html"); + } + + final byte[] fileBytes = Files.readAllBytes(new File(resource.getFile()).toPath()); + responseBody = new String(fileBytes); + } + + if (method.equals("GET") && requestURL.equals("/")) { + responseBody = "Hello world!"; + } final var response = String.join("\r\n", "HTTP/1.1 200 OK ", - "Content-Type: text/html;charset=utf-8 ", + "Content-Type: " + contentType, "Content-Length: " + responseBody.getBytes().length + " ", "", responseBody); outputStream.write(response.getBytes()); outputStream.flush(); + } catch (IOException | UncheckedServletException e) { log.error(e.getMessage(), e); } diff --git a/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java b/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java index 2aba8c56e0..e0ef9d44ff 100644 --- a/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java +++ b/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java @@ -35,7 +35,7 @@ void process() { @Test void index() throws IOException { // given - final String httpRequest= String.join("\r\n", + final String httpRequest = String.join("\r\n", "GET /index.html HTTP/1.1 ", "Host: localhost:8080 ", "Connection: keep-alive ", @@ -53,7 +53,7 @@ void index() throws IOException { var expected = "HTTP/1.1 200 OK \r\n" + "Content-Type: text/html;charset=utf-8 \r\n" + "Content-Length: 5564 \r\n" + - "\r\n"+ + "\r\n" + new String(Files.readAllBytes(new File(resource.getFile()).toPath())); assertThat(socket.output()).isEqualTo(expected); From 26af54a717b09234e02f4c1e9dad6784559df52f Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Thu, 5 Sep 2024 11:11:09 +0900 Subject: [PATCH 04/11] fix: remove implementation logback-classic on gradle (#501) (cherry picked from commit fed02f6f5f4308400e55c160d9495cad010f5bfb) --- study/build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/study/build.gradle b/study/build.gradle index 5c69542f84..87a1f0313c 100644 --- a/study/build.gradle +++ b/study/build.gradle @@ -19,7 +19,6 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-webflux' - implementation 'ch.qos.logback:logback-classic:1.5.7' implementation 'org.apache.commons:commons-lang3:3.14.0' implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1' implementation 'pl.allegro.tech.boot:handlebars-spring-boot-starter:0.4.1' From 8be3accc67e4ecd849767f07687ee2640d5643c2 Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Thu, 5 Sep 2024 13:51:07 +0900 Subject: [PATCH 05/11] fix: add threads min-spare configuration on properties (#502) (cherry picked from commit 7e9135698878932274ddc1f523ba817ed9c56c70) --- study/src/main/resources/application.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/study/src/main/resources/application.yml b/study/src/main/resources/application.yml index 4e8655a962..e3503a5fb9 100644 --- a/study/src/main/resources/application.yml +++ b/study/src/main/resources/application.yml @@ -6,4 +6,5 @@ server: accept-count: 1 max-connections: 1 threads: + min-spare: 2 max: 2 From b1aeece5c5a06b50bab8c68e05576631df1a7b6b Mon Sep 17 00:00:00 2001 From: ashsty Date: Fri, 6 Sep 2024 17:01:19 +0900 Subject: [PATCH 06/11] =?UTF-8?q?feat:=20cache=20=ED=95=99=EC=8A=B5=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=A7=84=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cache/com/example/GreetingController.java | 9 ++++--- .../cachecontrol/CacheInterceptor.java | 24 +++++++++++++++++++ .../example/cachecontrol/CacheWebConfig.java | 5 ++++ .../example/etag/EtagFilterConfiguration.java | 15 ++++++++---- .../version/CacheBustingWebConfig.java | 8 ++++++- study/src/main/resources/application.yml | 15 ++++++++++++ .../{templates => static}/index.html | 0 .../resource-versioning.html | 0 8 files changed, 66 insertions(+), 10 deletions(-) create mode 100644 study/src/main/java/cache/com/example/cachecontrol/CacheInterceptor.java rename study/src/main/resources/{templates => static}/index.html (100%) rename study/src/main/resources/{templates => static}/resource-versioning.html (100%) diff --git a/study/src/main/java/cache/com/example/GreetingController.java b/study/src/main/java/cache/com/example/GreetingController.java index c0053cda42..ad6fabb714 100644 --- a/study/src/main/java/cache/com/example/GreetingController.java +++ b/study/src/main/java/cache/com/example/GreetingController.java @@ -1,18 +1,17 @@ package cache.com.example; +import jakarta.servlet.http.HttpServletResponse; import org.springframework.http.CacheControl; import org.springframework.http.HttpHeaders; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; -import jakarta.servlet.http.HttpServletResponse; - @Controller public class GreetingController { @GetMapping("/") public String index() { - return "index"; + return "index.html"; } /** @@ -25,12 +24,12 @@ public String cacheControl(final HttpServletResponse response) { .cachePrivate() .getHeaderValue(); response.addHeader(HttpHeaders.CACHE_CONTROL, cacheControl); - return "index"; + return "index.html"; } @GetMapping("/etag") public String etag() { - return "index"; + return "index.html"; } @GetMapping("/resource-versioning") diff --git a/study/src/main/java/cache/com/example/cachecontrol/CacheInterceptor.java b/study/src/main/java/cache/com/example/cachecontrol/CacheInterceptor.java new file mode 100644 index 0000000000..0270b3efd9 --- /dev/null +++ b/study/src/main/java/cache/com/example/cachecontrol/CacheInterceptor.java @@ -0,0 +1,24 @@ +package cache.com.example.cachecontrol; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.CacheControl; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +@Component +public class CacheInterceptor implements HandlerInterceptor { + + @Override + public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) throws Exception { + String cacheControl = CacheControl + .noCache() + .cachePrivate() + .getHeaderValue(); + + response.setHeader(HttpHeaders.CACHE_CONTROL, cacheControl); + + return true; + } +} diff --git a/study/src/main/java/cache/com/example/cachecontrol/CacheWebConfig.java b/study/src/main/java/cache/com/example/cachecontrol/CacheWebConfig.java index 305b1f1e1e..1292b1643d 100644 --- a/study/src/main/java/cache/com/example/cachecontrol/CacheWebConfig.java +++ b/study/src/main/java/cache/com/example/cachecontrol/CacheWebConfig.java @@ -1,5 +1,6 @@ package cache.com.example.cachecontrol; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -7,7 +8,11 @@ @Configuration public class CacheWebConfig implements WebMvcConfigurer { + @Autowired + private CacheInterceptor cacheInterceptor; + @Override public void addInterceptors(final InterceptorRegistry registry) { + registry.addInterceptor(cacheInterceptor).addPathPatterns("/"); } } diff --git a/study/src/main/java/cache/com/example/etag/EtagFilterConfiguration.java b/study/src/main/java/cache/com/example/etag/EtagFilterConfiguration.java index 41ef7a3d9a..7e265d06bb 100644 --- a/study/src/main/java/cache/com/example/etag/EtagFilterConfiguration.java +++ b/study/src/main/java/cache/com/example/etag/EtagFilterConfiguration.java @@ -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() { -// return null; -// } + @Bean + public FilterRegistrationBean shallowEtagHeaderFilter() { + FilterRegistrationBean filter = new FilterRegistrationBean<>(new ShallowEtagHeaderFilter()); + + filter.addUrlPatterns("/etag"); + + return filter; + } } diff --git a/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java b/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java index 6da6d2c795..081fe6228c 100644 --- a/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java +++ b/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java @@ -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 { @@ -20,6 +23,9 @@ public CacheBustingWebConfig(ResourceVersion version) { @Override public void addResourceHandlers(final ResourceHandlerRegistry registry) { registry.addResourceHandler(PREFIX_STATIC_RESOURCES + "/" + version.getVersion() + "/**") - .addResourceLocations("classpath:/static/"); + .addResourceLocations("classpath:/static/") + .setEtagGenerator(resource -> version.getVersion()) + .setCacheControl(CacheControl.maxAge(Duration.ofDays(365)).cachePublic()); + //.resourceChain(true); } } diff --git a/study/src/main/resources/application.yml b/study/src/main/resources/application.yml index e3503a5fb9..0e638685a2 100644 --- a/study/src/main/resources/application.yml +++ b/study/src/main/resources/application.yml @@ -8,3 +8,18 @@ server: threads: min-spare: 2 max: 2 + compression: + enabled: true + min-response-size: 128 + +spring: + resources: + cache: + cache-control: + max-age: 365d + public: true + chain: + strategy: + content: + enabled: true + paths: /js/* diff --git a/study/src/main/resources/templates/index.html b/study/src/main/resources/static/index.html similarity index 100% rename from study/src/main/resources/templates/index.html rename to study/src/main/resources/static/index.html diff --git a/study/src/main/resources/templates/resource-versioning.html b/study/src/main/resources/static/resource-versioning.html similarity index 100% rename from study/src/main/resources/templates/resource-versioning.html rename to study/src/main/resources/static/resource-versioning.html From df98d05bdbe79cc04faa41672b787328cf0a8532 Mon Sep 17 00:00:00 2001 From: ashsty Date: Fri, 6 Sep 2024 17:30:18 +0900 Subject: [PATCH 07/11] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EA=B5=AC=ED=98=84,=20=EC=BF=BC?= =?UTF-8?q?=EB=A6=AC=20=EC=8A=A4=ED=8A=B8=EB=A7=81=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../apache/coyote/http11/Http11Processor.java | 121 ++++++++++++++---- .../coyote/http11/Http11ProcessorTest.java | 3 + 2 files changed, 101 insertions(+), 23 deletions(-) diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java index b6c938e6f3..beb2c1b03a 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -1,6 +1,6 @@ package org.apache.coyote.http11; -import com.techcourse.exception.UncheckedServletException; +import com.techcourse.db.InMemoryUserRepository; import org.apache.coyote.Processor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -11,10 +11,19 @@ import java.io.InputStreamReader; import java.net.Socket; import java.net.URL; +import java.net.URLDecoder; import java.nio.file.Files; +import java.util.HashMap; +import java.util.Map; public class Http11Processor implements Runnable, Processor { + public static final String QUERY_PARAMETER = "?"; + public static final String STATIC = "/static"; + public static final String HTML = ".html"; + public static final String CSS = ".css"; + public static final String JS = ".js"; + private static final Logger log = LoggerFactory.getLogger(Http11Processor.class); private final Socket connection; @@ -38,46 +47,112 @@ public void process(final Socket connection) { final String line = bufferedReader.readLine(); + if (line == null) { + return; + } + final String[] httpRequestLine = line.split(" "); final String method = httpRequestLine[0]; final String requestURL = httpRequestLine[1]; - String contentType = "text/html;charset=utf-8 "; - String responseBody = ""; - - if (requestURL.endsWith(".css")) { - contentType = "text/css"; - } - - if (method.equals("GET") && !requestURL.equals("/")) { - URL resource = getClass().getResource("/static" + requestURL); + String contentType; + byte[] responseBody; - if (resource == null) { - responseBody = "404 Not Found"; - contentType = "text/plain"; - resource = getClass().getResource("/static/404.html"); - } + String resourcePath = determineResourcePath(requestURL); + contentType = determineContentType(resourcePath); - final byte[] fileBytes = Files.readAllBytes(new File(resource.getFile()).toPath()); - responseBody = new String(fileBytes); - } + URL resource = getClass().getResource(resourcePath); - if (method.equals("GET") && requestURL.equals("/")) { - responseBody = "Hello world!"; + if (resource == null) { + responseBody = "404 Not Found".getBytes(); + contentType = "text/plain"; + } else { + responseBody = Files.readAllBytes(new File(resource.getFile()).toPath()); } final var response = String.join("\r\n", "HTTP/1.1 200 OK ", "Content-Type: " + contentType, - "Content-Length: " + responseBody.getBytes().length + " ", + "Content-Length: " + responseBody.length + " ", "", - responseBody); + ""); outputStream.write(response.getBytes()); + outputStream.write(responseBody); outputStream.flush(); - } catch (IOException | UncheckedServletException e) { + } catch (IOException e) { log.error(e.getMessage(), e); } } + + private String determineResourcePath(String requestURL) { + if (requestURL.equals("/") || requestURL.equals("/index.html")) { + return "/static/index.html"; + } + + if (requestURL.contains(QUERY_PARAMETER)) { + String path = parseLoginQueryString(requestURL); + return STATIC + path + HTML; + } + + if (requestURL.endsWith(HTML) || requestURL.endsWith(CSS) || requestURL.endsWith(JS)) { + return STATIC + requestURL; + } + + return STATIC + requestURL + HTML; + } + + private String determineContentType(String resourcePath) { + if (resourcePath.endsWith(CSS)) { + return "text/css "; + } + + if (resourcePath.endsWith(JS)) { + return "application/javascript "; + } + + return "text/html;charset=utf-8 "; + } + + private String parseLoginQueryString(String requestURL) { + int index = requestURL.indexOf(QUERY_PARAMETER); + findUser(parseUserInfo(requestURL.substring(index + 1))); + + return requestURL.substring(0, index); + } + + private Map parseUserInfo(String queryString) { + Map userInfo = new HashMap<>(); + + String[] info = queryString.split("&"); + + for (String metadata : info) { + String[] temp = metadata.split("="); + String key = URLDecoder.decode(temp[0]); + String value = URLDecoder.decode(temp[1]); + + userInfo.put(key, value); + } + + return userInfo; + } + + private void findUser(Map userInfo) { + String account = userInfo.get("account"); + String password = userInfo.get("password"); + + InMemoryUserRepository.findByAccount(account).ifPresent( + user -> { + boolean isValidPassword = user.checkPassword(password); + if (isValidPassword) { + log.info("user : {}", user); + } + + if (!isValidPassword) { + log.info(user.getAccount() + "의 비밀번호가 잘못 입력되었습니다."); + } + } + ); + } } diff --git a/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java b/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java index e0ef9d44ff..62456ccf9d 100644 --- a/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java +++ b/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java @@ -1,5 +1,6 @@ package org.apache.coyote.http11; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import support.StubSocket; @@ -12,6 +13,8 @@ class Http11ProcessorTest { + // 메인 페이지를 "Hello World!" -> index.html로 변경하면서 테스트 비활성화 + @Disabled @Test void process() { // given From bd4f051f6ef7ed5b08c748844eff2d419c63b51e Mon Sep 17 00:00:00 2001 From: ashsty Date: Fri, 6 Sep 2024 17:30:37 +0900 Subject: [PATCH 08/11] =?UTF-8?q?docs:=20README=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/README.md b/README.md index 602236edba..afff3682eb 100644 --- a/README.md +++ b/README.md @@ -2,19 +2,50 @@ ## 톰캣 구현하기 +### 구현하면서 궁금했던 점 + +1. Http11Processor의 이 부분을 + +``` +String line = bufferedReader.readLine(); + +line = line.split(" ")[1]; + +final URL resource = getClass().getResource("/static" + line); +``` + +``` +String line = bufferedReader.readLine(); + +line = line.split(" ")[1]; + +final URL resource = getClass().getResource("/static/index.html"); +``` + +이렇게 적으면 `localhost:8080/index.html`을 호출했을 때 디스플레이 되는 페이지 모습이 달랐습니다. +(전자: 그래프 노출되지 않음) +(후자: 그래프 노출됨) + +정확한 이유가 뭘까요? 🤔 + +




+ ### 학습목표 + - 웹 서버 구현을 통해 HTTP 이해도를 높인다. - HTTP의 이해도를 높혀 성능 개선할 부분을 찾고 적용할 역량을 쌓는다. - 서블릿에 대한 이해도를 높인다. - 스레드, 스레드풀을 적용해보고 동시성 처리를 경험한다. ### 시작 가이드 + 1. 미션을 시작하기 전에 파일, 입출력 스트림 학습 테스트를 먼저 진행합니다. - [File, I/O Stream](study/src/test/java/study) - 나머지 학습 테스트는 다음 강의 시간에 풀어봅시다. 2. 학습 테스트를 완료하면 LMS의 1단계 미션부터 진행합니다. ## 학습 테스트 + 1. [File, I/O Stream](study/src/test/java/study) 2. [HTTP Cache](study/src/test/java/cache) 3. [Thread](study/src/test/java/thread) From e4c6b60b9df7dfdd3684eb695f68299bacee1f92 Mon Sep 17 00:00:00 2001 From: ashsty Date: Mon, 9 Sep 2024 19:56:59 +0900 Subject: [PATCH 09/11] =?UTF-8?q?refactor:=20=ED=95=99=EC=8A=B5=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=ED=94=BC=EB=93=9C=EB=B0=B1=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- study/src/main/resources/application.yml | 6 +----- study/src/test/java/study/IOStreamTest.java | 3 ++- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/study/src/main/resources/application.yml b/study/src/main/resources/application.yml index 0e638685a2..a415d86015 100644 --- a/study/src/main/resources/application.yml +++ b/study/src/main/resources/application.yml @@ -18,8 +18,4 @@ spring: cache-control: max-age: 365d public: true - chain: - strategy: - content: - enabled: true - paths: /js/* + diff --git a/study/src/test/java/study/IOStreamTest.java b/study/src/test/java/study/IOStreamTest.java index e83174637f..362f1a16ea 100644 --- a/study/src/test/java/study/IOStreamTest.java +++ b/study/src/test/java/study/IOStreamTest.java @@ -5,6 +5,7 @@ import org.junit.jupiter.api.Test; import java.io.*; +import java.nio.charset.StandardCharsets; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.*; @@ -138,7 +139,7 @@ class InputStream_학습_테스트 { */ inputStream.read(bytes); - final String actual = new String(bytes, "UTF-8"); + final String actual = new String(bytes, StandardCharsets.UTF_8); assertThat(actual).isEqualTo("🤩"); assertThat(inputStream.read()).isEqualTo(-1); From e982bafbb1dc08f611fd2bd2b122fcdf761998e1 Mon Sep 17 00:00:00 2001 From: ashsty Date: Mon, 9 Sep 2024 21:48:45 +0900 Subject: [PATCH 10/11] =?UTF-8?q?feat:=20404=20=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=EA=B0=80=20404=20=EC=9D=91=EB=8B=B5=EA=B3=BC=20=ED=95=A8?= =?UTF-8?q?=EA=BB=98=20404.html=EB=A5=BC=20=EB=B0=98=ED=99=98=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../apache/coyote/http11/Http11Processor.java | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java index beb2c1b03a..e35050361d 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -23,6 +23,7 @@ public class Http11Processor implements Runnable, Processor { public static final String HTML = ".html"; public static final String CSS = ".css"; public static final String JS = ".js"; + public static final String SVG = ".svg"; private static final Logger log = LoggerFactory.getLogger(Http11Processor.class); @@ -58,20 +59,21 @@ public void process(final Socket connection) { String contentType; byte[] responseBody; + String httpStatus = "200 OK "; String resourcePath = determineResourcePath(requestURL); contentType = determineContentType(resourcePath); URL resource = getClass().getResource(resourcePath); if (resource == null) { - responseBody = "404 Not Found".getBytes(); - contentType = "text/plain"; - } else { - responseBody = Files.readAllBytes(new File(resource.getFile()).toPath()); + resource = getClass().getResource("/static/404.html"); + httpStatus = "404 NOT FOUND "; } + responseBody = Files.readAllBytes(new File(resource.getFile()).toPath()); + final var response = String.join("\r\n", - "HTTP/1.1 200 OK ", + "HTTP/1.1 " + httpStatus, "Content-Type: " + contentType, "Content-Length: " + responseBody.length + " ", "", @@ -91,6 +93,10 @@ private String determineResourcePath(String requestURL) { return "/static/index.html"; } + if (requestURL.endsWith(SVG)) { + return STATIC + "/assets/img/error-404-monochrome.svg"; + } + if (requestURL.contains(QUERY_PARAMETER)) { String path = parseLoginQueryString(requestURL); return STATIC + path + HTML; @@ -112,6 +118,10 @@ private String determineContentType(String resourcePath) { return "application/javascript "; } + if (resourcePath.endsWith(SVG)) { + return "image/svg+xml "; + } + return "text/html;charset=utf-8 "; } From b6cdfca99af303251d4a180e51df82720cc65e35 Mon Sep 17 00:00:00 2001 From: ashsty Date: Mon, 9 Sep 2024 21:49:22 +0900 Subject: [PATCH 11/11] =?UTF-8?q?refactor:=20user=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=EC=9D=B4=20filter=EB=A5=BC=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../apache/coyote/http11/Http11Processor.java | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java index e35050361d..fb5547cf53 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -1,6 +1,7 @@ package org.apache.coyote.http11; import com.techcourse.db.InMemoryUserRepository; +import com.techcourse.model.User; import org.apache.coyote.Processor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -15,6 +16,7 @@ import java.nio.file.Files; import java.util.HashMap; import java.util.Map; +import java.util.Optional; public class Http11Processor implements Runnable, Processor { @@ -152,17 +154,17 @@ private void findUser(Map userInfo) { String account = userInfo.get("account"); String password = userInfo.get("password"); - InMemoryUserRepository.findByAccount(account).ifPresent( - user -> { - boolean isValidPassword = user.checkPassword(password); - if (isValidPassword) { - log.info("user : {}", user); - } - - if (!isValidPassword) { - log.info(user.getAccount() + "의 비밀번호가 잘못 입력되었습니다."); - } - } - ); + Optional loginUser = InMemoryUserRepository.findByAccount(account); + + if (loginUser.isEmpty()) { + log.info(account + "는(은) 등록되지 않은 계정입니다."); + return; + } + + loginUser.filter(user -> user.checkPassword(password)) + .ifPresentOrElse( + user -> log.info("user : {}", user), + () -> log.info(account + "의 비밀번호가 잘못 입력되었습니다.") + ); } }