# 멀티 스레드

![](https://389280719-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LxjHkZu4T9MzJ5fEMNe%2Fsync%2Faab969961f0e0136668a7763fe9fb34d5e400dd6.png?generation=1622978522023523\&alt=media)

![](https://389280719-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LxjHkZu4T9MzJ5fEMNe%2Fsync%2F91943212f7a6c44a44a5419dd737ee1a57a0314e.png?generation=1622978528409745\&alt=media)

브라우저가 요청을 보내면 WAS가 응답을 한다. 요청을 했을 때 TCP 커넥션이 연결되면서 서블릿이 호출된다.

![](https://389280719-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LxjHkZu4T9MzJ5fEMNe%2Fsync%2Ff71527323ae981668c08127f5c8a0dfedc2302dc.png?generation=1622978528578720\&alt=media)

그런데 이 서블릿 객체는 누가 호출하는 걸까? 바로 스레드다.

## 스레드

* 애플리케이션 코드를 하나하나 순차적으로 실행한다.
* 자바 메인 메서드를 처음 실행하면 main이라는 이름의 스레드가 실행된다.
* 스레드가 없다면 자바 애플리케이션 실행이 불가능하다.
* 스레드는 한 번에 하나의 코드 라인만 수행한다.
* 동시 처리가 필요하면 스레드를 추가로 만들어야 한다.

## 단일 스레드

스레드가 하나만 있다고 가정해보자.

### 단일 요청

![](https://389280719-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LxjHkZu4T9MzJ5fEMNe%2Fsync%2Fa2b3469638435b2bb0f0797c792068d763830374.png?generation=1622978530065337\&alt=media)

![](https://389280719-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LxjHkZu4T9MzJ5fEMNe%2Fsync%2F81ed07d09cc59482dd1b52f057fd1d374a9e7280.png?generation=1622978523930633\&alt=media)

요청이 오면 스레드를 할당한다. 이 스레드를 가지고 서블릿을 실행한다.

![](https://389280719-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LxjHkZu4T9MzJ5fEMNe%2Fsync%2F942dcc6a5178860b6204c2d818faf9be5c8f9103.png?generation=1622978524351379\&alt=media)

다 실행하고 나면 응답을 보낸다.

### 다중 요청

![](https://389280719-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LxjHkZu4T9MzJ5fEMNe%2Fsync%2F0605d2bfb0c4901be0b1d750c8a03afd3d96d3f1.png?generation=1622978517983606\&alt=media)

![](https://389280719-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LxjHkZu4T9MzJ5fEMNe%2Fsync%2Ffe0e3f198e439f4c89f6c0318b2f23593b07eba0.png?generation=1622978521198337\&alt=media)

요청 1이 들어와서 스레드를 사용해 서블릿을 요청한다. 여러 가지 이유로 인해 처리에 시간이 많이 걸린다고 해보자.

![](https://389280719-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LxjHkZu4T9MzJ5fEMNe%2Fsync%2Fc4b8601a302971806a66846d95ab2956f2826dfa.png?generation=1622978529825302\&alt=media)

![](https://389280719-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LxjHkZu4T9MzJ5fEMNe%2Fsync%2F29b9923be9c23651bc3f0a738e001baf309c3ab4.png?generation=1622978519164984\&alt=media)

이때 요청 2가 들어오면 스레드를 사용할 수 있을 때까지 기다린다. 이렇게 되면 둘 다 죽게된다. 1번이 잡고 있으니 2번이 실행 자체를 못하게 되고 결국 타임 아웃이 발생하는 것이다.

## 다중 스레드

문제를 해결하려면 요청마다 스레드를 생성하면 된다.

### 요청마다 스레드 생성

![](https://389280719-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LxjHkZu4T9MzJ5fEMNe%2Fsync%2Ffc768c84e7ad7b7246e7b07fa9961cfa8508889e.png?generation=1622978517037976\&alt=media)

요청 1이 들어오고 처리가 지연되어도 요청 2가 오면 새로 스레드를 만든다. 같은 서블릿을 호출해도 스레드는 다르기 때문에 각 스레드대로 요청을 처리하고 응답을 보낸다. 응답이 끝나면 스레드를 날린다.

**장점**

* 동시 요청을 처리할 수 있다.
* 리소스(CPU, 메모리)가 허용할 때까지 처리할 수 있다.
* 하나의 스레드가 지연되어도 나머지 스레드는 정상 동작한다.

**단점**

* 스레드는 생성 비용이 매우 비싸다.
  * 고객 요청이 올 때마다 스레드를 생성하면 응답 속도가 늦어진다.
* 스레드는 컨텍스트 스위칭 비용이 발생한다.
  * 스레드는 코어 수만큼 동시에 돌아간다.
    * 2개라고 치면 사실 둘은 동시에 수행하는 게 아니라 번갈아가면서 수행한다.
    * 너무 빨라서 동시에 한다고 느껴지는 것 뿐이다.
  * 이 두 개를 전환할 때 비용이 발생하는데 이것을 컨텍스트 스위칭 비용이라고 한다.
  * 스레드가 많아지면 이 비용 자체가 점점 커지게 된다.
* 스레드 생성에 제한이 없다.
  * 고객의 요청이 너무 많이 오면 CPU와 메모리 임계점을 넘어서 서버가 죽을 수 있다.

### 스레드 풀

요청마다 생성하는 단점을 보완해 스레드 풀 안에 스레드를 미리 만들어놓고 쓰는 방법이다.

![](https://389280719-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LxjHkZu4T9MzJ5fEMNe%2Fsync%2F812bba94868273aa997595048ba4030c2dfb6392.png?generation=1622978523088400\&alt=media)

요청이 오면 스레드 풀에서 스레드를 가져다 쓴다. 200개니까 2개가 줄어 198개가 된다. 응답을 보내고 나면 스레드를 다시 반납해서 200개로 돌아간다.

![](https://389280719-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LxjHkZu4T9MzJ5fEMNe%2Fsync%2F4b55a39cb1b6801f3efc761cba27441431d7dde4.png?generation=1622978520028885\&alt=media)

만약 200개보다 더 많은 요청이 온다면 추가적인 요청들은 대기하고 있다가 200개 중에 응답을 보내고 반납한 스레드를 사용할 수 있다. 그래도 감당하기 힘들면 거절을 하기도 한다.

**특징**

* 필요한 스레드를 스레드 풀에 보관하고 관리한다.
* 스레드 풀에 생성 가능한 스레드의 최대치를 관리한다.
  * 톰캣은 최대 200개가 기본 설정이며 변경 가능하다.
* 스레드가 필요하면 이미 생성되어있는 스레드를 스레드 풀에서 꺼내 사용한다.
* 사용을 종료하면 스레드 풀에 해당 스레드를 반납한다.
* 최대 스레드가 모두 사용중이어서 스레드 풀에 스레드가 없다면?
  * 기다리는 요청은 거절하거나 특정 숫자만큼만 대기하도록 설정할 수 있다.

**장점**

* 스레드가 미리 생성되어 있어 스레드를 생성, 종료하는 비용(CPU)이 절약되고 응답 시간이 빠르다.
* 생성 가능한 스레드의 최대치가 있으므로 너무 많은 요청이 들어와도 기존 요청은 안전하게 처리할 수 있다.
  * 물론 너무 많은 요청이 들어오면 클라이언트에선 계속 연결이 안되겠지만 성공한 사람은 어찌저찌 내부에서 로직이 돌아간다.

### 실무 팁

* WAS의 주요 튜닝 포인트는 최대 스레드 수이다.
* 값을 너무 낮게 설정하면?
  * 동시 요청이 많을 때 서버 리소스는 여유롭지만 클라이언트는 금방 응답이 지연된다.
* 값을 너무 높게 설정하면?
  * 동시 요청이 많으면 CPU, 메모리 리소스 임계점 초과로 서버가 다운된다.
* 장애가 발생하면?
  * 클라우드라면 일단 서버부터 늘리고 이후에 튜닝한다.
  * 클라우드가 아니면 열심히 튜닝한다.

![](https://389280719-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LxjHkZu4T9MzJ5fEMNe%2Fsync%2Ffefd3795b31e74be6beacab34698844f4fdd42aa.png?generation=1622978523422676\&alt=media)

스레드 풀을 너무 낮게 설정했을 경우 가능한 스레드의 100개의 요청이 온다면 90개가 대기한다. 리소스를 확인하면 CPU를 5%로만 사용하고 있다고 나온다.

안에서 10개만 정상적으로 수행되고 요청은 계속 쌓인다. 내가 처리하는건 10개인데 요청은 더 많으니 밀리면서 장애가 발생한다. WAS는 살아있어도 서버 전체는 장애가 난다.

사실 이럴 경우 못해도 CPU를 50%는 써줘야 한다. 설정 하나만 잘해줘도 늘어나는데 잘못해서 5%밖에 못쓴다고 인스턴스를 늘리고 있으면 돈만 그만큼 나간다.

## 스레드 풀의 적정 숫자

* 애플리케이션 로직의 복잡도, CPU, 메모리, IO 리소스 상황에 따라 모두 다르다.
* 최대한 실제 서비스와 유사하게 성능 테스트를 시도하자.
  * 아파치 ab, 제이미터, nGrinder

## WAS의 멀티 스레드 지원

* 멀티 스레드에 대한 부분은 WAS가 처리한다.
* 개발자가 멀티 스레드 관련 코드를 신경쓰지 않아도 된다.
* 개발자는 마치 싱글 스레드 프로그래밍을 하듯 편리하게 소스 코드를 개발한다.
* 멀티 스레드 환경이므로 싱글턴 객체(서블릿, 스프링 빈)은 주의해서 사용한다.
