스레드를 직접 사용할 때의 문제점
1. 스레드 생성 시간으로 인한 성능 문제
2. 스레드 관리 문제
3. Runnable 인터페이스의 불편함
스레드 생성 시간으로 인한 성능 문제
스레드를 생성할 때는 비용이 상당히 많이 든다. 어떤 작업을 하나 수행할 때마다 스레드를 생성해서 실행하면 작업의 시간보다 스레드 생성 시간이 더 오래 걸리는 상황이 발생할 수 있다.
스레드 관리 문제
스레드는 CPU와 메모리를 사용하기 때문에 무한하게 만들 수 없다.CPU와 메모리가 가 감당가능한 선에서 스레드를 만들어야한다.
Runnable 인터페이스의 불편함
Runnable 인터페이스에는 크게 2가지 불편함이 있다.
첫번째는 Runnable 인터페이스의 run() 메소드는 반환값을 가지지 않기 때문에 다른 메소드에서 해당 스레드가 종료되길 기다렸다가 종료된 후에 멤버변수에서 값을 빼내와야 한다.
두번째는 run()는 체크예외를 던질 수 없고 무조건 메소드 내부에서 처리해야한다.
스레드 풀
컬렉션에 스레드를 보관하고 필요할때마다 가져다가 쓰는 것. 이 방법은 커넥션풀과도 굉장히 유사하기 때문에 커넥션 풀을 알고 있는 사람은 바로 이해할 수 있다.
Executor
자바의 Excutor 프레임워크는 멀티스레드를 쉽게 사용할 수 있도록 기능을 모아둔 것이다.
package java.util.concurrent;
public interface Executor {
void execute(Runnable command);
}
Excutor은 excute(Runnable command) 메소드 만을 가진다.
ExecutorService 인터페이스
Excutor 인터페이스를 확장해서 여러가지 기능을 추가로 제공한다. 실제로는 이 인터페이스를 주로 사용한다.
ThreadPoolExcutor
ExcutorService의 구현체이다. ThreadPoolExcutor은 크게 2가지로 구성되어 있다.
1. 스레드 풀 : 스레드 관리
2. BLockingQueue: 작업 보관. 생산자 소비자 문제를 해결 가능.
ExecutorService es = new ThreadPoolExecutor(2,2,0,TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>());
es.excute(new Runnable());
ex.excute()를 호출하면 내부에서 작업을 BlockingQueue에 보관한다.
ThreadPoolExecutor 생성자
- corePoolSize : 스레드 풀에서 관리되는 기본 스레드의 수
- maximumPoolSize: 스레드 풀에서 관리되는 최대 스레드의 수
- keepAliveTime, TimeUnit unit: 기본 스레드 수를 초과해서 만들어진 스레드가 생존할 수 있는 대기 시간. 이 시간 이후에 작업이 없으면 초과 스레드는 제거
- BlockingQueue workQueue: 작업을 보관할 블로킹 큐
Callable
Runnable 인터페이스의 단점을 해결할 수 있는 인터페이스
제네릭 V 타입으로 반환이 가능하고 Exception을 던질 수 있다.
package java.util.concurrent;
public interface Callable<V> {
V call() throws Exception;
}
Future
- 비동기 작업의 결과를 나중에 받을 수 있게 해주는 객체.
- 스레드에서 작업을 비동기적으로 하고 future.get()을 통해 나중에 값을 꺼내올 수 있음.
- Future의 구현체인 FutureTask는 Runnable 인터페이스도 함꼐 구현하고 있다.
- FutureTask의 run()메소드가 수행되면 run()메소드가 XXCallable.call()을 수행한다.
ExecutorService의 주요 메소드
void Execute(Runnable command): Runnable
작업을 제출한다. 반환값 없음
<T> Future<T> submit(Callable<T> task): Callable
작업을 제출하고 결과를 반환받는다
Future<?> submit(Runnable task): Runnable
작업을 제출하고 결과를 반환받는다
invokeAll()
모든 작업을 제출하고, 모든 작업이 완료될 때까지 기다린다.
timeout과 unit 파라미터를 추가하면 지정된 시간 내에 모든 Callable 작업을 제출하고 완료될 때까지 기다린다.
지정된 시간 내에 끝나지 않으면 나머지 작업은 취소된다.
invokeAny()
하나의 Callable 작업이 완료될 때까지 기다리고, 가장 먼저 완료된 작업의 결과를 반환한다.완료되지 않은 나머지 작업은 취소한다.
timeout과 unit 파라미터를 추가하면 지정된 시간 내에 하나의 Callable 작업이 완료될 때까지 기다리고, 가장 먼저 완료된 작업의 결과를 반환한다. 완료되지 않은 나머지 작업은 취소한다.
void shutdown()
새로운 작업을 받지 않고 이미 제출된 작업을 모두 완료한 후에 종료
List<Runnable> shutdownNow()
실행중인 작업을 중단하고, 대기중인 작업을 반환하며 즉시 종료
실행중인 작업을 중단하기 위해 인터럽트 발생
close()
자바 19부터 지원하는 서비스 종료 메소드. shutdown()을 호출하고 하루뒤에도 작업이 완료되지 않거나 인터럽트가 발생하면 shutdownNow() 호출.
스레드 풀 전략
자바는 Executors 클래스를 통해 3가지 스레드풀 전략을 제공한다.
- newSingleThreadPool(): 단일 스레드 풀 전략
- newFixedThreadPool(nThreads): 고정 스레드 풀 전략
- newCachedThreadPool(): 캐시 스레드 풀 전략
단일 스레드 풀 전략 : newSingleThreadPool()
- 스레드 풀에 기본 스레드 1개만 사용한다.
- 큐 사이즈에 제한이 없다
- 테스트 용도 or 간단하게 사용
고정 스레드 풀 전략 : newFixedThreadPool(nThread)
- 스레드 풀에 nThread 만큼의 기본 스레드 생성
- 초과 스레드는 생성하지 않음
- 큐 사이즈에 재한 없음
- 스레드 수가 고정되어 있기 때문에 CPU, 메모리 리소스를 예측 가능하다 -> 안정적
- 단점: 갑작스런 요청 증가에 대응할 수 없다.
캐시 풀 전략 : newCachedThreaPool()
- 기본 스레드를 사용하지 않고 60초 생존 주기를 가진 초과 스레드만 사용한다
- 초과 스레드의 수는 제한 없음
- 큐에 작업을 저장하지 않는다
- 생산자의 요청을 스레드풀의 소비자 스레드가 직접 받아서 처리한다
- 모든 요청이 대기하지 않고 스레드가 바로바로 처리한다.
- 매우 빠르고 유연한 전략
- 단점: 사용자가 계속 늘어날때 스레드 제한이 없기 때문에 스레드가 계속 늘어나다가 시스템이 죽을 수 있음
- 단점: 갑작스런 사용자 증가에도 서버가 터져버림 -> 안정성이 없음
SynchronousQueue
- BlockingQueue의 구현체 중 하나
- 내부 저장 공간이 없음. 대신에 생산자의 작업을 소비자 스레드에 직접 전달
- 생산자와 소비자를 동기화하는 큐
사용자 정의 풀 전략
위의 각 전략들의 단점을 해결하는 방법은 일반적인 상황에서는 고정크기의 스레드로 서비스를 안정적으로 운영하다가 사용자의 요청이 갑자기 증가하면 스레드를 추가해서 작업을 처리하는 것이다. 하지만 사용자의 요청이 계속 쌓이면 스레드가 무한히 증가하기 때문에 일정 숫자 이상의 사용자 요청은 거절하는 방법이다. 사용자의 요청을 거절하는 것이 서버가 다운되는것 보다 낫다.
𝐐. 그럼 톰캣은 어떤 스레드풀을 사용할까
톰캣은 내부적으로 FixedThreadPool을 사용하고 작업큐로는 TaskQueue를 사용한다.
TaskQueue는 BlockingQueue를 확장해서 약간 커스터마이징한 큐임
𝐐. 왜 톰캣은 BlockingQueue를 그냥 쓰지 않고 TaskQueue를 만들어서 쓰는가 (차이점이 무엇인가& 무엇을 커스터마이징 한건가)
TaskQueue는 큐가 비어있어도 스레드가 부족하면 스레드를 더 만든다.
=> 스레드가 꽉 차있고 새로운 요청이 들어오면 큐에 넣는 것 보다 스레드를 만드는것을 우선함.
당연히 최대 스레드 수 안에서 생성한다.
𝐐. 톰캣의 스레드풀을 설정하는 방법
spring:
task:
execution:
pool:
core-size: 5 # 항상 유지할 스레드 수
max-size: 20 # 최대 스레드 수
keep-alive: 60s # 추가 스레드의 유휴 대기 시간
queue-capacity: 100 # 대기 큐 용량
thread-name-prefix: async- # 스레드 이름 접두사
yml파일 기준이다.
'Study > Spring' 카테고리의 다른 글
스프링 AOP (1) | 2025.05.14 |
---|---|
스프링 의존성 주입 (1) | 2025.05.09 |
서블릿 컨테이너의 이해 (0) | 2025.04.04 |
스프링 시큐리티의 구조와 대체 방안 (3) | 2025.01.02 |
스프링 예외 처리 방식(체크 예외 vs 언체크 예외) (3) | 2024.12.30 |