들어가기..
이전에 ThreadPool을 선언하는 법 , Async와 Transaction , DB커넥션 풀과의 관계, 내부동작 등 여러 가지를 알아봤다 이제는 얼마 안남았다 2개 정도..?? 조금만 힘내서 끝까지 학습해 보자!!
RejectedExecutionHandler
이전에 ThreadPool을 설정할 때 우리가 한 가지 안 다룬 코드가 한 줄 있다 바로..!
executor.setRejectedExecutionHandler(
new ThreadPoolExecutor.CallerRunsPolicy()
);
여기서 RejectedExecutionHandler 란??
서버가 더 이상 못 받는 순간에 선택할 정책 을 결정한다
즉 서버가 더 이상 요청을 못받는 상황에서 선택할 정책을 결정한다
정책의 종류와 장단점
종류
정책은 크게 4가지가 있다
- Abort Policy
- CallerRunsPolicy
- Discard Policy
- DiscardOldestPolicy
장단점
① AbortPolicy
throw new RejectedExecutionException();
결과
- 요청 즉시 실패
- 예외 전파
언제 쓰나?
- 테스트
- 실패가 곧 오류여야 하는 내부 시스템
❌ 실서비스 거의 안 씀
② CallerRunsPolicy (⭐)
동작
작업을 "요청한 스레드"가 직접 실행
장점
- 자연스러운 백프레셔
- 서버가 스스로 속도 제한
- 작업 유실 ❌
단점
- 응답 시간 증가
- 타임아웃 위험
언제 쓰나?
👉 비동기 작업 대부분
③ DiscardPolicy
동작
아무 일도 안 하고 그냥 버림
특징
- 예외 ❌
- 로그 ❌
언제 쓰나?
- 진짜 버려도 되는 작업
- 통계 로그
- 모니터링 이벤트
👉 핵심 비즈니스 로직에는 절대 금지
④ DiscardOldestPolicy
동작
큐에서 가장 오래된 작업 제거 → 새 작업 삽입
특징
- 최신 작업 우선
- 오래된 작업은 유실
언제 쓰나?
- 실시간성 중요
- 위치 업데이트
- 상태 동기화
한눈에 보면..
정리)
| Abort | ❌ | 즉시 실패 | 거의 ❌ |
| CallerRuns | ❌ | 느려짐 | ⭐⭐⭐ |
| Discard | ⭕ | 없음 | 극히 제한 |
| DiscardOldest | ⭕ | 없음 | 제한적 |
Usecase)
| 푸시 알림 | CallerRuns |
| 외부 API | CallerRuns + Timeout |
| 로그 수집 | Discard |
| 실시간 상태 | DiscardOldest |
궁금증 발생..!!
여기까지 공부하다가 하나의 궁금증이 발생했다.. CP 운용 방식과 비슷하게 callerRunspolicy 쓰면서 대기큐에서 기다리다가 Timeout만 잘 설정해주면 상관없는 거 아닌가..?
결론부터 말하면
“CallerRunsPolicy + 큐 대기시간 설정”만으로는 해결 안 된다.
왜냐하면 큐 자체에는 ‘대기시간제한’이라는 개념이 없기 때문이다
ThreadPoolExecutor의 큐는 시간 개념이 없다
- 큐는 단순히 들어온 순서대로 보관
- “몇 초 이상 기다리면 버린다” 기능만 수행한다..
그럼 “대기시간제한”은 어떻게 구현하지?
실무에서 가장 많이 쓰이는 방식은..!! Semaphore + tryAcquire(timeout)이다
코드로 보자면..!!
if (!semaphore.tryAcquire(500, TimeUnit.MILLISECONDS)) {
// 너무 밀렸음 → 포기
return;
}
try {
// 작업 실행
} finally {
semaphore.release();
}
이제 진짜 다 학습했다 그러면 이제 까지 학습한 걸 토대로 전체 흐름을 생각해 보고 코드로 구현해보자!!
전체 흐름
Client
↓
OS (TCP accept queue)
↓
Tomcat Connector
↓
Tomcat Worker Thread (Thread Pool)
↓
Spring DispatcherServlet
↓
Controller → Service
주의할 것!!
Tomcat Thread Pool ≥ Async Thread Pool ≥ DB Connection Pool
마지막으로 코드로 한번 구현해 보고 글을 마무리해보쟈!
1️⃣ Async 스레드 풀 설정
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "asyncExecutor")
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("async-");
executor.setRejectedExecutionHandler(
new ThreadPoolExecutor.CallerRunsPolicy()
);
executor.initialize();
return executor;
}
}
2️⃣ Semaphore Bean 정의
@Configuration
public class SemaphoreConfig {
@Bean
public Semaphore pushSemaphore() {
return new Semaphore(5); // 동시에 5개만 실행
}
}
3️⃣ @Async + tryAcquire 적용
@Service
@RequiredArgsConstructor
public class PushService {
private final Semaphore pushSemaphore;
@Async("asyncExecutor")
public void sendPush(Long userId) {
boolean acquired = false;
try {
acquired = pushSemaphore.tryAcquire(300, TimeUnit.MILLISECONDS);
if (!acquired) {
// 너무 밀림 → 포기 or 로그
log.warn("Push skipped due to high load. userId={}", userId);
return;
}
// ===== 실제 비즈니스 로직 =====
callExternalPushApi(userId);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (acquired) {
pushSemaphore.release();
}
}
}
}

'🖥️ 컴퓨터 공부 > 주니어 백엔드 개발자가 반드시 알아야 할 실무 지식' 카테고리의 다른 글
| ThreadPool (0) | 2026.01.03 |
|---|