F-LAB JAVA · 5주차 · Phase 1 · 스레드 풀의 필요성 재정리
🏆 Phase 1 완주 — 스레드 풀 필요성 정리
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
스레드 풀은 스레드의 3가지 문제 (생성 비용·무한 생성 위험·자원 낭비) 를 재사용·개수 제한·idle 회수로 해결하며, 이것이 4주차에서 본 Executor 프레임워크의 존재 이유다.
생성 비용 은 미리 만든 스레드를 재사용 하여 해결하고, 무한 생성 위험 은 풀의 최대 개수를 제한 하여 막으며, 노는 스레드의 자원 낭비 는 일정 시간 후 idle 스레드를 회수 (keepAlive) 하여 줄인다.
일반 웹 서버는 대부분 I/O 바운드 (DB·외부 API 대기) 이므로 코어 수보다 훨씬 많은 스레드를 두지만, CPU 바운드 작업은 코어 수 + 1 정도가 적정이다.
Tomcat 의 기본 maxThreads 는 200 으로, 동시에 처리할 수 있는 요청 수의 상한이며 웹 요청이 I/O 바운드라는 가정에 기반한다.
실무 결론은 명확하다 — 스레드를 직접 만들지 말고 ExecutorService 를 쓰며, 풀 크기는 워크로드 특성에 맞춰 신중히 정한다.
스레드 풀 = 잘 운영되는 콜센터:
문제 → 해결:
생성 비용 (전화마다 채용):
→ 재사용 (상담원 팀 상시)
무한 생성 (무한 채용):
→ 개수 제한 (정원)
노는 직원 (월급 낭비):
→ idle 회수 (한가하면 임시직 해고)
적정 인원:
- 통화 위주 (I/O): 많이 (대기 중 다른 일)
- 계산 위주 (CPU): 코어 수만큼
Tomcat maxThreads 200:
- 동시 처리 전화 상한
- 웹 요청 = 대기 많음 (I/O)
- 그래서 코어보다 많이
실무:
"직접 채용하지 마라.
인사팀(ExecutorService)에 맡겨라.
정원은 신중히."
→ 스레드 풀 = 3문제 해결 (재사용/제한/회수), I/O 는 많이·CPU 는 코어 수.
1. 문제 → 해결 매핑
2. 재사용
3. 개수 제한
4. idle 회수
5. Executor 프레임워크의 존재 이유
6. 적정 스레드 수 (I/O vs CPU)
7. Tomcat maxThreads
8. Phase 1 완주 정리
9. 면접 + 자기 점검
문제 → 스레드 풀 해결:
1. 생성 비용 ↑
→ 재사용
2. 무한 생성 위험
→ 개수 제한
3. 자원 낭비 (노는 스레드)
→ idle 회수
| 문제 | 원인 | 해결 |
|---|---|---|
| 생성 비용 | 매번 생성/소멸 | 재사용 |
| 무한 생성 | 요청마다 스레드 | 최대 수 제한 |
| 자원 낭비 | 노는 스레드 | keepAlive 회수 |
한 문장:
스레드 풀 =
미리 만든 스레드를 (생성 비용)
제한된 수만큼 (무한 생성)
재사용하고 회수 (낭비)
→ 3문제 동시 해결
@Service
public class ProblemSolutionMapping {
// 3가지 해결을 모두 담은 풀
private final ThreadPoolExecutor executor = new ThreadPoolExecutor(
4, // 1. 재사용 (core 유지)
20, // 2. 개수 제한 (max)
60L, TimeUnit.SECONDS, // 3. idle 회수 (keepAlive)
new LinkedBlockingQueue<>(1000),
new ThreadPoolExecutor.CallerRunsPolicy()
);
public void process(Shipment shipment) {
executor.submit(() -> doProcess(shipment));
// - 재사용 (생성 비용 ↓)
// - 제한 (무한 생성 방지)
// - 회수 (노는 스레드 ↓)
}
private void doProcess(Shipment s) { }
}
스레드 풀이 3가지 문제를 해결하는 방식은?
답:
1. 생성 비용:
무한 생성:
자원 낭비:
한 문장:
재사용:
미리 생성한 스레드를
여러 작업에 반복 사용.
효과:
- 생성 비용 1회
- 빠른 응답
생성 비용 절감:
직접 생성:
- 작업마다 생성/소멸
- 1만 작업 = 1만 번
재사용:
- 풀 시작 시 1회
- 1만 작업 = 재사용
워커 루프 (재사용 메커니즘):
while (실행 중) {
작업 = 큐에서 꺼냄
작업.실행()
// 끝나도 죽지 X
// 다음 작업 (재사용)
}
@Service
public class ThreadReuse5 {
private final ExecutorService executor = Executors.newFixedThreadPool(4);
public void demonstrateReuse() {
// 100 작업을 4 스레드 재사용
for (int i = 0; i < 100; i++) {
final int id = i;
executor.submit(() -> {
log.debug("Task {} on {}", id,
Thread.currentThread().getName());
// 4 스레드 (pool-1-thread-1~4) 반복
// 100 작업 처리 (재사용)
});
}
}
}
재사용은?
답:
1. 재사용:
절감:
워커 루프:
효과:
개수 제한:
최대 스레드 수 고정:
- 무한 생성 방지
- 자원 보호
- 적정 동시성
자원 보호:
제한 없으면:
- 요청 폭증 → 스레드 폭증
- OOM
- 스위칭 폭증
제한:
- 최대 수 보장
- 초과 작업은 큐
스위칭 통제:
개수 제한 → 스위칭 통제:
- 적정 스레드만
- 전환 최소
- 효율
@Service
public class ThreadCountLimit {
// 개수 제한 (무한 생성 방지)
private final ThreadPoolExecutor executor = new ThreadPoolExecutor(
10, // core
20, // max (제한!)
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000),
new ThreadPoolExecutor.AbortPolicy()
);
public boolean process(Shipment shipment) {
try {
executor.submit(() -> doProcess(shipment));
return true;
} catch (RejectedExecutionException e) {
// 한계 초과 (자원 보호)
log.warn("처리 거부 (과부하)");
return false;
}
}
private void doProcess(Shipment s) { }
}
개수 제한은?
답:
1. 제한:
자원 보호:
초과:
효과:
idle 회수:
노는 스레드를 일정 시간 후 정리.
- keepAliveTime
- core 초과분
- 메모리 절약
CachedThreadPool 회수:
newCachedThreadPool:
- 60초 유휴 후 회수
- 동적 (0 ~ 무제한)
- 짧은 작업 효율
단, 무제한 (주의)
회수 동작:
피크: max 까지 (예: 20)
↓ (작업 감소)
유휴 스레드
↓ keepAliveTime
회수 (20 → core)
↓
core 유지
@Service
public class IdleRecycle {
// 변동 부하 — idle 회수
private final ThreadPoolExecutor executor = new ThreadPoolExecutor(
4, // 평상시 4
20, // 피크 20
30L, TimeUnit.SECONDS, // 유휴 30초 후 회수
new LinkedBlockingQueue<>(200)
);
// 피크 후 한가하면 20 → 4 회수
// 노는 스레드 메모리 절약
public void process(Shipment shipment) {
executor.submit(() -> doProcess(shipment));
}
private void doProcess(Shipment s) { }
}
idle 회수는?
답:
1. 회수:
CachedThreadPool:
동작:
효과:
Executor 프레임워크 존재 이유:
스레드의 3문제 해결:
- 재사용 (생성 비용)
- 제한 (무한 생성)
- 회수 (낭비)
+ 작업 관리:
- submit, Future
- shutdown
4주차 Executor 연결:
Phase 7 에서 배운:
- ExecutorService
- ThreadPoolExecutor
- 스레드 풀 종류
= 이 3문제의 해결책
Executor 추상화:
스레드 관리를 추상화:
- 작업 제출 (submit)
- 실행 정책 (풀)
- 분리
→ 비즈니스 로직 집중
핵심 가치:
- 스레드 생명주기 관리
- 작업 큐
- 결과 (Future)
- 종료 (shutdown)
→ 직접 관리보다 안전/편리
@Service
public class ExecutorRationale {
// Executor = 스레드 3문제 해결 + 작업 관리
private final ExecutorService executor = new ThreadPoolExecutor(
4, 20, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000),
new ThreadPoolExecutor.CallerRunsPolicy()
);
// 1. 작업 제출 (추상화)
public Future<BigDecimal> calculateAsync(Shipment shipment) {
return executor.submit(() -> calculateFreight(shipment));
}
// 2. 종료 관리
@PreDestroy
public void shutdown() throws InterruptedException {
executor.shutdown();
if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
}
// Executor 가 스레드 관리 + 작업 관리 모두
private BigDecimal calculateFreight(Shipment s) { return s.getWeight(); }
}
Executor 프레임워크의 존재 이유는?
답:
1. 3문제 해결:
4주차 연결:
추상화:
가치:
작업 유형:
CPU 바운드:
- 계산 위주
- CPU 사용 높음
I/O 바운드:
- 대기 위주
- DB, 네트워크, 디스크
CPU 바운드 적정:
N(threads) = N(cores) + 1
이유:
- 코어 수만큼 동시 계산
- +1 (한 스레드 페이지 폴트 대비)
- 더 많으면 스위칭만
I/O 바운드 적정:
N = N(cores) × (1 + 대기시간/계산시간)
예:
- 대기 90%, 계산 10%
- N = cores × (1 + 9) = cores × 10
이유:
- 대기 중 다른 작업
- 코어보다 많이
일반 웹 서버:
대부분 I/O 바운드:
- DB 조회 대기
- 외부 API 대기
- 파일 I/O
→ 코어보다 많은 스레드
→ 대기 중 다른 요청 처리
측정 우선:
공식은 출발점:
- 부하 테스트
- 응답 시간 모니터링
- 처리량 측정
- 튜닝
실측 > 공식
@Configuration
public class OptimalPoolSize {
private final int cores = Runtime.getRuntime().availableProcessors();
// CPU 바운드 (운임 계산)
@Bean("cpuPool")
public ExecutorService cpuPool() {
return Executors.newFixedThreadPool(cores + 1);
// 코어 + 1
}
// I/O 바운드 (외부 추적 API)
@Bean("ioPool")
public ExecutorService ioPool() {
// 추적 API 대기 95%, 처리 5% 가정
// N = cores × (1 + 0.95/0.05) = cores × 20
return new ThreadPoolExecutor(
cores * 4, // core
cores * 20, // max (I/O 대기 고려)
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(500),
new ThreadPoolExecutor.CallerRunsPolicy()
);
}
// 측정 후 조정
}
I/O 바운드 vs CPU 바운드 풀 크기 차이는?
답:
1. CPU 바운드:
I/O 바운드:
웹 서버:
측정:
Tomcat maxThreads:
동시 처리 요청 스레드 수 상한.
- 기본값: 200
- 요청마다 스레드 (전통 모델)
기본 200 의미:
- 동시 200 요청 처리
- 201번째: 대기 (acceptCount 큐)
- 큐도 차면: 거부
웹 요청 = I/O 바운드 가정:
- 코어보다 많이
- 대기 중 다른 요청
maxThreads 조정:
너무 작으면:
- 동시 처리 ↓
- 처리량 병목
너무 크면:
- 메모리 (스택 × 수)
- 스위칭 ↑
- 컨텍스트 오버헤드
→ 워크로드 측정
Tomcat 관련 설정:
maxThreads: 최대 워커 (200)
minSpareThreads: 최소 유휴 (10)
acceptCount: 대기 큐 (100)
maxConnections: 최대 연결
→ 함께 조정
// application.yml (Spring Boot - Tomcat)
/*
server:
tomcat:
threads:
max: 200 # maxThreads (동시 요청)
min-spare: 10 # 최소 유휴
accept-count: 100 # 대기 큐
max-connections: 8192
*/
@Service
public class TomcatThreadContext {
// 톰캣 워커 스레드 (요청 처리) + 별도 작업 풀
// 무거운 비동기 작업은 별도 풀 (톰캣 워커 보호)
private final ExecutorService asyncPool =
Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors() * 2);
public void handleRequest(Shipment shipment) {
// 톰캣 워커 스레드 (요청)
// 무거운 작업은 별도 풀로 위임
asyncPool.submit(() -> heavyProcess(shipment));
// → 톰캣 워커 빠르게 반환
}
private void heavyProcess(Shipment s) { }
}
Tomcat 기본 maxThreads와 의미는?
답:
1. maxThreads:
의미:
조정:
관련:
Phase 1 — 스레드 풀의 필요성 재정리
Unit 1.1 — 스레드를 많이 쓸 때의 함정
- 3함정 (스위칭/데드락/노는 스레드)
- 공짜 아님
Unit 1.2 — 컨텍스트 스위칭의 진짜 비용
- 4단계, 캐시 무효화
- 스레드 수 비례
Unit 1.3 — 스레드 풀이라는 답
- 재사용/제한/회수
- 적정 크기
Phase 1 핵심 메시지:
"스레드는 공짜가 아니다.
많을수록 빠른 게 아니다.
스레드 풀로 재사용·제한·회수하라.
풀 크기는 워크로드에 맞춰 신중히."
실무 결론:
1. 스레드 직접 생성 X
- ExecutorService
2. 풀 크기 신중
- CPU: 코어 + 1
- I/O: 더 많이
3. 측정 기반
- 부하 테스트
- 튜닝
Phase 1 → Phase 2:
- 스레드 풀 → 동시성 안전 도구
Phase 2 — 동시성 안전 도구 3종:
- synchronized / volatile / Atomic
- 가시성 vs 원자성
- CAS (★ 마스터)
Phase 1의 종합은?
답:
1. 3 Unit:
메시지:
실무:
| Q | 핵심 답변 |
|---|---|
| 3문제 해결? | 재사용/제한/회수 |
| 재사용? | 생성 비용 ↓ |
| 개수 제한? | 자원 보호 |
| idle 회수? | keepAlive |
| Executor 이유? | 3문제 해결 |
| 적정 수? | CPU 코어+1, I/O 많이 |
| 웹 서버? | I/O 바운드 |
| Tomcat maxThreads? | 200 (동시 요청) |
| 실무 결론? | 직접 생성 X |
| 측정? | 공식은 출발점 |
답:
답:
답:
답:
답:
1. 3문제 해결
2. 적정 크기
3. 실무 결론
🚀 Phase 1 — 스레드 풀의 필요성 재정리
✅ Unit 1.1 스레드를 많이 쓸 때의 함정
✅ Unit 1.2 컨텍스트 스위칭의 진짜 비용
✅ Unit 1.3 스레드 풀이라는 답 ← 여기, Phase 1 완주
→ 스레드는 공짜가 아니다
→ 3함정 (스위칭/데드락/노는 스레드)
→ 스레드 풀 (재사용/제한/회수)
Phase 2 — 동시성 안전 도구 3종 비교
Unit 2.1 — 두 가지 동시성 문제 구분 (가시성/원자성)
Unit 2.2 — synchronized (둘 다, 느림)
Unit 2.3 — volatile (가시성만)
Unit 2.4 — Atomic + CAS (★ 마스터)
✅ Part A — 동시성 마무리
✅ Phase 1 — 스레드 풀 필요성 (3 Unit) ← 완주
⏭ Phase 2 — 동시성 안전 도구 (4 Unit)
총: 3/26 Unit