-
Notifications
You must be signed in to change notification settings - Fork 388
[4단계 - Tomcat 구현하기] 새양(양경호) 미션 제출합니다. #692
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
Changes from all commits
367301a
7843fba
89e2bbd
a7fca22
941dd9c
90b0715
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
server: | ||
tomcat: | ||
accept-count: 1 | ||
max-connections: 1 | ||
max-connections: 3 | ||
threads: | ||
min-spare: 2 | ||
max: 2 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,29 +1,24 @@ | ||
package thread.stage0; | ||
|
||
import org.junit.jupiter.api.Test; | ||
import static org.assertj.core.api.Assertions.assertThat; | ||
|
||
import java.util.concurrent.Executors; | ||
import java.util.concurrent.TimeUnit; | ||
import java.util.stream.IntStream; | ||
|
||
import static org.assertj.core.api.Assertions.assertThat; | ||
import org.junit.jupiter.api.Test; | ||
|
||
/** | ||
* 다중 스레드 환경에서 두 개 이상의 스레드가 변경 가능한(mutable) 공유 데이터를 동시에 업데이트하면 경쟁 조건(race condition)이 발생한다. | ||
* 자바는 공유 데이터에 대한 스레드 접근을 동기화(synchronization)하여 경쟁 조건을 방지한다. | ||
* 동기화된 블록은 하나의 스레드만 접근하여 실행할 수 있다. | ||
* | ||
* Synchronization | ||
* https://docs.oracle.com/javase/tutorial/essential/concurrency/sync.html | ||
* 다중 스레드 환경에서 두 개 이상의 스레드가 변경 가능한(mutable) 공유 데이터를 동시에 업데이트하면 경쟁 조건(race condition)이 발생한다. 자바는 공유 데이터에 대한 스레드 접근을 | ||
* 동기화(synchronization)하여 경쟁 조건을 방지한다. 동기화된 블록은 하나의 스레드만 접근하여 실행할 수 있다. | ||
* <p> | ||
* Synchronization https://docs.oracle.com/javase/tutorial/essential/concurrency/sync.html | ||
*/ | ||
class SynchronizationTest { | ||
|
||
/** | ||
* 테스트가 성공하도록 SynchronizedMethods 클래스에 동기화를 적용해보자. | ||
* synchronized 키워드에 대하여 찾아보고 적용하면 된다. | ||
* | ||
* Guide to the Synchronized Keyword in Java | ||
* https://www.baeldung.com/java-synchronized | ||
* 테스트가 성공하도록 SynchronizedMethods 클래스에 동기화를 적용해보자. synchronized 키워드에 대하여 찾아보고 적용하면 된다. | ||
* <p> | ||
* Guide to the Synchronized Keyword in Java https://www.baeldung.com/java-synchronized | ||
*/ | ||
@Test | ||
void testSynchronized() throws InterruptedException { | ||
|
@@ -41,7 +36,7 @@ private static final class SynchronizedMethods { | |
|
||
private int sum = 0; | ||
|
||
public void calculate() { | ||
public synchronized void calculate() { | ||
setSum(getSum() + 1); | ||
} | ||
Comment on lines
+39
to
41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 메서드에 동기화를 적용하셨군요. 저도예요. ㅎ ㅎ 그런데 만약에 이 메서드에 지금 보다 훨씬 더 많은 스레드가 접근하려고 시도한다면, 힌트: 동기화하는 범위를 줄여볼 수 있지 않을까? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 라고 생각했지만 아래 코멘트를 통해 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 음.. 저도 스레드 및 동기화를 완벽히 마스터하지는 못해서 새양의 코멘트를 보고 지금 긴가민가한 상태인데요! public void calculate() {
synchronized(this) {
setSum(getSum() + 1);
}
} 이렇게 동기화 블록을 사용하면, 한 스레드가 calculate()를 호출하는 동안 다른 스레드가 setSum()이나 getSum()을 호출하여 sum 값을 수정할 수 있어, 스레드 간섭 문제가 발생하고 데이터의 정합성이 깨진다는 것이죠? 🤔 |
||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,23 +1,19 @@ | ||
package thread.stage0; | ||
|
||
import org.junit.jupiter.api.Test; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
import static org.assertj.core.api.Assertions.assertThat; | ||
|
||
import java.util.concurrent.Executors; | ||
import java.util.concurrent.ThreadPoolExecutor; | ||
|
||
import static org.assertj.core.api.Assertions.assertThat; | ||
import org.junit.jupiter.api.Test; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
/** | ||
* 스레드 풀은 무엇이고 어떻게 동작할까? | ||
* 테스트를 통과시키고 왜 해당 결과가 나왔는지 생각해보자. | ||
* | ||
* Thread Pools | ||
* https://docs.oracle.com/javase/tutorial/essential/concurrency/pools.html | ||
* | ||
* Introduction to Thread Pools in Java | ||
* https://www.baeldung.com/thread-pool-java-and-guava | ||
* 스레드 풀은 무엇이고 어떻게 동작할까? 테스트를 통과시키고 왜 해당 결과가 나왔는지 생각해보자. | ||
* <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 | ||
*/ | ||
class ThreadPoolsTest { | ||
|
||
|
@@ -31,8 +27,8 @@ void testNewFixedThreadPool() { | |
executor.submit(logWithSleep("hello fixed thread pools")); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 스레드 풀에 작업 처리를 요청하는 방법에는 학습 테스트에서 제시된 submit()외에도 한 가지 방법이 더 있습니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. https://www.baeldung.com/java-execute-vs-submit-executor-service 둘 다 인자로
모르는 사람이 이 메서드 사용을 볼 때 작업이 끝나고 어떠한 데이터를 받아 추가 작업을 할 것 으로 예상되는데 아무런 행동을 하지 않으면 오히려 이상할 것 같았어요! Baeldung 의 마지막 부분에 아래 글이 적혀있더라구요!
저도 이 의견에 동의 하는게 어떠한
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 오 열심히 학습하셨네요. 새양의 말이 맞습니다. 이미 학습하셔서 알겠지만, execute()는 작업 처리 도중 예외가 발생하면 스레드가 종료되고, 해당 스레드가 스레드 풀에서 제거된다고 합니다. 반면, submit()은 작업 처리 도중 예외가 발생하더라도 스레드가 종료되지 않고 재사용된다고 하네요! |
||
|
||
// 올바른 값으로 바꿔서 테스트를 통과시키자. | ||
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()); | ||
|
@@ -46,7 +42,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()); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,12 +7,17 @@ public class UserServlet { | |
|
||
private final List<User> users = new ArrayList<>(); | ||
|
||
public void service(final User user) { | ||
public synchronized void service(final User user) { | ||
join(user); | ||
} | ||
|
||
private void join(final User user) { | ||
if (!users.contains(user)) { | ||
try { | ||
Thread.sleep(1); // Expected context switching to another thread | ||
} catch (InterruptedException e) { | ||
throw new RuntimeException(e); | ||
} | ||
users.add(user); | ||
} | ||
} | ||
Comment on lines
14
to
23
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
문제 원인에 대해 잘 설명해 주셨습니다.👍 +) 일부러 컨텍스트 스위칭 코드를 추가하신 이유가 궁금해요 ~ 😄 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 앗! 디버거를 사용하지 않고도 빠르게 현상을 확인할 수 있는 방법을 추가하기 위해 넣어두었습니다!
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,31 +1,47 @@ | ||
package org.apache.catalina.connector; | ||
|
||
import org.apache.coyote.http11.Http11Processor; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
import java.io.IOException; | ||
import java.io.UncheckedIOException; | ||
import java.net.ServerSocket; | ||
import java.net.Socket; | ||
import java.util.concurrent.ArrayBlockingQueue; | ||
import java.util.concurrent.ExecutorService; | ||
import java.util.concurrent.RejectedExecutionException; | ||
import java.util.concurrent.ThreadPoolExecutor; | ||
import java.util.concurrent.TimeUnit; | ||
import org.apache.coyote.http11.Http11Processor; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
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 DEFAULT_ACCEPT_COUNT = 500; | ||
|
||
private static final int DEFAULT_MIN_SPARE_COUNT = 10; | ||
private static final int DEFAULT_MAX_THREAD_COUNT = 250; | ||
private static final int DEFAULT_MAX_QUEUE_COUNT = 100; | ||
Comment on lines
+21
to
+25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 새양이 기본 값을 이렇게 설정하신 근거가 궁금합니다. ◠‿◠ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. https://docs.spring.io/spring-boot/appendix/application-properties/index.html#application-properties.server.server.tomcat.accept-count 위 4개의 링크가 현재 설정 상황에서는 당연히 근거가 되는 데이터가 없어 근거를 들 수 없었습니다. |
||
|
||
private final ServerSocket serverSocket; | ||
private final ExecutorService executorService; | ||
private boolean stopped; | ||
|
||
public Connector() { | ||
this(DEFAULT_PORT, DEFAULT_ACCEPT_COUNT); | ||
} | ||
|
||
public Connector(final int port, final int acceptCount) { | ||
this(port, acceptCount, DEFAULT_MIN_SPARE_COUNT, DEFAULT_MAX_THREAD_COUNT); | ||
} | ||
|
||
public Connector(final int port, final int acceptCount, final int minSpareCount, final int maxThreadCount) { | ||
this.serverSocket = createServerSocket(port, acceptCount); | ||
this.stopped = false; | ||
this.executorService = new ThreadPoolExecutor(minSpareCount, maxThreadCount, | ||
60L, TimeUnit.SECONDS, | ||
new ArrayBlockingQueue<>(DEFAULT_MAX_QUEUE_COUNT)); | ||
Comment on lines
+42
to
+44
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Executors.newFixedThreadPool()이 아닌 ThreadPoolExecutor의 생성자를 사용하신 이유가 궁금합니다. 😄 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 불필요하게 많은 스레드를 미리 생성해둘 필요가 없었다 생각이 되었고 최소 10개를 유지한 채 필요할 경우 250개까지 늘린다라고 생각하였습니다. |
||
} | ||
|
||
private ServerSocket createServerSocket(final int port, final int acceptCount) { | ||
|
@@ -57,7 +73,7 @@ public void run() { | |
private void connect() { | ||
try { | ||
process(serverSocket.accept()); | ||
} catch (IOException e) { | ||
} catch (IOException | RejectedExecutionException e) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. RejectedExecutionException은 어떤 상황에서 발생하나요?! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
log.error(e.getMessage(), e); | ||
} | ||
} | ||
|
@@ -67,13 +83,14 @@ private void process(final Socket connection) { | |
return; | ||
} | ||
var processor = new Http11Processor(connection); | ||
new Thread(processor).start(); | ||
executorService.execute(processor); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. submit()과 execute() 중 후자를 선택하신 이유가 궁금해요. 😸 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
} | ||
|
||
public void stop() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 스레드 풀은 종료하지 않아도 될까요? 🤔 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 해당 부분에 여기서 |
||
stopped = true; | ||
try { | ||
serverSocket.close(); | ||
executorService.shutdown(); | ||
} catch (IOException e) { | ||
log.error(e.getMessage(), e); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,15 +2,15 @@ | |
|
||
import com.techcourse.model.User; | ||
import jakarta.servlet.http.HttpSession; | ||
import java.util.HashMap; | ||
import java.util.Map; | ||
import java.util.UUID; | ||
import java.util.concurrent.ConcurrentHashMap; | ||
import org.apache.catalina.Manager; | ||
import org.apache.coyote.http11.request.HttpRequest; | ||
|
||
public class SessionManager implements Manager { | ||
|
||
private static final Map<String, HttpSession> SESSIONS = new HashMap<>(); | ||
private static final Map<String, HttpSession> SESSIONS = new ConcurrentHashMap<>(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ConcurrentHashMap과 HashTable의 차이가 뭘까요? ㅎ ㅎ 참고) https://tecoble.techcourse.co.kr/post/2021-11-26-hashmap-hashtable-concurrenthashmap/ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. HashMap 과 다르게 둘 다 멀티 스레드 환경에서 동시성을 보장해주지만 스레드간 동기화 락을 거는 HashMap 은 다소 느릴 수 있다는 단점이 존재했습니다. ConcurrentHashMap 은 조작 하려는 특정 Entry 에 대해서만 락을 걸기 때문에 멀티 스레드에서의 성능을 향상 시킨다고 하네요! 간단 명료한 정리 글도 읽기 정말 좋군요! 감사합니다~ |
||
private static final SessionManager SESSION_MANAGER = new SessionManager(); | ||
private static final String ATTRIBUTE_USER_NAME = "user"; | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
package org.apache.catalina.connector; | ||
|
||
import static org.assertj.core.api.Assertions.assertThat; | ||
|
||
import java.net.http.HttpResponse; | ||
import java.util.ArrayList; | ||
import java.util.List; | ||
import java.util.concurrent.ExecutorService; | ||
import java.util.concurrent.Executors; | ||
import java.util.concurrent.Future; | ||
import java.util.concurrent.TimeUnit; | ||
import org.junit.jupiter.api.DisplayName; | ||
import org.junit.jupiter.api.Test; | ||
import support.TestHttpUtils; | ||
|
||
class ConnectorTest { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 헉 테스트까지 작성하셨네요. 역시 고수시다. . . There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 동시성 테스트 첫 시도라 아직 많이 어렵네요... 잘모르겠어요 😥 |
||
|
||
@Test | ||
@DisplayName("켜져있는 서버 메인 페이지를 600개 요청후 성공이 스레드 개수 250과 큐 100개 합친 350인지 확인한다.") | ||
void request600() throws InterruptedException { | ||
// given | ||
int threadCount = 600; | ||
int expectedCount = 350; | ||
ExecutorService pool = Executors.newFixedThreadPool(threadCount); | ||
List<Future<Integer>> futures = new ArrayList<>(); | ||
|
||
// when | ||
for (int i = 0; i < threadCount; i++) { | ||
futures.add(pool.submit(() -> { | ||
HttpResponse<String> response = TestHttpUtils.send("/index.html"); | ||
return response.statusCode(); | ||
})); | ||
} | ||
|
||
pool.shutdown(); | ||
pool.awaitTermination(5, TimeUnit.MINUTES); | ||
|
||
long successCount = futures.stream() | ||
.filter(future -> { | ||
try { | ||
return future.get() == 200; | ||
} catch (Exception e) { | ||
return false; | ||
} | ||
}) | ||
.count(); | ||
|
||
// then | ||
assertThat(successCount).isEqualTo(expectedCount); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
package support; | ||
|
||
import java.io.IOException; | ||
import java.net.URI; | ||
import java.net.http.HttpClient; | ||
import java.net.http.HttpRequest; | ||
import java.net.http.HttpResponse; | ||
import java.time.Duration; | ||
|
||
public class TestHttpUtils { | ||
|
||
private static final HttpClient httpClient = HttpClient.newBuilder() | ||
.version(HttpClient.Version.HTTP_1_1) | ||
.connectTimeout(Duration.ofSeconds(1)) | ||
.build(); | ||
|
||
public static HttpResponse<String> send(final String path) { | ||
final var request = HttpRequest.newBuilder() | ||
.uri(URI.create("http://localhost:8080" + path)) | ||
.timeout(Duration.ofSeconds(1)) | ||
.build(); | ||
|
||
try { | ||
return httpClient.send(request, HttpResponse.BodyHandlers.ofString()); | ||
} catch (IOException | InterruptedException e) { | ||
throw new RuntimeException(e); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
학습 테스트에서 주어진 키워드는 아니지만 다음 키워드들의 차이점도 알고 가면 좋을 것 같아요. 😸
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
와...
Atomic
변수 그동안 동시성 문제를 편하게 해결해 준다고만 생각하고 원리에 대해 공부를 못하였는데 기존synchornized
와volatile
키워드의 원자성 및 성능 저하 문제를 해결해주는 신기한 친구였군요!CPU 캐시 메모리와 RAM 메인 메모리의 데이터 정합성 문제를 CAS(Compare And Swap) 으로 해결하였네요!
좋은 학습 이었어요! 감사합니다!!