스레드에 대한 자세한 설명은 스레드란 무엇인가?를 보면 될 거 같다.
스레드(Thread)란?
간단하게 말해서 스레드는 일꾼이다. 하나의 프로세스 내에서 작업을 처리하는 단위로, 컴퓨터의 CPU는 스레드 단위로 작업을 처리한다. CPU는 매우 짧은 시간 동안 여러 작업을 번갈아 가면서 처리하기 때문에, 사용자 입장에서는 동시에 여러 작업이 실행되는 것처럼 보이지만 사실은 여러 스레드가 번갈아가면서 작업을 수행하는 것이다.
스레드가 많으면 좋을까?
스레드가 많다고 해서 무조건 좋은 것은 아니다. 스레드를 많이 생성하면 다음과 같은 문제들이 발생할 수 있다.
- 메모리 소모: 스레드가 생성될 때마다 메모리를 사용하기 때문에, 스레드가 많아지면 메모리 부족 문제가 발생할 수 있다.
- Race Condition: 여러 스레드가 동시에 동일한 자원을 사용하려고 하면 경합이 일어날 수 있는데, 이를 레이스 컨디션(Race Condition)이라고 한다.
- 컨텍스트 스위칭 비용 증가: CPU가 스레드 간에 전환할 때마다 컨텍스트 스위칭(Context Switching) 비용이 발생한다. 스레드가 많아지면 전환 비용이 커져 성능 저하를 초래할 수 있다.
그러나, 너무 적은 스레드를 사용하면 CPU 자원을 효율적으로 사용하지 못해 성능이 떨어질 수 있다. 따라서 적절한 스레드 수를 설정하는 것이 중요하다.
스레드 사용의 문제점
스레드는 각 요청마다 할당되는데, 스레드를 생성하고 소멸하는 과정은 운영체제(OS)와 자바 가상 머신(JVM)에 큰 부담을 줄 수 있다. 동시에 많은 요청이 들어오면 서버가 과부하되거나 다운될 위험이 있다.
이러한 문제를 해결하기 위해 Tomcat은 스레드 풀(Thread Pool)을 사용한다.
스레드 풀(Thread Pool)
스레드 풀은 미리 일정 수의 스레드를 생성해 두고, 요청이 들어오면 그 스레드를 재사용하는 방식이다. 이를 통해 스레드를 반복해서 생성하고 소멸하는 비용을 줄이고, 효율적으로 자원을 사용할 수 있다.
Tomcat의 경우, maxThreads, minSpareThreads와 같은 설정을 통해 스레드 풀의 크기와 스레드 관리 방식을 조절할 수 있다.
Thread Pool의 흐름
스레드를 미리 만들어 놓고 필요한 작업에 할당했다가 다시 돌려받은 후 재사용하는 일종의 보관소이다.
- 스레드 풀에 작업 요청
- 작업 요청이 작업 큐에 쌓임
- 작업 큐에 있는 작업들을 스레드 풀에 있는 스레드가 하나씩 맡아서 수행한다. 만약 처리할 스레드가 없다면 작업 큐에서 대기한다.
- 상태가 지속되어 작업 큐가 꽉 찬다면, 스레드를 새로 생성한다.
- 스레드 최대 사이즈에 도달하고, 작업큐도 꽉 차게 되면 추가 요청에 대해선 거절을 한다.
Thread Pool 사용
server:
tomcat:
accept-count: 1
max-connections: 2
threads:
max: 2
port: 8081
- maxThread : Tomcat이 동시에 처리할 수 있는 Thread 수 (기본 200)
- maxConnections : 동시에 연결할 수 있는 커넥션 수 (기본 8192)
- accept-count : 연결된 Thread를 초과했을 때 대기할 수 있는 대기방 크기 (기본 100)
간단한 예시 코드를 구현해 보았다.
package com.example.demo.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.client.RestTemplate;
@Controller
public class TestController {
private Logger log = LoggerFactory.getLogger(TestController.class);
private final RestTemplate restTemplate;
@Autowired
public TestController(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@GetMapping("/")
@ResponseBody
public ResponseEntity<Void> 스레드_5개_발사() {
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(() -> {
log.info("스레드 출격!");
String result = restTemplate.getForObject("<http://localhost:8081/hello>", String.class);
log.info(result);
});
thread.start();
}
return ResponseEntity.ok().build();
}
}
package com.example.demo.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
private Logger log = LoggerFactory.getLogger(HelloController.class);
@GetMapping("/hello")
public ResponseEntity<Void> hello() throws InterruptedException {
log.info("start");
Thread.sleep(3000);
log.info("end");
return ResponseEntity.ok().build();
}
}
3초 대기하는 hello 메서드를 만들었다.
즉, 5개의 요청이 들어오면 2개는 스레드가 작업하고 1명은 대기방에서 쉰다. 앞서 말한 스레드 풀의 흐름을 보면 나머지 4, 5 번은 거절되어야 한다고 예상된다.
하지만!
왜 인지 모르겠지만 1에서 5번째 스레드 때 오류가 발생한다. 이에 따라 5에서 10 스레드로 예를 들겠다. 해결되면 수정하려고 한다… ㅠㅠ
결과를 보면 5번의 요청이 한 번에 보내졌고, 2개의 요청이 처리되고 3초를 쉬며 2개가 더 가고 3초 뒤에 마지막 스레드가 출격하였다. 하지만 앞서 말한 작업 큐는 1칸 이므로 9, 10번째의 요청은 받을 수 없었을 텐데…?
위 상황은 BIO (Blocking I/O) 일 때 일어나는 상황이다.
NIO (Non-blocking I/O)와 BIO (Blocking I/O)의 차이점이 중요한 역할을 한다. Tomcat 8.0부터 NIO가 기본으로 채택되었으므로, 이 방식이 요청 처리에 영향을 미친다.
BIO vs NIO
- BIO (Blocking I/O)
- 스레드와 연결: 각 연결에 대해 별도의 스레드를 생성한다.. 스레드는 요청을 처리하는 동안 블로킹 상태로 대기한다.
- 작업 큐: 스레드가 요청을 처리 중일 때, 대기 중인 요청은 작업 큐에 쌓인다. 큐의 크기와 스레드의 수에 따라 요청 처리 방식이 결정된다.
- NIO (Non-blocking I/O)
- 스레드와 연결: NIO는 하나의 스레드가 여러 연결을 비동기적으로 처리할 수 있다. 스레드는 I/O 작업을 비동기적으로 처리하고, 요청이 처리될 준비가 되면 콜백을 통해 결과를 처리한다.
- 작업 큐: NIO는 커넥션을 유지하면서 스레드 풀과 작업 큐를 활용한다. 스레드는 블로킹 없이 연결을 유지하며, 요청이 처리될 때까지 대기한다.
결론
NIO는 최대 커넥션 수(maxConnections)까지 요청을 유지하며, 스레드가 부족할 경우 스레드를 동적으로 추가할 수 있다. 따라서, maxThreads가 2로 설정되어 있어도 NIO는 추가 스레드를 동적으로 생성하여 동시에 처리할 수 있어 사진과 같은 결과가 나온 것이다.
'WEB' 카테고리의 다른 글
소프트 딜리트란? (1) | 2024.09.27 |
---|---|
Spring Data JPA로 된 코드를 JDBC로 다시 짜보기 (1) | 2024.09.25 |
MDC를 이용한 로깅 도입기 (0) | 2024.09.23 |
@SpringBootTest vs @Mock (0) | 2024.09.12 |
트랜잭션이란? (0) | 2024.09.05 |
JUnit을 사용해서 Java 단위 테스트 하기 (0) | 2024.09.02 |
RDB와 NoSQL의 차이점 (3) | 2024.09.01 |
영속성 컨텍스트란? (1) | 2024.08.31 |