스레드풀
자바에서는 멀티 스레드를 사용해서 여러개의 작업을 병렬로 처리할 수 있다. 만약 매우 많은 작업이 한 번에 들어온다면, 그에 맞게 스레드 를 생성하고, 메모리 사용량이 늘어나 어플리케이션 성능에 영향을 줄 수 있다.
만약 미리 스레드를 여러개 만들어 놓고, 작업이 들어올때 스레드에게 하나씩 할당해준다면 스레드 생성, 메모리 비용을 줄일 수 있을것이다.
자바에서는 스레드풀을 사용할 수 있도록 ExecutorService를 제공해준다.
ExecutorService
ExecutorService는 직접 생성하거나, Executors 클래스의 팩토리 메서드를 사용해서 생성할 수 있다.
ExecutorService executor = Executors.newFixedThreadPool(3);
CachedThreadPool
: 필요한 수 만큼 동적으로 스레드 수를 늘린다. 사용하지 않는 스레드는 풀에서 제거한다.FixedThreadPool
: 주어진 개수대로 N개의 코어 스레드를 사용하며, 스레드가 놀고 있더라고 풀에서 제거하지 않는다.ScheduledThreadPool
: 주어진 delay 뒤에 작업을 한 번 진행한다. 다양한 방식이 있지만 다음에 포스팅하겠다.SingleThreadPool
: 싱글 스레드이다. 한 번에 하나의 작업만 할 수 있다.
스레드풀의 작동방식은 아래와 같다.
- 작업 요청
- 작업 큐에 작업 추가
- 스레드에서 큐에 있는 작업을 꺼내서 처리
작업은 Runnable
(리턴값이 없는 경우) 또는 Callable
(리턴값이 있는 경우)를 구현해야한다.
Runnable을 구현한 예제를 만들어보았다.
package threadpool;
public class SimpleTask implements Runnable {
private int sequence;
public SimpleTask(int sequence) {
this.sequence = sequence;
}
@Override
public void run() {
System.out.println(sequence + ": [" + Thread.currentThread().getName() + "]: 작업 진행중!");
try {
Thread.sleep(100);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(sequence + ": [" + Thread.currentThread().getName() + "]: 작업 완료!");
}
}
package threadpool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExecutorServiceMain {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(3);
for (int i=0; i < 6; ++i) {
executor.submit(new SimpleTask(i + 1));
}
executor.shutdown();
}
}
실행결과 3개의 스레드가 한 번에 3개씩 총 6개의 작업을 수행하는것을 확인할 수 있었다.
1: [pool-1-thread-1]: 작업 진행중!
2: [pool-1-thread-2]: 작업 진행중!
3: [pool-1-thread-3]: 작업 진행중!
3: [pool-1-thread-3]: 작업 완료!
2: [pool-1-thread-2]: 작업 완료!
1: [pool-1-thread-1]: 작업 완료!
4: [pool-1-thread-2]: 작업 진행중!
5: [pool-1-thread-1]: 작업 진행중!
6: [pool-1-thread-3]: 작업 진행중!
5: [pool-1-thread-1]: 작업 완료!
4: [pool-1-thread-2]: 작업 완료!
6: [pool-1-thread-3]: 작업 완료!
executor는 마지막에 shutdown 메서드로 스레드풀을 종료시켜줘야한다.
submit
submit 메서드를 통해 작업 큐에 작업을 넣을 수 있다. (execute를 사용할 수도 있는데 예외가 발생하면 스레드를 종료하고 스레드풀에서 제거시키기 때문에 스레드를 재사용하는 submit을 권장한다.)
submit 메서드는 작업 큐에 작업을 넣는것이지, 실행 완료를 보장하는것이 아니다. 반환값으로는 Future<V>를 반환한다.
이 Future 객체는 실제 작업 결과가 아닌, 작업이 완료될때까지 대기하고, 작업이 완료되면 결과를 받을 수 있는 Pending 객체이다. (Javascript의 Promise 객체와 유사하다는 느낌이든다)
반환값이 존재하는 Callable 객체를 구현한 task를 만들어 테스트 해보자. Callable 인터페이스는 FunctionalInterface이므로(Runnable도 마찬가지) 람다식으로 표현할 수 있다.
package threadpool;
import java.util.concurrent.*;
public class ExecutorServiceFutureMain {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(3);
Future<Integer> futureResult = executorService.submit(() -> {
int sum = 0;
for (int i = 0; i < 100; i++) {
sum += i;
}
return sum;
});
int result = futureResult.get();
System.out.println("result = " + result); // result = 4950
executorService.shutdown();
}
}
Future의 한계
futureResult에서 get메서드를 호출하면 작업을 수행하고 있는 스레드에서 작업을 완료할때까지 블로킹 된다. 그러므로 작업을 완료할때까지 다른 코드를 실행할 수 없게 된다. 그러므로 따로 스레드를 만들어주거나, 타임아웃을 설정하거나, 콜백 객체를 사용하는 방식등의 다른 방법을 사용해야 한다. 또한, get 작업을 진행하는 도중 지연이 발생해서 클라이언트가 영원히 작업을 기다리는 경우가 발생할 수 있다. 타임아웃을 설정한다고 해도, 클라이언트는 어느 부분이 문제인지 예외를 정확히 확인하기 어렵다는 문제가 있다.
예외 처리, 블로킹 문제 등을 보완한 CompletableFuture는 다음에 포스팅 할 것이다.
참고
https://www.hanbit.co.kr/store/books/look.php?p_code=B4861113361
이것이 자바다(개정판)
2015년 초판이 출간된 이후부터 지금까지 기본 개념에 충실한 설명으로 독자들에게 큰 사랑을 받아온 『이것이 자바다』의 개정판. 기존 Java 8 버전에 최신 Java 17 LTS 버전까지 아우르는 내용으로
www.hanbit.co.kr
'Java,Kotlin,SpringBoot' 카테고리의 다른 글
[SpringBoot] AbstractAggregateRoot로 DomainEvent 발행하기 (3) | 2024.11.27 |
---|---|
[SpringBoot] @Async로 비동기 작업 처리하기 (1) | 2024.09.19 |
[SpringBoot] JPA에서 Enum 값 다루기(AttributeConverter) (3) | 2024.09.13 |
[SpringBoot] h2 테이블 이름 upper_case 해제하기 (1) | 2024.08.10 |
[SpringBoot] MockK 사용하기 (0) | 2023.07.09 |