ComputerScience

스레드 풀(Thread Pool)

slown 2024. 9. 22. 19:27

목차

1.스레드 풀?

2.스레드 풀이 필요한 이유 

3.스레드 풀이 작동되는 과정

4.JAVA/Spring에 적용

 

1.스레드 풀?

스레드풀은 미리 생성된 스레드 집합을 유지하고, 요청된 작업이 있을 때 이 스레드를 재사용하여 처리하는 방식을 말합니다. 

 

※스레드 :  스레드는 프로세스 내에서 실행되는 실행 단위를 말합니다.

※프로세스 :  프로세스는 실행중인 프로그램을 말합니다.

2.스레드 풀이 필요한 이유

우선은 스레드풀이 왜 필요한지를 알기 위해서는 스레드풀이 없는 경우를 한번 이야기를 해보겠습니다. 

 

스레드풀이 없는 경우 

  • 무분별한 리소스 소모: 스레드를 무분별하게 생성을 하고 작업을 수행을 하게되면 많은 리소스를 소모합니다. 또한 스레드 생성은 시간이 걸리며, 과도한 스레드 생성은 시스템 성능을 저하를 일으킵니다.
  • 시스템 성능저하: 첫번째 이유와 맞물려서 CPU의 메모리와 자원이 고갈이 되고 이로 인해서 시스템이 느려질 가능성이 높습니다.

이러한 무분별한 스레드의 생성보다는 스레드를 재사용을 하기 위해서 스레드 풀을 사용을 합니다. 그럼 스레드풀을 사용하면 어떤 이점이 있는지를 말해보겠습니다.

 

스레드풀이 있는 경우

  • 성능 향상: 스레드풀을 사용하면 새로운 스레드를 매번 생성하는 대신 미리 생성된 스레드를 재사용하여 응답 속도를 개선할 수 있습니다. 이는 작업 처리를 빠르게 하여 전체 시스템의 성능을 높입니다.
  • 리소스 관리: 스레드 수를 제한하여 CPU와 메모리 자원의 사용을 최적화합니다. 무한정 스레드를 생성하는 것을 방지하고, 시스템 자원을 효율적으로 관리할 수 있습니다.
  • 비동기 처리: I/O 작업이나 네트워크 작업과 같은 시간이 소요되는 작업을 비동기적으로 처리할 수 있어, 애플리케이션의 응답성을 향상시킵니다.

이렇게 스레드 풀을 사용을 하면 코스트가 높은 스레드를 무분별한 사용을 막고 재사용을 하면서 효과적으로 성능을 최적화를 할 수 있습니다. 

 

그렇다면 '스레드풀을 많이 가져오고 쓴다면 괜찮지 않을까?' 라는 생각을 가질수도 있지만 스레드풀에 스레드를 많이 생성을 하게 된다면 다음과 같은 단점이 있습니다.

  • 성능 저하
    • 스레드 생성 오버헤드: 새로운 스레드를 생성하는 데는 시간이 소요됩니다. 스레드가 너무 많이 생성되면 CPU가 스레드를 생성하고 관리하는 데 시간을 소모하게 되어, 실제 작업 수행에 할애할 수 있는 시간이 줄어듭니다.
  • 자원 고갈
    • CPU 및 메모리 사용량 증가: 각 스레드는 CPU와 메모리 자원을 소비합니다. 너무 많은 스레드가 생성되면 시스템의 CPU와 메모리가 고갈될 수 있습니다. 이로 인해 시스템이 느려지거나, 아예 응답하지 않을 수 있습니다.
    • 스레드 스위칭 오버헤드: 많은 스레드가 동시에 실행되면 컨텍스트 스위칭이 자주 발생합니다. 이 과정에서 CPU는 스레드 간의 상태를 전환하는 데 많은 시간을 소비하게 되며, 이는 전체 성능을 저하시킵니다.
  • 관리 복잡성 증가
    • 동기화 문제: 많은 스레드가 동시에 데이터에 접근하려고 할 경우, 동기화 문제가 발생할 수 있습니다. 데이터의 무결성을 유지하기 위해 추가적인 동기화 메커니즘이 필요하게 되어 코드가 복잡해질 수 있습니다.
  • 응답시간 저하
    • 대기 시간 증가: 스레드가 너무 많아지면 시스템은 각 스레드의 작업을 처리하는 데 시간이 더 걸리게 됩니다. 이로 인해 애플리케이션의 응답성이 저하될 수 있습니다.

컨텍스트 스위칭 : 운영 체제가 실행 중인 프로세스나 스레드의 상태 정보를 저장하고, 다른 프로세스나 스레드의 상태 정보를 로드하는 과정

3.스레드 풀이 작동되는 과정 

그럼 스레드 풀이 어떻게 작동하는지는 다음과 같습니다. 

 

순서는 사진과 같고 설명을 하자면 다음과 같습니다.

 

1. 어플리케이션에서 스레드풀의 작업요청을 합니다.

2. 요청을 받았으면 작업큐를 통해서 해당 작업을 넣습니다.

3. 작업큐에 넣어진 작업들은 순차적으로 미리 생성된 스레드에 할당이 되어서 작업을 실행을 합니다.

4. 스레드가 작업을 실행을 하게 되면 결과를 전달을 합니다.

 

대략적으로 스레드 풀이 이렇게 작동이 되었는지를 알았다면 JAVA와 Spring에는 어떻게 적용이 되는지를 확인을 해보겠습니다.

4.JAVA/Spring에 적용

그럼 Java/Spring에서는 어떻게 스레드풀을 설정을 하는지를 알아보겠습니다. 

@EnableAsync
@Configuration
public class AsyncConfig extends AsyncConfigurerSupport {
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5); // 기본적으로 실행 대기 중인 Thread 개수
        executor.setMaxPoolSize(10); // 동시에 동작하는 최대 Thread 개수
        executor.setQueueCapacity(500); // CorePool이 초과될때 Queue에 저장했다가 꺼내서 실행된다. (500개까지 저장함)
        executor.setThreadNamePrefix("async-"); // Spring에서 생성하는 Thread 이름의 접두사
        executor.initialize();
        return executor;
    }
}

 

위의 설정에 대해서 설명을 하자면 다음과 같습니다.

  • @EnableAsync
    • 이 어노테이션은 스프링에서 비동기 메서드 호출을 활성화하기 위해 사용됩니다.
    • @Async 애너테이션이 적용된 메서드를 비동기로 실행하게 됩니다. 
  •  AsyncConfigurerSupport
    • 비동기 메서드를 실행할 때 사용할 스레드 풀 설정을 제공할 수 있는 클래스
    • 상속받은 이 클래스를 활용을 해서 커스텀 스레드 풀을 설정할 수 있습니다. getAsyncExecutor() 메서드를 통해 스레드 풀을 반환하게 됩니다.
  • ThreadPoolTaskExecutor
    • 스레드 풀을 관리하는 클래스입니다.
    • 이 클래스는 비동기 작업이 발생할 때 여러 작업을 동시에 처리하기 위해 사용되며, 설정된 값들에 따라 스레드를 생성하고 작업을 큐에 저장

다음은 스레드풀의  설정값에 대한 설명입니다.

 

CorePoolSize(기본 스레드 수) : 기본적으로 실행 대기 중인 스레드의 개수입니다. 예를 들어, 설정된 5개의 스레드가 비동기 작업을 처리할 준비가 되어 있습니다.

 

MaxPoolSize(최대 스레드 수) : 동시에 동작할 수 있는 최대 스레드 개수를 지정합니다. 이 경우 10개로 설정되어 있어서 최대 10개의 작업이 동시에 실행될 수 있습니다.

 

QueueCapacity(큐 용량) : 기본 스레드(5개)가 모두 사용 중일 때 추가로 들어오는 작업은 큐에 저장됩니다. 이 큐는 500개의 작업까지 저장할 수 있으며, 최대 스레드 수(10개)도 초과하면 새로운 스레드를 생성할 수 없고 작업이 거부될 수 있습니다.


ThreadNamePrefix : 스레드 이름의 접두사입니다. 이 설정을 통해 생성된 스레드 이름 앞에 "async-"라는 접두사가 붙습니다. 예를 들어, 스레드 이름이 async-1, async-2와 같은 형식이 됩니다. 이를 통해 디버깅 시 스레드 풀에서 생성된 스레드를 구분할 수 있습니다.


executor.initialize() : 설정한 값을 기반으로 스레드 풀을 초기화합니다.

 

그럼 위의 설정코드를 사용하면 비동기 작업을 쉽게 처리할 수 있습니다. 한 예로는 제가 만들었던 프로젝트에서 이메일 전송을 위해서 외부api를 사용을 하는데 속도를 좀 더 비약적으로 빠르게 처리를 할 수가 있었습니다.