Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion study/src/main/resources/application.yml
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
Expand Down
25 changes: 10 additions & 15 deletions study/src/test/java/thread/stage0/SynchronizationTest.java
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
Comment on lines +19 to +21
Copy link
Member

Choose a reason for hiding this comment

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

학습 테스트에서 주어진 키워드는 아니지만 다음 키워드들의 차이점도 알고 가면 좋을 것 같아요. 😸

synchronized vs volatile vs AtomicInteger(Atomic 변수)

Copy link
Member Author

Choose a reason for hiding this comment

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

와... Atomic 변수 그동안 동시성 문제를 편하게 해결해 준다고만 생각하고 원리에 대해 공부를 못하였는데 기존 synchornizedvolatile 키워드의 원자성 및 성능 저하 문제를 해결해주는 신기한 친구였군요!

CPU 캐시 메모리와 RAM 메인 메모리의 데이터 정합성 문제를 CAS(Compare And Swap) 으로 해결하였네요!
좋은 학습 이었어요! 감사합니다!!

*/
@Test
void testSynchronized() throws InterruptedException {
Expand All @@ -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
Copy link
Member

Choose a reason for hiding this comment

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

메서드에 동기화를 적용하셨군요. 저도예요. ㅎ ㅎ
이제 calculate() 메서드에는 한 번에 하나의 스레드만 접근할 수 있게 되었습니다.

그런데 만약에 이 메서드에 지금 보다 훨씬 더 많은 스레드가 접근하려고 시도한다면,
왠지 성능 저하가 발생할 것 같아요. 성능 저하를 최소화할 방법이 없을까요?

힌트: 동기화하는 범위를 줄여볼 수 있지 않을까?

Copy link
Member Author

Choose a reason for hiding this comment

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

calculate 메서드에서 범위를 더 줄인다면 setSum 메서드를 걸어볼 수 있을 것 같은데 이렇게 되면 getSum 메서드를 동시에 읽는 문제가 발생하여 결국 데이터 정합성에 어긋날 것 같아요!

LMSthread PDF 14페이지의 이미지를 가져와보았습니다!
image

Copy link
Member Author

Choose a reason for hiding this comment

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

라고 생각했지만 아래 코멘트를 통해 volatile 키워드를 알게 되었고, Main Memory 에서의 데이터를 읽어 데이터 정합성을 지킬 수 있는 것을 알게 되었습니다! 😮

Copy link
Member

Choose a reason for hiding this comment

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

음.. 저도 스레드 및 동기화를 완벽히 마스터하지는 못해서 새양의 코멘트를 보고 지금 긴가민가한 상태인데요!

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

이렇게 동기화 블록을 사용하면, 한 스레드가 calculate()를 호출하는 동안 다른 스레드가 setSum()이나 getSum()을 호출하여 sum 값을 수정할 수 있어, 스레드 간섭 문제가 발생하고 데이터의 정합성이 깨진다는 것이죠? 🤔
헷갈리네요... 알려주셔서 감사합니다 새양. ㅎ ㅎ 덕분에 또 배워갑니다. 😸


Expand Down
28 changes: 12 additions & 16 deletions study/src/test/java/thread/stage0/ThreadPoolsTest.java
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 {

Expand All @@ -31,8 +27,8 @@ void testNewFixedThreadPool() {
executor.submit(logWithSleep("hello fixed thread pools"));
Copy link
Member

Choose a reason for hiding this comment

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

스레드 풀에 작업 처리를 요청하는 방법에는 학습 테스트에서 제시된 submit()외에도 한 가지 방법이 더 있습니다.
이는 무엇이고, 두 방법의 차이점, 그리고 왜 submit()이 더 권장되는지 알고 계신가요? 😸

Copy link
Member Author

@geoje geoje Sep 16, 2024

Choose a reason for hiding this comment

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

https://www.baeldung.com/java-execute-vs-submit-executor-service
여기저기 찾아봐도 제가 알던 느낌과 같아서 일단 해당 게시글을 들고 오게 되었어요!

둘 다 인자로 Runnable 을 받아 스레드에게 Task 를 전달해주는 것은 맞지만 execute 의 리턴 값은 void 인 반면 submit 의 리턴 값은 Future 입니다. 이 반환 값을 통해 작업이 끝났을 때 어떠한 값을 받아 추가 작업을 할 때 유용할 것입니다!

submit 에 2번째 인자로 defaultValue 를 줄 수도 있고 장점은 많지만 리턴 값이 없는 상황에서 굳이 사용해야하나? 라는 느낌을 받긴 했습니다.

모르는 사람이 이 메서드 사용을 볼 때 작업이 끝나고 어떠한 데이터를 받아 추가 작업을 할 것 으로 예상되는데 아무런 행동을 하지 않으면 오히려 이상할 것 같았어요!

Baeldung 의 마지막 부분에 아래 글이 적혀있더라구요!

If we need to obtain the result of a task or handle exceptions, we should use submit(). On the other hand, if we have a task that doesn’t return a result and we want to fire it and forget it, execute() is the right choice.

저도 이 의견에 동의 하는게 어떠한 Task 가 실행하고 그냥 잊어버리면 될 경우 execute 를 사용하는 것이 좋지 않을까요...??

submit 에 권장되는 이유가 궁금합니다!! 😃

Copy link
Member

Choose a reason for hiding this comment

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

오 열심히 학습하셨네요. 새양의 말이 맞습니다.
작업 처리 결과가 필요없다면 execute()를 사용하면 되겠죠!

이미 학습하셔서 알겠지만, execute()는 작업 처리 도중 예외가 발생하면 스레드가 종료되고, 해당 스레드가 스레드 풀에서 제거된다고 합니다. 반면, submit()은 작업 처리 도중 예외가 발생하더라도 스레드가 종료되지 않고 재사용된다고 하네요!
이러한 점에서 가급적이면 "스레드의 생성 오버헤드를 줄이기 위해 submit()을 사용하는 게 좋다"라고 말씀드리고 싶었는데, 괜히 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());
Expand All @@ -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());
Expand Down
31 changes: 13 additions & 18 deletions study/src/test/java/thread/stage0/ThreadTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,51 +5,45 @@
import org.slf4j.LoggerFactory;

/**
* 자바로 동시에 여러 작업을 처리할 때 스레드를 사용한다.
* 스레드 객체를 직접 생성하는 방법부터 알아보자.
* 진행하면서 막히는 부분은 아래 링크를 참고해서 해결한다.
*
* Thread Objects
* https://docs.oracle.com/javase/tutorial/essential/concurrency/threads.html
*
* Defining and Starting a Thread
* https://docs.oracle.com/javase/tutorial/essential/concurrency/runthread.html
* 자바로 동시에 여러 작업을 처리할 때 스레드를 사용한다. 스레드 객체를 직접 생성하는 방법부터 알아보자. 진행하면서 막히는 부분은 아래 링크를 참고해서 해결한다.
* <p>
* Thread Objects https://docs.oracle.com/javase/tutorial/essential/concurrency/threads.html
* <p>
* Defining and Starting a Thread https://docs.oracle.com/javase/tutorial/essential/concurrency/runthread.html
*/
class ThreadTest {

private static final Logger log = LoggerFactory.getLogger(ThreadTest.class);

/**
* 자바에서 직접 스레드를 만드는 방법은 2가지가 있다.
* 먼저 Thread 클래스를 상속해서 스레드로 만드는 방법을 살펴보자.
* 주석을 참고하여 테스트 코드를 작성하고, 테스트를 실행시켜서 메시지가 잘 나오는지 확인한다.
* 자바에서 직접 스레드를 만드는 방법은 2가지가 있다. 먼저 Thread 클래스를 상속해서 스레드로 만드는 방법을 살펴보자. 주석을 참고하여 테스트 코드를 작성하고, 테스트를 실행시켜서 메시지가 잘
* 나오는지 확인한다.
*/
@Test
void testExtendedThread() throws InterruptedException {
// 하단의 ExtendedThread 클래스를 Thread 클래스로 상속하고 스레드 객체를 생성한다.
Thread thread = new ExtendedThread("hello thread");

// 생성한 thread 객체를 시작한다.
thread.start();
thread.start();

// thread의 작업이 완료될 때까지 기다린다.
thread.join();
thread.join();
}

/**
* Runnable 인터페이스를 사용하는 방법도 있다.
* 주석을 참고하여 테스트 코드를 작성하고, 테스트를 실행시켜서 메시지가 잘 나오는지 확인한다.
* Runnable 인터페이스를 사용하는 방법도 있다. 주석을 참고하여 테스트 코드를 작성하고, 테스트를 실행시켜서 메시지가 잘 나오는지 확인한다.
*/
@Test
void testRunnableThread() throws InterruptedException {
// 하단의 RunnableThread 클래스를 Runnable 인터페이스의 구현체로 만들고 Thread 클래스를 활용하여 스레드 객체를 생성한다.
Thread thread = new Thread(new RunnableThread("hello thread"));

// 생성한 thread 객체를 시작한다.
thread.start();
thread.start();

// thread의 작업이 완료될 때까지 기다린다.
thread.join();
thread.join();
}

private static final class ExtendedThread extends Thread {
Expand Down Expand Up @@ -80,3 +74,4 @@ public void run() {
}
}
}

7 changes: 6 additions & 1 deletion study/src/test/java/thread/stage1/UserServlet.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

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

테스트에서 2개의 작업을 동시에 수행할 때 UserServlet.join() 메서드 첫 줄인 if (!users.contains(user)) 에서 유저 추가 전 검증을 하게 되는데 새로운 유저가 추가되기 전 2개의 스레드 모두가 이 if 문을 통과한다면 유저를 2번 추가하는 동시성 문제 Thread Interference 가 발생하게 됩니다.
따라서 public void 사이에 public synchronized void 를 하게되면 한 스레드가 이 메서드에 접근할 때 이미 다른 스레드가 동작 중이면 끝날 때 까지 대기하게 됩니다.

문제 원인에 대해 잘 설명해 주셨습니다.👍
그런데 synchronized 키워드를 추가하지 않아 테스트가 실패합니다.😅

+) 일부러 컨텍스트 스위칭 코드를 추가하신 이유가 궁금해요 ~ 😄

Copy link
Member Author

Choose a reason for hiding this comment

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

앗! 디버거를 사용하지 않고도 빠르게 현상을 확인할 수 있는 방법을 추가하기 위해 넣어두었습니다!

synchoronized 키워드를 추가하여 테스트를 통과하도록 만들어두었습니다!

Expand Down
31 changes: 24 additions & 7 deletions tomcat/src/main/java/org/apache/catalina/connector/Connector.java
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
Copy link
Member

Choose a reason for hiding this comment

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

새양이 기본 값을 이렇게 설정하신 근거가 궁금합니다. ◠‿◠
혹시 톰캣의 기본 설정 값도 알고 계신가요~?

Copy link
Member Author

@geoje geoje Sep 16, 2024

Choose a reason for hiding this comment

The 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
https://docs.spring.io/spring-boot/appendix/application-properties/index.html#application-properties.server.server.tomcat.threads.max
https://docs.spring.io/spring-boot/appendix/application-properties/index.html#application-properties.server.server.tomcat.threads.max-queue-capacity
https://docs.spring.io/spring-boot/appendix/application-properties/index.html#application-properties.server.server.tomcat.threads.min-spare

위 4개의 링크가 tomcat 의 주요 properties 기본 값 입니다.
설정한 근거의 경우 LMS 요구사항에 따른 값도 있으며 MIN_SPARE 의 경우 훨씬 경력이 많은 tomcat 을 따라하였습니다!

현재 설정 상황에서는 당연히 근거가 되는 데이터가 없어 근거를 들 수 없었습니다.
아마 프로젝트에서 해당 값을 튜닝할 시기가 온다면 기존에 수집해오던 매트릭 정보를 기반으로 동시접속 피크 시점과 최소 동시접속 수 그리고 CachedThreadPool 의 스레드 생성 및 제거도 비용이기 때문에 한 요청당 얼마나의 시간이 걸렸는지도 체크한 후 이러한 데이터를 기반으로 분석하여 정할 것 같습니다!


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
Copy link
Member

@cutehumanS2 cutehumanS2 Sep 14, 2024

Choose a reason for hiding this comment

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

Executors.newFixedThreadPool()이 아닌 ThreadPoolExecutor의 생성자를 사용하신 이유가 궁금합니다. 😄

Copy link
Member Author

Choose a reason for hiding this comment

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

불필요하게 많은 스레드를 미리 생성해둘 필요가 없었다 생각이 되었고 최소 10개를 유지한 채 필요할 경우 250개까지 늘린다라고 생각하였습니다.
주요 이유로는 놀고 있는 스레드로 인한 메모리 낭비 방지입니다!

}

private ServerSocket createServerSocket(final int port, final int acceptCount) {
Expand Down Expand Up @@ -57,7 +73,7 @@ public void run() {
private void connect() {
try {
process(serverSocket.accept());
} catch (IOException e) {
} catch (IOException | RejectedExecutionException e) {
Copy link
Member

@cutehumanS2 cutehumanS2 Sep 14, 2024

Choose a reason for hiding this comment

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

RejectedExecutionException은 어떤 상황에서 발생하나요?!

Copy link
Member Author

Choose a reason for hiding this comment

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

ExecutorService 에서 더 이상 Task 를 처리할 수 없을 때 발생합니다!

스레드 250 개가 전부 사용되고 대기 큐에 100 개가 모두 찬 상황일 때 새로운 Task 가 요청되면 발생합니다!

image

log.error(e.getMessage(), e);
}
}
Expand All @@ -67,13 +83,14 @@ private void process(final Socket connection) {
return;
}
var processor = new Http11Processor(connection);
new Thread(processor).start();
executorService.execute(processor);
Copy link
Member

Choose a reason for hiding this comment

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

submit()과 execute() 중 후자를 선택하신 이유가 궁금해요. 😸

Copy link
Member Author

Choose a reason for hiding this comment

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

ConnectorProcessor 의 작업들을 스레드에 잘 분배해 처리만 해주지 어떠한 결과 값을 받아 후처리를 할 필요가 없다고 판단하였기 때문입니다!

}

public void stop() {
Copy link
Member

Choose a reason for hiding this comment

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

스레드 풀은 종료하지 않아도 될까요? 🤔

Copy link
Member Author

Choose a reason for hiding this comment

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

해당 부분에 executorService.shutdown(); 를 추가하였습니다!!
생각해보니 과거 스레드가 남아있어 프로세스가 종료되지 않아 직접 작업관리자 or 커맨드로 제거해줬던 경험이 있는데 스레드풀도 마찬가지 겠군요...!

여기서 shutdownNow 메서드와 close 메서드가 보여 비교해본 결과!
전자는 대기 중인 작업을 취소하고, 실행 중인 작업을 가능한 한 중단하려 하는 것이고, try-with-resources 와 같은 구문 내에서 사용되며 내부적으론 shutdown 메서드 처럼 작동하는 것을 기대한다 합니다.

stopped = true;
try {
serverSocket.close();
executorService.shutdown();
} catch (IOException e) {
log.error(e.getMessage(), e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<>();
Copy link
Member

Choose a reason for hiding this comment

The 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/

Copy link
Member Author

Choose a reason for hiding this comment

The 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";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public Http11Processor(final Socket connection) {

@Override
public void run() {
// log.info("connect host: {}, port: {}", connection.getInetAddress(), connection.getPort());
log.info("connect host: {}, port: {}", connection.getInetAddress(), connection.getPort());
process(connection);
}

Expand Down
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 {
Copy link
Member

Choose a reason for hiding this comment

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

헉 테스트까지 작성하셨네요. 역시 고수시다. . .

Copy link
Member Author

Choose a reason for hiding this comment

The 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);
}
}
29 changes: 29 additions & 0 deletions tomcat/src/test/java/support/TestHttpUtils.java
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);
}
}
}