Files
llm-wiki/raw/interviews/async-executor-saturation-context-propagation-2026-06-13.md
T

5.0 KiB
Raw Blame History

title, source_type, status, related_branches, related_projects, tags, created, status_label
title source_type status related_branches related_projects tags created status_label
interview / async-executor-saturation-context-propagation-2026-06-13 interview-prep raw
feature-background-job-async-contract
ca-skeleton
interview
ca-skeleton
async
threadpool
taskdecorator
mdc
graceful-shutdown
micrometer
2026-06-13 captured

interview: async-executor-saturation-context-propagation-2026-06-13

Layer: raw/interviews/ — 작업에서 정직하게 도출 가능한 면접 질문. 답은 실제 구현/검증 근거에 묶는다.

Parent / 부모

질문 / Questions

Q1. Spring Boot 의 기본 @Async executor 를 운영에서 그대로 쓰면 무슨 문제가 있나?

  • 핵심: ThreadPoolTaskExecutor 의 queue capacity 기본값이 Integer.MAX_VALUE(사실상 unbounded). JDK ThreadPoolExecutor 는 큐가 가득 찰 때만 core→max 로 성장하므로, unbounded 큐에서는 maxPoolSize 가 영원히 발동하지 않는다. 부하가 몰리면 스레드가 아니라 큐(=힙)가 무한정 쌓여 OOM/지연으로 번진다.
  • 후속: 어떻게 고치나? → bounded queue 강제 + 직접 executor 빈 등록(자동 구성은 @ConditionalOnMissingBean(Executor.class) 로 back-off). Integer.MAX_VALUE 큐 용량은 "이름만 bounded" 이므로 설정 검증에서 거부.

Q2. saturation(거부)이 발생했을 때 무엇이 "조용히 삼켜지는" 위험인가? 어떻게 막나?

  • AbortPolicy 는 RejectedExecutionException 을 던지지만, fire-and-forget @Async 호출이면 호출부가 그 예외를 못 본다. 따라서 거부 핸들러를 감싸 (1) 구조화 ERROR 로그(error.code), (2) 카운터(executor.rejected.total) 를 먼저 남기고 예외를 재던진다. 거부율은 alert(p1)로 노출.
  • 후속: CallerRunsPolicy 는 왜 기본이 아닌가? → caller 가 request 스레드면 back-pressure 가 요청 지연을 직접 침식한다. use case 차원에서 명시 선언할 때만 허용.

Q3. @Async 작업에 호출 스레드의 MDC(request_id/trace_id 등)를 어떻게 넘기나? 함정은?

  • TaskDecorator 로 submit 시점에 MDC.getCopyOfContextMap() 스냅숏을 떠 worker 에서 복원. 두 함정: (1) 캡처 시점 — run time 이 아니라 decorate(submit) time 에 떠야 호출 당시 컨텍스트가 잡힌다. (2) 대칭 복원 — 작업 후 worker 의 이전 MDC 로 되돌리지 않으면 풀 재사용 스레드가 한 작업의 MDC 를 다음 작업으로 흘린다(MDC bleed).
  • 후속: 왜 InheritableThreadLocal 을 안 쓰나? → 풀 스레드는 미리 생성/재사용되므로 상속 시점이 호출과 무관해 stale. 명시적 capture/restore 가 정답.

Q4. SecurityContext(principal)는 왜 기본 전파하지 않나?

  • 풀 스레드 재사용 + MODE_INHERITABLETHREADLOCAL 조합은 다른 요청의 principal 이 남아있는 stale context 위험. 그래서 기본 전파 대상은 MDC 4키뿐이고(registry 상 user_principal=propagation: [none]), principal 이 필요한 use case 만 DelegatingSecurityContextTaskExecutor 로 명시적 opt-in.

Q5. graceful shutdown 에서 in-flight 배경 작업을 어떻게 다루나? 19s 같은 숫자는 어디서 오나?

  • setWaitForTasksToCompleteOnShutdown(true) + setAwaitTerminationSeconds(N). 예산 계층: executor await(≤19s) < app shutdown(20s) ≤ spring.lifecycle.timeout-per-shutdown-phase < k8s terminationGracePeriodSeconds(기본 30s). 19 = 20s 1s 정리 마진. grace period 초과 시 SIGKILL 이라 await 가 그 안에 끝나야 한다.
  • 후속: interrupt 에 반응 안 하는 blocking call(JDBC)이면? → awaitTermination 초과 → SIGKILL 노출. 그래서 in-flight 가 19s 를 넘으면 멱등 retry-on-next-startup 을 전제로 설계.

Q6. retry 횟수(retry_attempt)를 metric 태그로 넣으면 안 되는 이유는?

  • 카디널리티 폭발. retry_attempt 는 값 범위가 작아 보여도 job_name×outcome×attempt 조합이 시계열을 곱한다. registry 에서 job.retry.total 의 태그는 job_name+outcome(bounded 4: SUCCESS/RETRY/EXHAUSTED/DLQ)뿐이고, retry_attempt 는 로그 필드로만 둔다. 메트릭 레코더의 시그니처에 attempt 를 넣지 않는 이유.

Sources / 근거