이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • Java Thread와 ExecutorService에서 붙잡아야 할 핵심 판단 기준은 무엇인가?
  • thread pool은 실행 정책과 대기 정책의 조합이다라는 기준은 어떤 JVM/OS 신호로 확인되는가?
  • Java/Spring 코드에서 Java Thread와 ExecutorService 판단 기준은 어떤 장애 신호로 드러나는가?

개요

이 문서의 주제는 Java Thread와 ExecutorService이다. Java Thread와 ExecutorService가 요청 처리량, queue, rejection, shutdown에 미치는 영향을 정리한다.

먼저 붙잡을 기준은 다음 문장이다. thread pool은 실행 정책과 대기 정책의 조합이다. 이 문서는 이 기준을 Java/Spring 실행 흐름, JVM/OS 관찰 지표, 장애 판단 순서로 연결한다.

핵심 모델

  • 판단 기준: ExecutorService는 thread 수가 아니라 실행 정책이다.
  • Executor는 worker 수, queue, rejection, shutdown, thread name, exception handling을 함께 가진다.
  • Unbounded queue는 지연을 실패로 드러내지 않고 memory와 대기 시간 안에 숨길 수 있다.
  • Fixed pool은 worker 수만 고정할 뿐 작업 제출 속도와 작업 시간의 불균형을 해결하지 않는다.
  • Executor를 닫지 않으면 애플리케이션 종료와 resource 회수가 꼬일 수 있다.

문서별 핵심 구분

  • ExecutorService는 thread 수, queue, rejection, lifecycle 정책의 묶음이다.
  • 작업 제출이 성공해도 queue에서 오래 기다리면 사용자는 느리게 느낀다.
  • shutdown 정책이 없으면 배포 종료나 장애 중 작업이 유실될 수 있다.

원리

Java Thread는 실행 흐름이고 ExecutorService는 그 실행 흐름을 재사용하는 정책이다. 직접 thread를 계속 만들면 생성 비용, stack memory, shutdown 관리가 문제가 되므로 서버 코드는 보통 executor를 통해 작업을 제출한다.

Executor의 핵심은 worker 수보다 queue와 rejection이다. worker가 모두 바쁘면 새 작업은 queue에 들어가거나 거절된다. queue가 무제한이면 장애가 즉시 드러나지 않고 latency와 memory 사용량이 뒤에서 커진다.

Blocking I/O 작업과 CPU 작업을 같은 executor에 섞으면 튜닝 기준이 흔들린다. CPU 작업은 core 수가 상한이고, blocking I/O 작업은 대기 시간이 길수록 worker를 더 오래 붙잡는다. 같은 pool을 공유하면 한 종류의 작업이 다른 작업을 굶길 수 있다.

Java Thread와 ExecutorService 관점에서 중요한 점은 “동시에 실행할 수 있는 수”와 “대기시킬 수 있는 수”를 분리하는 것이다. 장애 중에는 active, queued, completed, rejected, thread name 분포를 함께 봐야 한다.

Java/Spring 연결

  • @Async는 기본 executor를 명시하지 않으면 팀이 모르는 thread pool에서 실행될 수 있다.
  • Scheduler, batch, 외부 API client는 요청 thread와 다른 executor 병목을 만든다.
  • Rejection policy가 없으면 장애 시 지연, memory 증가, silent drop 중 무엇이 일어나는지 알기 어렵다.
  • Executor thread name을 업무별로 나누면 dump에서 병목 pool을 바로 찾을 수 있다.
  • Shutdown hook과 graceful shutdown timeout은 executor 작업 완료 가능성과 연결된다.

코드와 설정 예시

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    20, 20,
    30, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(100),
    new ThreadPoolExecutor.CallerRunsPolicy()
);

예시는 executor의 운영 계약을 드러내기 위한 것이다. worker 20개, queue 100개, rejection 정책이 함께 있어야 장애 중 새 작업을 계속 받을지, 호출자에게 backpressure를 걸지 판단할 수 있다.

관찰 명령

jcmd <pid> Thread.print
curl -s localhost:8080/actuator/metrics/executor.active
curl -s localhost:8080/actuator/metrics/jvm.threads.live

Java Thread와 ExecutorService 관찰 명령은 결론이 아니라 가설 확인 도구다. 운영 중에는 반복 간격, 대상 PID, 수집 시각을 함께 남기고 출력이 큰 명령은 범위를 제한한다.

지표 해석

신호먼저 의심할 것다음 확인
RUNNABLE thread가 core보다 많음CPU 경합 또는 native I/O 대기top -H, stack trace, load average
BLOCKED가 특정 lock에 몰림synchronized/monitor 경합lock owner와 기다리는 thread
WAITING/TIMED_WAITING이 pool에 몰림queue 대기, connection 대기, sleep/retryexecutor, Hikari, timeout 지표
thread 수가 계속 증가unbounded executor 또는 thread leakthread name 분포와 생성 위치

Java Thread와 ExecutorService에서는 핵심 신호와 보조 지표를 같은 시간축에서 맞춘다. 먼저 변한 신호와 뒤따라 포화된 신호를 분리해야 원인과 결과를 구분할 수 있다.

장애 상황에서 보는 순서

  1. executor별 active, queue, completed, rejected 지표를 확인한다.
  2. thread dump에서 thread name prefix별 stack을 분리한다.
  3. queue가 증가하는지, worker가 blocking 중인지, rejection이 발생하는지 나눈다.
  4. @Async나 scheduler가 같은 pool에 nested task를 제출하는 구조를 찾는다.
  5. pool size 조정 전에 queue bound, timeout, rejection, bulkhead를 먼저 정한다.

이 순서는 executor를 단순 thread 개수가 아니라 실행 정책으로 검증하기 위한 루틴이다.

실전 팁

  • executor별 thread name prefix를 명확히 둔다.
  • queue는 무제한보다 업무별 상한과 rejection 정책을 둔다.
  • blocking 작업과 CPU 작업은 같은 executor에 섞지 않는다.
  • Actuator executor metric을 endpoint latency와 함께 본다.
  • 장애 중에는 active, queued, rejected를 동시에 확인한다.

주니어 팁

  • Java Thread와 ExecutorService 주제는 용어 암기보다 정상 상태와 장애 상태에서 어떤 증거가 달라지는지 연결해 익힌다.
  • 처음에는 Java Thread와 ExecutorService를 재현 가능한 기본 명령과 Actuator 지표로 확인하는 습관부터 만든다.
  • Java Thread와 ExecutorService 숫자는 크기 자체보다 평소 기준선에서 얼마나 벗어났는지로 판단한다.

시니어 팁

  • Java Thread와 ExecutorService 주제의 영향이 pool, queue, retry, timeout으로 번지는 경로를 설계 리뷰에 포함한다.
  • Java Thread와 ExecutorService runbook에는 실행 명령, 정상 범위, 해석 기준, 금지할 대응을 같이 둔다.
  • Java Thread와 ExecutorService 판단이 container, LB, 다중 인스턴스 환경에서도 같은 의미인지 확인한다.

Guru급 팁

  • Java Thread와 ExecutorService 주제는 단일 snapshot보다 변화 방향이 중요하다. 같은 명령을 간격을 두고 실행해 상태가 증가, 유지, 회복 중 어디인지 본다.
  • 고급 도구는 Java Thread와 ExecutorService 가설을 좁힐 때만 사용하고 기본 지표와 모순되는지 먼저 확인한다.
  • Java Thread와 ExecutorService 조치 후에는 CPU, memory, socket, disk, pool 중 다음 포화 지점이 어디로 이동했는지 확인한다.

위험 신호!

  • 무제한 queue로 backpressure를 memory 문제로 바꾼다.
  • 공용 executor에 batch와 사용자 요청 후속 작업을 같이 넣는다.
  • Future 결과를 같은 pool thread에서 기다려 starvation을 만든다.
  • shutdownNow를 안전한 종료로 오해한다.
  • executor metric 없이 thread 수만 설정한다.

확인 질문

확인 질문

  • Java Thread와 ExecutorService 주제를 장애 중 확인할 때 Application 로그만으로 부족한 이유는 무엇인가?
    • 로그는 코드 경로를 보여 주지만 Java Thread와 ExecutorService의 JVM/OS 자원 상태, 대기 위치, 포화 방향은 별도 지표로 확인해야 하기 때문이다.
  • Java Thread와 ExecutorService 관련 설정이나 pool 크기를 바꾸기 전에 확인할 것은 무엇인가?
    • 먼저 변한 신호, 하위 자원 capacity, 변경이 완화인지 증폭인지 판단할 기준을 Java Thread와 ExecutorService 지표로 확인해야 한다.
  • Java Thread와 ExecutorService 재발을 막기 위해 장애 기록에 무엇을 남겨야 하는가?
    • 발생 시간, 영향 범위, 사용한 명령, Java Thread와 ExecutorService 핵심 지표, 원인 판단, 완화 조치, 근본 수정, 남은 리스크를 남긴다.

참고 문서