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단계] 후추(주찬민) 미션 제출합니다. #451

Merged
merged 27 commits into from
Sep 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
8bba381
[docs] 3단계 요구사항 정리
Combi153 Sep 8, 2023
54496f7
[refactor] http version 정하는 로직 수정
Combi153 Sep 9, 2023
0c056ba
[feat] AuthService 세션 ID로 로그인 여부 확인 기능 추가
Combi153 Sep 9, 2023
d3f70fd
[feat] HomeController 추가
Combi153 Sep 9, 2023
8899c22
[feat] LoginController 추가
Combi153 Sep 9, 2023
fca1ca3
[test] HomeController 테스트코드 변경
Combi153 Sep 9, 2023
6640674
[refactor] body 값 반환 로직 변경
Combi153 Sep 9, 2023
2d46acb
[test] Controller 테스트코드 이름 수정
Combi153 Sep 9, 2023
837e93d
[feat] RegisterController 기능 추가
Combi153 Sep 9, 2023
b3f185c
[feat] ResourceController 기능 추가
Combi153 Sep 9, 2023
aff9856
[feat] RequestMapping 기능 추가
Combi153 Sep 9, 2023
fe4c74c
[feat] RequestMapping 연결
Combi153 Sep 9, 2023
75c99ac
[학습 테스트] Thread 학습 테스트 진행
Combi153 Sep 10, 2023
7c65585
[feat] 동시성 대응 기능 적용
Combi153 Sep 10, 2023
43578d8
[test] 테스트 작성(실패하는 중)
Combi153 Sep 10, 2023
70b65df
[refactor] 사용하지 않는 클래스 삭제, 생성자 정리
Combi153 Sep 10, 2023
33334a5
[refactor] toMap default 설정 명시 삭제
Combi153 Sep 10, 2023
640fdfe
[refactor] 테스트 코드 메서드 변경 및 불필요한 메서드 삭제
Combi153 Sep 10, 2023
d7c64d9
[refactor] 패키지 구조 변경
Combi153 Sep 10, 2023
a3476af
[refactor] InMemory UserRepository 로직 변경
Combi153 Sep 10, 2023
85a24f0
[refactor] 회원 저장 로직 변경
Combi153 Sep 10, 2023
149b3ac
[refactor] Controller service 로직 변경
Combi153 Sep 11, 2023
faee486
[refactor] InMemoryUserRepository, SessionManager 싱글톤으로 변경
Combi153 Sep 11, 2023
5828d26
[feat] 예외 처리 로직 추가
Combi153 Sep 11, 2023
3fb8fe1
[refactor] 자원 읽기 로직 변경
Combi153 Sep 11, 2023
de691b5
[test] IndexController 테스트 추가
Combi153 Sep 11, 2023
c54f0c7
[style] 불필요한 메소드 제거
Combi153 Sep 11, 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
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,22 @@

- [x] 세션 클래스 구현
- [x] 연결

## 3단계

- [x] HttpRequest 클래스 구현
- [x] HttpResponse 클래스 구현
- [x] Controller 추가
- [x] Controller 인터페이스 추가
- [x] AbstractController 추가
- [x] LoginController 추가
- [x] RegisterController 추가
- [x] ResourceController 추가
- [x] HomeController 추가
- [x] RequestMapping 추가
- [x] if문 분기 삭제

## 4단계

- [x] ThreadPool 적용
- [x] 동시성 컬렉션 사용
5 changes: 2 additions & 3 deletions study/src/main/java/thread/stage2/SampleController.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package thread.stage2;

import java.util.concurrent.atomic.AtomicInteger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.concurrent.atomic.AtomicInteger;

@Controller
public class SampleController {

Expand All @@ -26,7 +25,7 @@ public SampleController(final HelloWorldService helloWorldService) {
@GetMapping("/test")
@ResponseBody
public String helloWorld() throws InterruptedException {
Thread.sleep(500);
Thread.sleep(500); // thread 0.5초 정지
log.info("http call count : {}", count.incrementAndGet());
return helloWorldService.helloWorld();
}
Expand Down
7 changes: 4 additions & 3 deletions study/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ handlebars:

server:
tomcat:
accept-count: 1
max-connections: 1
accept-count: 1 # 작업큐 사이즈
max-connections: 2 # 가능한 connection 총 개수
threads:
max: 2
max: 2 # 생성 가능 thread 총 개수
min-spare: 1 # 항상 활성화 되어있는 (idle) thread 개수

compression:
enabled: true
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
*/
@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);
}

Expand Down
69 changes: 51 additions & 18 deletions study/src/test/java/thread/stage0/ThreadPoolsTest.java
Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
package thread.stage0;

import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.assertj.core.api.SoftAssertions.assertSoftly;

import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

import static org.assertj.core.api.Assertions.assertThat;
import java.util.concurrent.TimeUnit;
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,11 +28,29 @@ void testNewFixedThreadPool() {
executor.submit(logWithSleep("hello fixed thread pools"));

// 올바른 값으로 바꿔서 테스트를 통과시키자.
final int expectedPoolSize = 0;
final int expectedPoolSize = 2;
final int expectedQueueSize = 1;

assertSoftly(softly -> {
softly.assertThat(executor.getCorePoolSize()).isEqualTo(expectedPoolSize);
softly.assertThat(executor.getMaximumPoolSize()).isEqualTo(expectedPoolSize);
softly.assertThat(executor.getKeepAliveTime(TimeUnit.SECONDS)).isEqualTo(0);
softly.assertThat(executor.getPoolSize()).isEqualTo(expectedPoolSize);
softly.assertThat(executor.getQueue().size()).isEqualTo(expectedQueueSize);
});
}

@Test
void testNewFixedThreadPool_initial() {
final var executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(2);

final int expectedCorePoolSize = 2;
final int expectedQueueSize = 0;

assertThat(expectedPoolSize).isEqualTo(executor.getPoolSize());
assertThat(expectedQueueSize).isEqualTo(executor.getQueue().size());
assertSoftly(softly -> {
softly.assertThat(executor.getCorePoolSize()).isEqualTo(expectedCorePoolSize);
softly.assertThat(executor.getQueue().size()).isEqualTo(expectedQueueSize);
});
}

@Test
Expand All @@ -46,11 +61,29 @@ void testNewCachedThreadPool() {
executor.submit(logWithSleep("hello cached thread pools"));

// 올바른 값으로 바꿔서 테스트를 통과시키자.
final int expectedPoolSize = 3;
final int expectedQueueSize = 0;

assertSoftly(softly -> {
softly.assertThat(executor.getMaximumPoolSize()).isEqualTo(Integer.MAX_VALUE);
softly.assertThat(executor.getCorePoolSize()).isEqualTo(0);
softly.assertThat(executor.getKeepAliveTime(TimeUnit.SECONDS)).isEqualTo(60);
softly.assertThat(executor.getPoolSize()).isEqualTo(expectedPoolSize);
softly.assertThat(executor.getQueue().size()).isEqualTo(expectedQueueSize);
});
}

@Test
void testNewCachedThreadPool_initial() {
final var executor = (ThreadPoolExecutor) Executors.newCachedThreadPool();

final int expectedPoolSize = 0;
final int expectedQueueSize = 0;

assertThat(expectedPoolSize).isEqualTo(executor.getPoolSize());
assertThat(expectedQueueSize).isEqualTo(executor.getQueue().size());
assertSoftly(softly -> {
softly.assertThat(executor.getPoolSize()).isEqualTo(expectedPoolSize);
softly.assertThat(executor.getQueue().size()).isEqualTo(expectedQueueSize);
});
}

private Runnable logWithSleep(final String message) {
Expand Down
18 changes: 7 additions & 11 deletions study/src/test/java/thread/stage1/ConcurrencyTest.java
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
package thread.stage1;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.Test;

/**
* 스레드를 다룰 때 어떤 상황을 조심해야 할까?
* - 상태를 가진 한 객체를 여러 스레드에서 동시에 접근할 경우
* - static 변수를 가진 객체를 여러 스레드에서 동시에 접근할 경우
*
* 위 경우는 동기화(synchronization)를 적용시키거나 객체가 상태를 갖지 않도록 한다.
* 객체를 불변 객체로 만드는 방법도 있다.
*
* 웹서버는 여러 사용자가 동시에 접속을 시도하기 때문에 동시성 이슈가 생길 수 있다.
* 어떤 사례가 있는지 아래 테스트 코드를 통해 알아보자.
* 스레드를 다룰 때 어떤 상황을 조심해야 할까? - 상태를 가진 한 객체를 여러 스레드에서 동시에 접근할 경우 - static 변수를 가진 객체를 여러 스레드에서 동시에 접근할 경우
* <p>
* 위 경우는 동기화(synchronization)를 적용시키거나 객체가 상태를 갖지 않도록 한다. 객체를 불변 객체로 만드는 방법도 있다.
* <p>
* 웹서버는 여러 사용자가 동시에 접속을 시도하기 때문에 동시성 이슈가 생길 수 있다. 어떤 사례가 있는지 아래 테스트 코드를 통해 알아보자.
*/
class ConcurrencyTest {

Expand Down
18 changes: 6 additions & 12 deletions study/src/test/java/thread/stage2/AppTest.java
Original file line number Diff line number Diff line change
@@ -1,25 +1,19 @@
package thread.stage2;

import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;

import java.net.http.HttpResponse;
import java.util.concurrent.atomic.AtomicInteger;

import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;

class AppTest {

private static final AtomicInteger count = new AtomicInteger(0);

/**
* 1. App 클래스의 애플리케이션을 실행시켜 서버를 띄운다.
* 2. 아래 테스트를 실행시킨다.
* 3. AppTest가 아닌 App의 콘솔에서 SampleController가 생성한 http call count 로그를 확인한다.
* 4. application.yml에서 설정값을 변경해보면서 어떤 차이점이 있는지 분석해본다.
* - 로그가 찍힌 시간
* - 스레드명(nio-8080-exec-x)으로 생성된 스레드 갯수를 파악
* - http call count
* - 테스트 결과값
* 1. App 클래스의 애플리케이션을 실행시켜 서버를 띄운다. 2. 아래 테스트를 실행시킨다. 3. AppTest가 아닌 App의 콘솔에서 SampleController가 생성한 http call
* count 로그를 확인한다. 4. application.yml에서 설정값을 변경해보면서 어떤 차이점이 있는지 분석해본다. - 로그가 찍힌 시간 - 스레드명(nio-8080-exec-x)으로 생성된 스레드
* 갯수를 파악 - http call count - 테스트 결과값
*/
@Test
void test() throws Exception {
Expand All @@ -32,7 +26,7 @@ void test() throws Exception {

for (final var thread : threads) {
thread.start();
Thread.sleep(50);
Thread.sleep(50); // thread 0.05초 정지
}

for (final var thread : threads) {
Expand Down
2 changes: 1 addition & 1 deletion study/src/test/java/thread/stage2/TestHttpUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public class TestHttpUtils {

private static final HttpClient httpClient = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_1_1)
.connectTimeout(Duration.ofSeconds(1))
.connectTimeout(Duration.ofSeconds(1)) // connection 1초 지나도 못 맺으면 HttpConnectTimeoutException
.build();

public static HttpResponse<String> send(final String path) {
Expand Down
31 changes: 31 additions & 0 deletions tomcat/src/main/java/common/ResponseStatus.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package common;

import static java.lang.Integer.MIN_VALUE;

public enum ResponseStatus {
OK(200, "OK"),
FOUND(302, "Found"),
BAD_REQUEST(400, "Bad Request"),
UNAUTHORIZED(401, "Unauthorized"),
NOT_FOUND(404, "Not Found"),
INTERNAL_SERVER_ERROR(500, "Internal Server Error"),

EMPTY_RESPONSE_STATUS(MIN_VALUE, "Empty Response Status"),
;

private final int code;
private final String response;

ResponseStatus(int code, String response) {
this.code = code;
this.response = response;
}

public String codeMessage() {
return String.valueOf(code);
}

public String responseMessage() {
return response;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package nextstep.jwp.controller;

import static org.apache.coyote.request.line.HttpMethod.DELETE;
import static org.apache.coyote.request.line.HttpMethod.GET;
import static org.apache.coyote.request.line.HttpMethod.PATCH;
import static org.apache.coyote.request.line.HttpMethod.POST;
import static org.apache.coyote.request.line.HttpMethod.PUT;

import nextstep.jwp.exception.NotFoundException;
import org.apache.coyote.request.HttpRequest;
import org.apache.coyote.response.HttpResponse;

public abstract class AbstractController implements Controller {

@Override
public boolean canProcess(HttpRequest httpRequest) {
return true;
}

@Override
public void service(HttpRequest httpRequest, HttpResponse httpResponse) {
if (httpRequest.consistsOf(POST)) {
doPost(httpRequest, httpResponse);
return;
}
if (httpRequest.consistsOf(GET)) {
doGet(httpRequest, httpResponse);
return;
}
if (httpRequest.consistsOf(DELETE)) {
doDelete(httpRequest, httpResponse);
return;
}
if (httpRequest.consistsOf(PUT)) {
doDelete(httpRequest, httpResponse);
return;
}
if (httpRequest.consistsOf(PATCH)) {
doDelete(httpRequest, httpResponse);
return;
}
throw new NotFoundException();
}

protected void doPost(HttpRequest httpRequest, HttpResponse httpResponse) {
throw new NotFoundException();
}

protected void doGet(HttpRequest httpRequest, HttpResponse httpResponse) {
throw new NotFoundException();
}

protected void doDelete(HttpRequest httpRequest, HttpResponse httpResponse) {
throw new NotFoundException();
}

protected void doPut(HttpRequest httpRequest, HttpResponse httpResponse) {
throw new NotFoundException();
}

protected void doPatch(HttpRequest httpRequest, HttpResponse httpResponse) {
throw new NotFoundException();
}
}
10 changes: 10 additions & 0 deletions tomcat/src/main/java/nextstep/jwp/controller/Controller.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package nextstep.jwp.controller;

import org.apache.coyote.request.HttpRequest;
import org.apache.coyote.response.HttpResponse;

public interface Controller {
boolean canProcess(HttpRequest httpRequest);

void service(HttpRequest httpRequest, HttpResponse httpResponse);
}
Loading