-
Notifications
You must be signed in to change notification settings - Fork 388
[4단계] 에버(손채영) 미션 제출합니다. #755
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
20344ab
8614dad
4f6bb60
8ac813b
d9853da
c3c2714
0fce7f5
417fad9
fa79e2c
789ed95
3c7cc1f
5d80027
bf5c2c6
4f644b9
407c475
dd1fa88
f66a521
5aebdbf
021b899
000b871
3a6ad60
697e51d
2516bc7
0e482c6
52f0b32
9690f33
ad8e324
8ebc973
8aed891
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,12 +1,15 @@ | ||
| package thread.stage0; | ||
|
|
||
| import org.junit.jupiter.api.Test; | ||
| import static org.assertj.core.api.Assertions.assertThat; | ||
|
|
||
| import java.util.concurrent.ExecutionException; | ||
| import java.util.concurrent.ExecutorService; | ||
| 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; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| /** | ||
| * 다중 스레드 환경에서 두 개 이상의 스레드가 변경 가능한(mutable) 공유 데이터를 동시에 업데이트하면 경쟁 조건(race condition)이 발생한다. | ||
|
|
@@ -18,6 +21,8 @@ | |
| */ | ||
| class SynchronizationTest { | ||
|
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. 이 테스트에서 사용된
Member
Author
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. 하나의 스레드가 calculate 로직을 수행하고 있으면 다른 스레드가 해당 로직을 수행하지 못하도록 합니다. 즉, 여러 스레드가 공유하고 있는 sum 필드에 동시에 접근하지 못하게 함으로써, 동시성에 의한 예상치 못한 에러를 방지합니다! |
||
|
|
||
| private static final Logger log = LoggerFactory.getLogger(SynchronizationTest.class); | ||
|
|
||
| /** | ||
| * 테스트가 성공하도록 SynchronizedMethods 클래스에 동기화를 적용해보자. | ||
| * synchronized 키워드에 대하여 찾아보고 적용하면 된다. | ||
|
|
@@ -27,12 +32,12 @@ class SynchronizationTest { | |
| */ | ||
| @Test | ||
| void testSynchronized() throws InterruptedException { | ||
| var executorService = Executors.newFixedThreadPool(3); | ||
| var synchronizedMethods = new SynchronizedMethods(); | ||
| ExecutorService executorService = Executors.newFixedThreadPool(3); | ||
| SynchronizedMethods synchronizedMethods = new SynchronizedMethods(); | ||
|
|
||
| IntStream.range(0, 1000) | ||
| .forEach(count -> executorService.submit(synchronizedMethods::calculate)); | ||
| executorService.awaitTermination(500, TimeUnit.MILLISECONDS); | ||
| .forEach(i -> executorService.submit(synchronizedMethods::calculate)); // 실행 (비동기로 요청 시도) | ||
| executorService.awaitTermination(500, TimeUnit.MILLISECONDS); // 정상적인 종료, 혹은 타임아웃 발생 여부 확인 후 대기 | ||
|
|
||
| assertThat(synchronizedMethods.getSum()).isEqualTo(1000); | ||
| } | ||
|
|
@@ -41,15 +46,17 @@ private static final class SynchronizedMethods { | |
|
|
||
| private int sum = 0; | ||
|
|
||
| public void calculate() { | ||
| public synchronized void calculate() { | ||
|
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.
Member
Author
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. 2개 이상의 스레드가 사용되는 환경, 즉 멀티스레드 환경에서, 여러 스레드가 동시에 공유 자원에 접근할 때 발생할 수 있는 문제를 방지하기 위해 사용되는 기술입니다. 하나의 스레드가 특정 로직을 수행하고 있을 때 다른 스레드의 요청이 들어올 경우, 해당 로직을 동시에 수행하는 것이 아닌, 하나의 스레드가 일을 마무리할 때까지 기다리도록 하는 방식입니다. 이를 통해 공유 자원의 일관성을 유지하고 race condition을 방지합니다. 스레드 동기화가 무엇인지 추상적으로만 알고 있었는데, 이렇게 정리할 수 있는 기회가 되니 좋네요 :) |
||
| log.info("before calculate: {}", sum); | ||
| setSum(getSum() + 1); | ||
| log.info("after calculate: {}", sum); | ||
| } | ||
|
|
||
| public int getSum() { | ||
| public /*synchronized*/ int getSum() { | ||
| return sum; | ||
| } | ||
|
|
||
| public void setSum(int sum) { | ||
| public /*synchronized*/ void setSum(int sum) { | ||
| this.sum = sum; | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,35 +25,46 @@ class ThreadPoolsTest { | |
|
|
||
| @Test | ||
| void testNewFixedThreadPool() { | ||
| final var executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(2); | ||
| ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(2); | ||
| // corePoolSize = 2 | ||
| // maximumPoolSize = 2 | ||
| // keepAliveTime = 0 | ||
|
|
||
| executor.submit(logWithSleep("hello fixed thread pools")); | ||
| executor.submit(logWithSleep("hello fixed thread pools")); | ||
| executor.submit(logWithSleep("hello fixed thread pools")); | ||
| // logWithoutSleep 진행 시 queue에 쌓이지 X => 3, 0 | ||
|
|
||
| // 올바른 값으로 바꿔서 테스트를 통과시키자. | ||
| final int expectedPoolSize = 0; | ||
| final int expectedQueueSize = 0; | ||
| final int expectedPoolSize = 2; | ||
| final int expectedQueueSize = 1; // holding tasks 저장 | ||
|
|
||
| assertThat(expectedPoolSize).isEqualTo(executor.getPoolSize()); | ||
| assertThat(expectedQueueSize).isEqualTo(executor.getQueue().size()); | ||
| assertThat(executor.getPoolSize()).isEqualTo(expectedPoolSize); | ||
| assertThat(executor.getQueue().size()).isEqualTo(expectedQueueSize); | ||
| } | ||
|
|
||
| @Test | ||
| void testNewCachedThreadPool() { | ||
| final var executor = (ThreadPoolExecutor) Executors.newCachedThreadPool(); | ||
| ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newCachedThreadPool(); | ||
| // corePoolSize = 0 | ||
| // maximumPoolSize = MAX_VALUE | ||
| // keepAliveTime = 60s | ||
|
|
||
| executor.submit(logWithSleep("hello cached thread pools")); | ||
| executor.submit(logWithSleep("hello cached thread pools")); | ||
| executor.submit(logWithSleep("hello cached thread pools")); | ||
| // 실험) maximumPoolSize 이상 횟수로 실행시키면? -> OutOfMemoryError | ||
|
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. 스레드 개수에 따라 여러 문제가 발생할 수 있기 때문에 적절한 스레드풀의 개수를 맞추는건 아주 중요한 작업이라고해요! 그럼...
같이 고민 해볼까요? 🤔🤔
Member
Author
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.
동시에 들어오는 요청의 수보다 스레드의 수가 한없이 적을 경우, 항상 모든 스레드가 점유되어 있기 때문에 스레드의 대기 시간이 길어질 수 있습니다. 또한 대기 중인 스레드가 큐를 모두 채울 경우 일부 요청은 거절될 수 있습니다.
반대로 동시에 들어오는 요청의 수보다 스레드의 수가 한없이 많다면, 쉬고 있는 스레드가 많아져 불필요하게 메모리를 차지하고 있을 수 있습니다.
큐에 쌓이는 요청의 개수를 보고 판단할 수 있을 것 같습니다. 큐에 너무 많은 요청이 쌓이는 경우 스레드 수를 늘려야 할 것이고, 큐에 요청이 전혀 들어오지 않는 경우는 쉬고 있는 스레드가 많다 판단하여 스레드의 수를 줄여야 할 것입니다.
각 HTTP 요청 별로 스레드가 생성되기 때문에 HTTP 요청이 들어오는 Connector 클래스에서 책정하면 될 것 같습니다! 모두 저의 개인적인 생각이므로, 혹시 켈리가 다르게 생각하고 계신다면 공유해주시면 감사하겠습니다 😁 |
||
|
|
||
| // 올바른 값으로 바꿔서 테스트를 통과시키자. | ||
| final int expectedPoolSize = 0; | ||
| final int expectedQueueSize = 0; | ||
| final long expectedPoolSize = 3; | ||
| final long expectedQueueSize = 0; | ||
|
|
||
| assertThat(expectedPoolSize).isEqualTo(executor.getPoolSize()); | ||
| assertThat(expectedQueueSize).isEqualTo(executor.getQueue().size()); | ||
| assertThat(executor.getPoolSize()).isEqualTo(expectedPoolSize); | ||
| assertThat(executor.getQueue().size()).isEqualTo(expectedQueueSize); | ||
| } | ||
|
|
||
| private Runnable logWithSleep(final String message) { | ||
| // 1초 후 로깅 | ||
| return () -> { | ||
| try { | ||
| Thread.sleep(1000); | ||
|
|
@@ -63,4 +74,10 @@ private Runnable logWithSleep(final String message) { | |
| log.info(message); | ||
| }; | ||
| } | ||
|
|
||
| private Runnable logWithoutSleep(final String message) { | ||
| return () -> { | ||
| log.info(message); | ||
| }; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -30,10 +30,14 @@ void testExtendedThread() throws InterruptedException { | |
| Thread thread = new ExtendedThread("hello thread"); | ||
|
|
||
| // 생성한 thread 객체를 시작한다. | ||
| thread.start(); | ||
| // log.info("before start"); | ||
| thread.start(); // ! run() 실행 ! | ||
| // log.info("after start"); | ||
|
|
||
| // thread의 작업이 완료될 때까지 기다린다. | ||
| thread.join(); | ||
| // log.info("before join"); | ||
| thread.join(); | ||
| // log.info("after join"); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -46,10 +50,16 @@ void testRunnableThread() throws InterruptedException { | |
| Thread thread = new Thread(new RunnableThread("hello thread")); | ||
|
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.
가볍게 찾아보면 재밌을거 같아요! (저 좀 알려주세요! 😎)
Member
Author
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. Runnable 인터페이스를 구현함으로써, 스레드가 실행될 때 수행할 역할을 정의하기 위함입니다! 아래는 Thread 클래스에 정의되어있는 run 메서드인데, Runnable 인터페이스를 구현한 클래스를 생성자 파라미터로 전달하는 경우 아래 메서드가 재정의됩니다. @Override
public void run() {
Runnable task = holder.task;
if (task != null) {
Object bindings = scopedValueBindings();
runWith(bindings, task);
}
} |
||
|
|
||
| // 생성한 thread 객체를 시작한다. | ||
| thread.start(); | ||
| // log.info("before start"); | ||
| thread.start(); | ||
| // log.info("after start"); | ||
|
|
||
| // ! run() 실행 ! | ||
|
|
||
| // thread의 작업이 완료될 때까지 기다린다. | ||
| thread.join(); | ||
| // log.info("before join"); | ||
| thread.join(); | ||
| // log.info("after join"); | ||
| } | ||
|
|
||
| private static final class ExtendedThread extends Thread { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -37,4 +37,5 @@ void test() throws InterruptedException { | |
| // 하지만 디버거로 개별 스레드를 일시 중지하면 if절 조건이 true가 되고 크기가 2가 된다. 왜 그럴까? | ||
|
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. 왜 그럴까요..!
Member
Author
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. 디버거로 두 개의 스레드를 일시 중지한 후 동시에 실행시키면 users 리스트가 비어있는 상태에서 동시에 users.contains문이 실행되고 두 스레드 모두에서 user는 리스트에 포함되어있지 않다고 출력됩니다. 따라서 두 스레드 모두에서 user 데이터 추가를 시도하게 되고 결과적으로 리스트 사이즈는 2가 됩니다. 이를 해결하기 위해 synchronized 키워드를 붙여 동기적으로 차례차례 실행되도록 하였습니다! |
||
| assertThat(userServlet.getUsers()).hasSize(1); | ||
| } | ||
| // 각 스레드 중단점 걸고 동시에 실행 -> synchronized 붙이면 동기로 처리하여 해결 (두번째 스레드는 아예 중단점 안 걸림) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| package org.apache.catalina.connector; | ||
|
|
||
| import static org.assertj.core.api.Assertions.assertThat; | ||
|
|
||
| import java.net.http.HttpResponse; | ||
| import java.util.concurrent.atomic.AtomicInteger; | ||
| import org.junit.jupiter.api.DisplayName; | ||
| import org.junit.jupiter.api.Test; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| class ConnectorTest { | ||
|
|
||
| private static final Logger log = LoggerFactory.getLogger(ConnectorTest.class); | ||
| private static final AtomicInteger count = new AtomicInteger(0); | ||
|
|
||
| @DisplayName("거의 동시에 300개의 요청이 들어오면?") | ||
|
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 | ||
| void test_concurrentRequest() { | ||
| Connector connector = new Connector(); | ||
| connector.start(); | ||
|
|
||
| for (int i = 0; i < 300; i++) { | ||
| incrementIfOk(TestHttpUtils.send("/")); | ||
| // log.info("pool size = {}", connector.getExecutorPoolSize()); | ||
| // log.info("queue size = {}", connector.getExecutorQueueSize()); | ||
| } | ||
|
|
||
| assertThat(connector.getExecutorPoolSize()).isEqualTo(250); | ||
| // assertThat(connector.getExecutorQueueSize()).isEqualTo(50); | ||
| connector.stop(); | ||
| } | ||
|
|
||
| private static void incrementIfOk(final HttpResponse<String> response) { | ||
| if (response.statusCode() == 200) { | ||
| count.incrementAndGet(); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| package org.apache.catalina.connector; | ||
|
|
||
| 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.
하루 10만건 정도의 요청이 들어오는 서비스라면 스레드 개수를 얼마나 잡아보면 좋을까요? 같이 고민해볼까요? 🤭
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.
좋은 고민 거리 주셔서 감사해요! 😃
알아보니 적정 스레드 수는
CPU 코어 수 × (1 + (대기 시간 / 처리 시간))로 계산하곤 하더라구요. CPU 코어의 수와 요청 처리 시간 및 대기 시간에 따라 설정해야 할 스레드 개수가 달라질 것 같아요! 또, 하루 10만건의 요청이 어느 분포로 들어오는지도 측정해본 후 개수를 설정해볼 것 같아요. 동시에 들어오는 요청의 수가 중요하다고 생각되어서요! 켈리는 어떻게 생각하시나요? 혼자서 나름 고민하다보니 켈리의 생각도 궁금해지네요 🤔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.
저도 에버의 의견에 동의합니다! 하루 10만건이 특정 시간대에 몰려서 오는건지, 혹은 하루동안 균일하게 요청이 들어오는건지에 따라 적정 스레드 풀 계수를 산정할거 같아요! 물론 저희가 취업전에 이정도 트래픽을 받아볼 일이 없..겠지만..! 가끔 이런 공상을 해보는것도 재밌더라구요 🤭