469 lines
62 KiB
Markdown
469 lines
62 KiB
Markdown
---
|
||
title: branch / feature-background-job-async-contract
|
||
source_type: branch-note
|
||
status: raw
|
||
branch: feature-background-job-async-contract
|
||
parent_branch:
|
||
related_projects: [ca-skeleton]
|
||
governing_docs: [raw/project-notes/ca-skeleton-operational-contract]
|
||
tags: [branch, ca-skeleton, async, scheduler, background-job]
|
||
created: 2026-05-22
|
||
target_merge:
|
||
status_label: in-progress
|
||
id: BR-CA-SKELETON-OPERATIONAL-CONTRACT-025
|
||
kind: project-work-item
|
||
project: ca-skeleton-operational-contract
|
||
work_item: WI-CA-SKELETON-OPERATIONAL-CONTRACT-025
|
||
inherits: [DEC-CA-SKELETON-OPERATIONAL-CONTRACT-SCHEDULER-LOCK-001@1, DEC-CA-SKELETON-OPERATIONAL-CONTRACT-EVENT-BROKER-001@1]
|
||
refines: []
|
||
overrides: []
|
||
depends_on: []
|
||
contract_packet: 1
|
||
contract_packet_sha256: 928d7721870b6023078790c09e8a4319b34e3a3a37e0e3b0cfa9666b4c1e4a46
|
||
---
|
||
|
||
# branch: feature-background-job-async-contract
|
||
|
||
> Layer: `raw/branch-notes/` — background job, scheduler, async executor 기준을 정의합니다.
|
||
|
||
<!-- section-id: branch-parent -->
|
||
## 부모 (필수)
|
||
|
||
- **Parent project (canonical SSOT)**: [[raw/project-notes/ca-skeleton-operational-contract]]
|
||
|
||
> ca-skeleton 은 별도 root branch 없이 project-note 가 SSOT 역할. 본 feature branch 는 project-note 의 운영 계약 중 해당 영역 (§<관련 섹션>) 의 결정/근거/금지 사항을 정제한다.
|
||
|
||
<!-- GENERATED: branch-contract:start -->
|
||
<!-- section-id: branch-contract-packet -->
|
||
## 브랜치 계약 패킷
|
||
|
||
- **생성 시 프로젝트 개정**: `1`
|
||
- **패킷 스키마**: `contract_packet: 1`
|
||
- **완료 조건**: duplicate scheduler/outbox execution 방지 test가 통과한다
|
||
|
||
<!-- section-id: inherited-project-decisions -->
|
||
### 상속한 프로젝트 결정
|
||
|
||
| Decision Ref | Project Summary | Branch Application | Source |
|
||
|---|---|---|---|
|
||
| `DEC-CA-SKELETON-OPERATIONAL-CONTRACT-SCHEDULER-LOCK-001@1` | single-instance가 default이며 multi-instance scheduler/outbox는 DB advisory lock을 사용한다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-operational-contract]] |
|
||
| `DEC-CA-SKELETON-OPERATIONAL-CONTRACT-EVENT-BROKER-001@1` | core는 broker-agnostic outbox를 제공하고 Kafka는 optional adapter로 둔다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-operational-contract]] |
|
||
|
||
<!-- section-id: branch-local-decisions -->
|
||
### 브랜치 지역 결정
|
||
|
||
> 기존 branch-local 결정은 아래 `## Decision Evidence Map / 결정-근거 매핑`의 D-row가 소유하며 이 packet에서 복제하지 않는다.
|
||
|
||
| Decision ID | Decision | Relation | Supporting Claims | Status |
|
||
|---|---|---|---|---|
|
||
|
||
<!-- section-id: declared-overrides -->
|
||
### 선언한 예외
|
||
|
||
| Override ID | Overrides | Reason | Approval | Status |
|
||
|---|---|---|---|---|
|
||
<!-- GENERATED: branch-contract:end -->
|
||
|
||
<!-- section-id: branch-goal -->
|
||
## 목표
|
||
|
||
요청 스레드 밖에서 발생하는 실패는 GlobalExceptionHandler로 잡히지 않습니다. async exception, executor saturation, scheduled job overlap, shutdown 중 job 처리 기준이 필요합니다.
|
||
|
||
- 이슈:
|
||
- PR:
|
||
|
||
<!-- section-id: branch-scope -->
|
||
## 범위
|
||
|
||
### 포함 범위
|
||
|
||
- async exception handling.
|
||
- executor saturation/rejection 기준.
|
||
- scheduled job overlap 기준.
|
||
- job id/correlationId 기준.
|
||
- retry/backoff 기준.
|
||
- shutdown 중 job 처리 기준.
|
||
- background failure logging 기준.
|
||
|
||
### 제외 범위
|
||
|
||
- business batch job 구현.
|
||
- external scheduler platform 연동.
|
||
- distributed job lock 기본 구현.
|
||
|
||
## 근거 (필수, 최소 1개+)
|
||
|
||
> 본 branch의 결정 근거. 상세 비교는 §외부 근거 / 대안 조사 (있다면) 참조.
|
||
|
||
| Source | 정당화하는 결정 |
|
||
|---|---|
|
||
| [[raw/official-docs/outbox-skip-locked-microservices-io]] | Chris Richardson 원형 |
|
||
| [[raw/official-docs/skip-locked-postgres-docs]] | Postgres SKIP LOCKED 원리 |
|
||
| [[raw/company-tech-blogs/outbox-woowahan-techblog-pattern]] | 우아한형제들 polling 사례 |
|
||
| [[raw/official-docs/outbox-debezium-official-docs]] | [[raw/company-tech-blogs/outbox-wix-engineering-debezium]] |
|
||
| [[raw/company-tech-blogs/outbox-wix-engineering-debezium]] | — |
|
||
| [[raw/company-tech-blogs/outbox-confluent-kafka-connect-smt]] | — |
|
||
| [[raw/official-docs/spring-transactional-event-listener]] | — |
|
||
| [[raw/official-docs/event-sourcing-vs-outbox-microservices-io]] | — |
|
||
| [[raw/company-tech-blogs/outbox-netflix-domain-events-cdc]] | — |
|
||
| [[raw/official-docs/dual-write-antipattern-microservices-io]] | outbox 도입 근거 |
|
||
| [[raw/official-docs/spring-framework-observability-context-propagating-task-decorator]] | D5/D6 — ContextPropagatingTaskDecorator + setTaskDecorator() 패턴이 Spring 공식 권고, MDC + Observation context worker thread 전파 근거 |
|
||
| [[raw/official-docs/retry-spring-retry-readme-backoff-defaults]] | D4 — maxAttempts default = 3 verbatim 확인 (SPRING-RETRY-C1); exp+jitter 는 라이브러리 default 아님, 명시 설정 필요 (SPRING-RETRY-C2) |
|
||
| [[raw/company-tech-blogs/retry-aws-exponential-backoff-and-jitter]] | D4 — Full Jitter 공식·no-jitter 열위 근거·Full vs Equal vs Decorrelated 비교 (AWS-JITTER-C1~C5) |
|
||
| [[raw/official-docs/spring-boot-graceful-shutdown-reference]] | D8 — SmartLifecycle earliest phase 신규 요청 차단(SB-GS-C2/C5) + `spring.lifecycle.timeout-per-shutdown-phase` phase timeout 상한(SB-GS-C4) |
|
||
| [[raw/official-docs/kubernetes-pod-lifecycle-termination]] | D8 — k8s terminationGracePeriodSeconds 기본 30s + SIGTERM→SIGKILL 시퀀스 근거 (K8S-POD-LC-C1~C3) |
|
||
| [[raw/official-docs/spring-executor-configuration-support-javadoc]] | D8 — setWaitForTasksToCompleteOnShutdown(true) + setAwaitTerminationSeconds(N) 조합이 in-flight job 을 컨테이너 종료와 정합시키는 공식 API (default 는 즉시 interrupt) — EXEC-CS-C1~C4 |
|
||
| [[raw/official-docs/micrometer-context-propagation-purpose-thread-local-accessor]] | D5/D6 — ContextSnapshot/ThreadLocalAccessor 가 async cross-thread ThreadLocal 전파의 공식 메커니즘 (MICRO-CP-C1~C5) |
|
||
| [[raw/official-docs/retry-aws-well-architected-rel05-bp03]] | D4 — "limit the maximum number of retries" 공식 근거 (WAF-REL05-C1/C2) + non-transient error retry 금지 (WAF-REL05-C3) + multi-layer retry storm anti-pattern (WAF-REL05-C4) + non-idempotent retry 금지 (WAF-REL05-C5) |
|
||
| [[raw/official-docs/spring-boot-task-execution-scheduling-reference]] | D7 — auto-configured executor 기본값(8 core / unbounded queue) 대비 bounded queue 강제의 공식 근거; virtual threads 대안 존재(SB-TASK-C1~C4) |
|
||
| [[raw/official-docs/spring-security-concurrency-delegating-security-context-executor]] | D5 — SecurityContext 의 `@Async` 전파는 `DelegatingSecurityContextExecutor`/`DelegatingSecurityContextTaskExecutor` 를 통한 explicit opt-in 이 공식 메커니즘 (SS-CONC-C3, SS-CONC-C4) |
|
||
| [[raw/official-docs/jdk21-threadpoolexecutor-javadoc]] | D7 — JDK `ThreadPoolExecutor` pool growth 3단계(TPE-JDK21-C1/C2), unbounded queue 에서 maximumPoolSize 무효(TPE-JDK21-C3), bounded queue resource-exhaustion 방지(TPE-JDK21-C4), AbortPolicy 기본값 시맨틱(TPE-JDK21-C5), CallerRunsPolicy 피드백 감속 메커니즘(TPE-JDK21-C6) |
|
||
| [[raw/official-docs/spring-framework-threadpooltaskexecutor-javadoc]] | D7 — Spring `ThreadPoolTaskExecutor` queueCapacity default = `Integer.MAX_VALUE` unbounded (SF-TPTE-C1) — bounded queue 강제의 negative evidence; 양수 → LinkedBlockingQueue / 0이하 → SynchronousQueue 분기(SF-TPTE-C2); maxPoolSize default = `Integer.MAX_VALUE`(SF-TPTE-C3); TaskDecorator primary use case = execution context + monitoring(SF-TPTE-C4) |
|
||
|
||
## 외부 근거 / 대안 조사 (2026-05-22 — Topic 3)
|
||
|
||
본 branch의 retry/DLQ/scheduler 결정에 대한 외부 source 조사. outbox publisher는 본 branch의 retry vocabulary를 consume. 6종 대안 비교는 (예정) `wiki/concepts/transactional-outbox-pattern.md` 참조.
|
||
|
||
- **채택 결정 (DB polling + FOR UPDATE SKIP LOCKED)**:
|
||
- [[raw/official-docs/outbox-skip-locked-microservices-io]] — Chris Richardson 원형
|
||
- [[raw/official-docs/skip-locked-postgres-docs]] — Postgres SKIP LOCKED 원리
|
||
- [[raw/company-tech-blogs/outbox-woowahan-techblog-pattern]] — 우아한형제들 polling 사례
|
||
- **검토한 대안**:
|
||
- **대안 1: Debezium CDC** — [[raw/official-docs/outbox-debezium-official-docs]], [[raw/company-tech-blogs/outbox-wix-engineering-debezium]]
|
||
- **대안 2: Kafka Connect outbox SMT** — [[raw/company-tech-blogs/outbox-confluent-kafka-connect-smt]]
|
||
- **대안 3: Spring @TransactionalEventListener** (in-process only) — [[raw/official-docs/spring-transactional-event-listener]]
|
||
- **대안 4: Event sourcing 전환** — [[raw/official-docs/event-sourcing-vs-outbox-microservices-io]]
|
||
- **대안 5: Netflix DBLog 급 자체 CDC** — [[raw/company-tech-blogs/outbox-netflix-domain-events-cdc]]
|
||
- **Negative reference (금지)**: [[raw/official-docs/dual-write-antipattern-microservices-io]] — outbox 도입 근거
|
||
- **비교 핵심**: polling lag vs CDC 인프라 비용이 결정 축. ca-tmpl 가정 = lag 수 초 허용 + Kafka Connect 운영 인력 부재 + DB가 SSOT. 가정 깨지면 Debezium migration. event sourcing은 "대안"이라기보다 도메인 모델 교체. background-job branch는 outbox publisher의 retry/DLQ를 owns. Debezium은 retry를 Kafka Connect dead-letter에 위임, ca-tmpl은 자체 DLQ vocabulary.
|
||
|
||
### 추가 외부 근거 (2026-06-11 — D4/D5/D6/D7/D8 자동조사)
|
||
|
||
`/branch-spec` 자동조사로 UNSUPPORTED 였던 D4·D5·D6·D7·D8 에 공식 doc 근거 12건을 아카이브 (위 Sources 표 11~22행). 대안 비교 요지:
|
||
|
||
- **D4 retry shape**: 채택 = exp + jitter + max 3 + DLQ. 대안 = fixed-interval(단일 인스턴스·예측 가능 복구 한정 — Spring Retry/Resilience4j 라이브러리 default), unlimited retry + circuit breaker(외부 HTTP 의존 전용 — DB 기반 DLQ 와 시맨틱 충돌). non-transient error 는 retry 자체가 anti-pattern (WAF-REL05-C3).
|
||
- **D5/D6 context propagation**: 채택 = TaskDecorator 1개 등록. Spring 공식 구현체 `ContextPropagatingTaskDecorator` 가 MDC + Observation 을 동시 전파 (SF-OBS-C1/C2) — 수동 4-key copy 대비 우위이나 `io.micrometer:context-propagation` classpath 필수 (SF-OBS-C3). `SecurityContextHolder.MODE_INHERITABLETHREADLOCAL` 은 thread pool 재사용 시 stale context 위험으로 부적합 — explicit opt-in 은 `DelegatingSecurityContext*` (SS-CONC-C3/C4).
|
||
- **D7 saturation**: 채택 = bounded queue + AbortPolicy. 대안 = CallerRunsPolicy(caller 가 request thread 가 아닐 때만 — request latency 직접 침식, TPE-JDK21-C6), unbounded queue 는 REJECTED(max pool 무효화 — TPE-JDK21-C3 + SF-TPTE-C1). Boot 3.2+ virtual threads(`SimpleAsyncTaskExecutor`)는 별도 검토 대상 (SB-TASK-C4).
|
||
- **D8 shutdown**: 채택 = budget-fit (await ≤ 19s + 멱등 retry-on-next-startup). 대안 = terminationGracePeriodSeconds 연장 — parent project 운영 계약 변경이므로 본 branch 범위 밖.
|
||
|
||
## TODO
|
||
|
||
> TODO drained — 결정은 아래 표/결정 사항 참조.
|
||
|
||
## 진행 중 메모
|
||
|
||
- background failure는 HTTP response가 없으므로 log/metric/alert가 핵심 계약입니다.
|
||
|
||
## 구현 기록
|
||
|
||
> `documented-only`/`planned` → 실 구현 + 로컬 검증 완료. 구현 git 브랜치: `feature/domain-event-outbox-contract` (background-job 을 outbox 브랜치 위에서 이어서 구현). §Audit A6 의 "전부 미구현(planned)" 상태가 아래로 갱신됨.
|
||
|
||
- **D4 retry/DLQ vocabulary** (`actually-implemented`): `shared-contract` `OperationalError` 에 `JOB_EXECUTOR_REJECTED`(TRANSIENT_DEPENDENCY/503/true), `JOB_TIMEOUT`(TRANSIENT_DEPENDENCY/500/true), `JOB_DEAD_LETTER`(INTERNAL/500/false) 추가 — registry SSOT 와 일치(ErrorCodeRegistryMappingTest + BackgroundJobErrorCodeContractTest 가 category/status/retryable/runbook_link 교차검증). retry **carrier 는 미구현(planned, §3 UNSUPPORTED_IMPL)** — 어휘(error code + metric recorder)만 SSOT 로 고정. `BackgroundJobMetrics` 가 `job.retry.total{job_name,outcome}`·`job.dlq.total{job_name}` recorder seam 제공(outbox/outbound consume 용). NOTE: `retry_attempt` 는 metric tag 아님(log field) — recorder 시그니처는 `(job_name, outcome)`.
|
||
- **D7 executor + saturation** (`actually-implemented` / 수치 `planned`): `app-bootstrap` `async/AsyncExecutorConfig` 가 bounded `ThreadPoolTaskExecutor`(core=10/max=50/queue=200, `applicationTaskExecutor`, `@Primary`, Boot unbounded auto-executor back-off) 등록. `AsyncExecutorSettings`(`ca-skeleton.async.executor.*`)가 `Integer.MAX_VALUE` 큐를 거부(unbounded forbidden). saturation = `LoggingAbortPolicy`(AbortPolicy + 구조화 ERROR 로그 error.code=JOB_EXECUTOR_REJECTED + `executor.rejected.total` + 재던짐) + `executor.saturation` 게이지. **수치(10/50/200)는 부하테스트 미검증 `planned`**.
|
||
- **D5/D6 context propagation** (`locally-verified`): `AsyncContextTaskDecorator` 1개 — submit 시점 `MDC.getCopyOfContextMap()` 스냅숏(request_id/trace_id/correlation_id/tenant_id + span_id) + `DomainContextPropagator.wrap` (shared seam), 대칭 복원으로 풀 스레드 MDC bleed 방지. "TaskDecorator 미설정이면 fail" = decorator 를 executor @Bean 의 필수 의존성으로 주입(부재 시 context 기동 실패, AsyncExecutorConfigTest 가 검증). **SecurityContext principal 은 기본 전파 안 함**(opt-in `DelegatingSecurityContextTaskExecutor`, registry user_principal=`propagation:[none]`) — spec "Async Context Propagation Contract" 의 principal 라인과의 긴장은 registry SSOT + D6 우선으로 해소(문서화). Observation **scope** 전파는 `context-propagation` 라이브러리 미반입으로 MDC 문자열 복사까지만(업그레이드 경로 문서화).
|
||
- **D8 graceful shutdown** (`locally-verified`): `setWaitForTasksToCompleteOnShutdown(true)` + `setAwaitTerminationSeconds(19)` (AsyncExecutorConfigTest 가 awaitTerminationMillis=19000 검증).
|
||
- **D3 scheduler overlap / multi-instance** (`actually-implemented`): overlap = `ScheduledJobOverlapPolicyTest` (ArchUnit) 가 production `@Scheduled` 의 fixedRate 사용 금지(전부 fixedDelay). multi-instance lock 은 **기존** `StartupSafetyValidator.REQUIRED_MULTI_INSTANCE_BEANS` 의 `distributedLockProvider` 를 consume(재구현 아님) — `StartupSafetyValidatorTest` 가 이미 검증(exit 72).
|
||
- **§Audit A4 runbooks** (`actually-implemented`): `docs/runbooks/job-executor-rejected.md`·`job-timeout.md`·`job-dead-letter.md` 작성 — `runbook://job/<scenario>` → `docs/runbooks/job-<scenario>.md` 해소(BackgroundJobErrorCodeContractTest 가 파일 존재 검증).
|
||
- **wiring**: `application.yml` `ca-skeleton.async.executor.*` + `src/.env` `APP_ASYNC_EXECUTOR_*` 3종(verifyEnvKeys green).
|
||
- **검증 명령**: `:shared-contract:test` 72/72 green; `:app-bootstrap:test` 256/257 green(유일 실패는 **선재** `CleanArchitectureTest#outbound_adapter_method_returns_only_domain_or_primitives` — adapter-outbound `OutboundHttpSettings`, 본 작업 무관, `git stash` baseline 로 확인 → [[raw/errors/archunit-b7-configprops-nested-record-accessor-outbound-2026-06-13]]); `verifyEnvKeys` green; `verifyCleanArchitectureDependencies` green. 3단 리뷰 체인(architect-sentinel PASS / spec-reviewer 22/22 / quality-reviewer 2건 수정) 통과.
|
||
- **변경 파일**: `shared-contract/.../OperationalError.java`(+test), `app-bootstrap/.../bootstrap/async/{AsyncExecutorSettings,AsyncContextTaskDecorator,BackgroundJobMetrics,LoggingAbortPolicy,AsyncExecutorConfig}.java`(+6 test), `app-bootstrap/.../contract/BackgroundJobErrorCodeContractTest.java`, `application.yml`, `src/.env`, `docs/runbooks/job-*.md`, `docs/superpowers/plans/2026-06-13-background-job-async-contract.md`.
|
||
|
||
## 결정 사항 (decisions)
|
||
|
||
- 2026-05-22: scheduler/async는 runtime lifecycle에서 별도 branch로 분리.
|
||
- 2026-05-22: retry/DLQ vocabulary의 SSOT는 이 branch. outbox/outbound branches는 이 vocabulary를 소비.
|
||
- 2026-05-22: scheduler/outbox publisher는 single-instance 기본이며 multi-instance 지원 시 DB advisory lock 또는 ShedLock contract test가 필요.
|
||
- 2026-05-22: 기본 backoff는 exponential backoff with jitter, max attempts 3, DLQ after exhausted attempts.
|
||
- 2026-05-22: @Async context propagation은 `TaskDecorator` 1개를 ThreadPoolTaskExecutor에 등록해 caller→worker thread로 MDC(`request_id`/`trace_id`/`correlation_id`/`tenant_id`), Micrometer Observation context를 복사한다. SecurityContext는 explicit opt-in 시에만 전파. executor 등록 시 TaskDecorator 미설정이면 fail.
|
||
- 2026-05-22: span_id는 Micrometer Observation context에서 자동 전파(MDC explicit copy 불필요), user_principal은 SecurityContext propagation이 opt-in일 때만 복사. 따라서 explicit MDC copy 대상은 foundation 6개 중 4개(request_id, trace_id, correlation_id, tenant_id).
|
||
- 2026-05-22: executor pool sizing default = core=10, max=50, queue=200. saturation policy default = AbortPolicy. CallerRunsPolicy는 명시적 use case-level 선언 시에만 허용.
|
||
- 2026-05-22: graceful shutdown = executor await termination ≤ **19s** (container-runtime의 app shutdown 20s 내부에서 1s cleanup margin 확보. 25s는 force-stop 유발하므로 forbidden).
|
||
- 2026-06-13 (구현 정정): D6 의 "span_id 는 Observation context 자동 전파(MDC explicit copy 불필요)" 는 구현과 어긋남 — 실제는 MDC **전체 스냅숏 문자열 복사**로 span_id 가 동승하며 Observation *scope* 는 전파하지 않음(context-propagation 라이브러리 미반입). parent-span linkage 필요 시 `ContextPropagatingTaskDecorator` upgrade. (Decision Evidence Map D6 갱신 반영.)
|
||
- 2026-06-13 (긴장 해소): "Async Context Propagation Contract" 의 "principal 이 caller 와 동일" 라인은 D6(`user_principal` = `propagation:[none]`) + 보안(풀 스레드 stale principal 위험)과 충돌 → **principal 은 기본 비전파**로 확정. SecurityContext 필요 use case 만 `DelegatingSecurityContextTaskExecutor` opt-in. 테스트 계약을 "principal 비전파" negative 검증으로 교체.
|
||
|
||
## Work Item Contract
|
||
|
||
각 TODO는 아래 판정 단위로 재작성되어야 canonical 승급 가능합니다. TODO가 단순히 `기준 작성`으로 남아 있으면 이 branch는 완료로 보지 않습니다.
|
||
|
||
| field | required | rule |
|
||
| --- | --- | --- |
|
||
| Decision | yes | 구현자가 선택해야 하는 기본값 |
|
||
| Allowed | yes | 허용되는 예외와 조건 |
|
||
| Forbidden | yes | 절대 금지되는 구현/문서 상태 |
|
||
| Required registry update | conditional | error/env/header/log/metric/capability 변경 시 필수 |
|
||
| Required contract test | yes | 계약 위반 시 실패해야 하는 테스트 |
|
||
| Failure condition | yes | review/build에서 실패로 판정할 상태 |
|
||
| Canonical extraction target | yes | `wiki/projects` 승급 위치 |
|
||
|
||
## Decisionized Work Items
|
||
|
||
| item | Decision | Allowed | Forbidden | Required test |
|
||
| --- | --- | --- | --- | --- |
|
||
| async exception | structured log + metric + runbook link | fail-fast for critical background worker | swallowed exception | async exception test |
|
||
| saturation | bounded executor + rejection log | caller-runs only if documented | unbounded queue | rejection test |
|
||
| scheduler overlap | no overlap by default | overlap only with idempotent job proof | concurrent same job mutation | overlap test |
|
||
| retry/DLQ | exp backoff jitter, max 3, DLQ exhausted | branch-specific override with metric | infinite retry | retry/DLQ test |
|
||
| multi-instance lock | single-instance default | DB advisory lock or ShedLock | multi-replica without lock | distributed lock test |
|
||
| async context propagation | TaskDecorator 1개로 MDC + Observation 전파 | SecurityContext explicit opt-in | TaskDecorator 미설정 executor 등록 | @Async 메서드 안에서 MDC.get("request_id"), traceId, principal이 caller와 동일해야 함 |
|
||
| saturation policy | AbortPolicy default (core=10, max=50, queue=200) | CallerRunsPolicy with explicit use case 선언 | unbounded queue / 미선언 fallback | saturation policy test |
|
||
| graceful shutdown | await termination ≤ 19s (app shutdown 20s − 1s cleanup margin) | 짧은 quiet period override | await ≥ 20s / terminate without await | shutdown await test |
|
||
|
||
## 결정-근거 매핑
|
||
|
||
> 각 결정이 어떤 raw source claim 으로 뒷받침되는지 명시. Company-tech-blog 인용은 사례 (`company-case-study`) 로만 사용하며 공식 best practice 로 단정하지 않는다.
|
||
> `선택 조건` 열(R2): "이 조건일 때 이 결정, 다른 조건이면 어떤 대안". 분기 없으면 `N/A`.
|
||
|
||
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|
||
|---|---|---|---|---|---|
|
||
| D1 | scheduler/async 는 runtime lifecycle 에서 별도 branch 로 분리 (이 branch 가 retry/DLQ vocabulary SSOT) | N/A — 내부 스코프 결정 (분기 없음) | UNSUPPORTED_DECISION — 내부 조직/스코프 결정으로 외부 raw 근거 부재 | `internal-only` | 다른 branch (outbox/outbound) 가 이 vocabulary 를 일관 참조하는지 lint 필요 |
|
||
| D2 | retry/DLQ vocabulary SSOT 결정 — outbox/outbound branches 가 이를 consume | N/A — 내부 계약 (분기 없음) | UNSUPPORTED_DECISION — 외부 raw 의 단일 SSOT 권고 인용 부재 (내부 계약) | `internal-only` | vocabulary drift 위험 |
|
||
| D3 | scheduler/outbox publisher 는 single-instance 기본, multi-instance 시 DB advisory lock 또는 ShedLock 필수 | `APP_MULTI_INSTANCE_ENABLED=false`(default) → lock 불요; `true` → `distributedLockProvider` bean 필수 (ca-tmpl `StartupSafetyValidator` 가 startup fail 로 강제) | `raw/official-docs/skip-locked-postgres-docs.md#SK-PG-C2`, `raw/official-docs/outbox-skip-locked-microservices-io.md#OUTBOX-MIO-C4` | `official-vendor-doc + needs-confirmation` (microservices.io needs verbatim recheck) | SKIP LOCKED 는 lock contention 회피만 보장, 순서 보장은 별도 — `SK-PG-C2` 의 "inconsistent view" 경고 |
|
||
| D4 | 기본 backoff = exponential + jitter, max attempts 3, DLQ after exhausted | multi-instance 가능 또는 공유 자원(DB) 대상 transient 실패 → exp+jitter (동기화 retry spike 방지, WAF-REL05-C1); 보장된 단일 인스턴스 + 예측 가능한 짧은 복구 → fixed-interval 허용(라이브러리 default); non-transient error(권한/도메인/스키마) → retry 없이 즉시 DLQ (WAF-REL05-C3); 외부 HTTP 의존 → circuit breaker 는 outbound adapter 레이어 보완재(대체재 아님) | maxAttempts=3: `raw/official-docs/retry-spring-retry-readme-backoff-defaults.md#SPRING-RETRY-C1`; exp+jitter 는 default 아님 명시 설정 필요: `#SPRING-RETRY-C2`; exp+jitter+max limit 조합 필수(WAF 공식 권고): `raw/official-docs/retry-aws-well-architected-rel05-bp03.md#WAF-REL05-C1`; max limit 없으면 metastable failure: `#WAF-REL05-C2`; non-transient error → retry 금지(DLQ 방향): `#WAF-REL05-C3`; single-layer retry 원칙: `#WAF-REL05-C4`; non-idempotent retry 금지: `#WAF-REL05-C5`; Full Jitter 사례: `raw/company-tech-blogs/retry-aws-exponential-backoff-and-jitter.md#AWS-JITTER-C1~C5` (company-case-study). DLQ 아키텍처 자체는 WAF-REL05-C3 방향으로 정당화, DLQ 설계 상세는 별도 doc 부재 | `official-vendor-doc` (WAF-REL05-C1~C5 + SPRING-RETRY-C1/C2) + `company-case-study` (AWS-JITTER); DLQ 설계 상세 `unsupported` | max=3 이 ca-tmpl 부하에 적합한지 측정 필요 (`WAF-REL05-C2` use-case 별 조정 권고); exp+jitter `@Backoff` 명시 설정 필요; retry carrier 미확정 (§구현 가이드 3) |
|
||
| D5 | `@Async` TaskDecorator 1개로 MDC(`request_id`/`trace_id`/`correlation_id`/`tenant_id`) + Observation context 전파, SecurityContext explicit opt-in, 미설정 fail | Micrometer tracing 활성 + `io.micrometer:context-propagation` classpath(Boot 3.2+) → `ContextPropagatingTaskDecorator` 권장(SF-OBS-C1); 라이브러리 반입 불가 또는 key 별 fine-grained 통제 필요 → 수동 4-key copy decorator; SecurityContext 필요 use case → `DelegatingSecurityContextTaskExecutor` opt-in(SS-CONC-C3); `MODE_INHERITABLETHREADLOCAL` 은 thread pool 에서 금지 | MDC+Observation: `raw/official-docs/spring-framework-observability-context-propagating-task-decorator.md#SF-OBS-C1~C4`; Micrometer: `raw/official-docs/micrometer-context-propagation-purpose-thread-local-accessor.md#MICRO-CP-C1`; **SecurityContext explicit opt-in: `raw/official-docs/spring-security-concurrency-delegating-security-context-executor.md#SS-CONC-C3` + `#SS-CONC-C4`**; 미설정 fail: UNSUPPORTED_DECISION | `official-vendor-doc` (SF-OBS/MICRO-CP/SS-CONC) + `unsupported` (미설정 fail 강제 메커니즘) | SecurityContext 를 `DelegatingSecurityContextTaskExecutor` 로 감싸는 것과 TaskDecorator 내 manual propagation 의 중복 여부 별도 검증 필요 |
|
||
| D6 | **구현 정정 (2026-06-13)**: TaskDecorator 가 submit 시점 `MDC.getCopyOfContextMap()` **전체 스냅숏**을 복사 → foundation 4키(request_id/trace_id/correlation_id/tenant_id) + 그 시점 MDC 에 있는 span_id 가 **문자열로 동승**. user_principal 은 MDC 비대상(`propagation:[none]`)이라 미전파. **Observation *scope* 자체는 전파 안 함**(context-propagation 라이브러리 미반입) — span_id 연속성은 "Observation 자동 전파"가 아니라 MDC 문자열 복사에 의존 | 현재 = MDC whole-map 복사(로그 필드 연속성까지); `io.micrometer:context-propagation` 도입 시 `ContextPropagatingTaskDecorator` 로 교체하면 Observation scope(parent-span linkage)까지 전파 — upgrade 경로 | whole-map 복사로 4키 포함 보장: `raw/official-docs/spring-framework-observability-context-propagating-task-decorator.md#SF-OBS-C2`; cross-thread ThreadLocal 전파 원리: `raw/official-docs/micrometer-context-propagation-purpose-thread-local-accessor.md#MICRO-CP-C1/C2/C5` | `official-vendor-doc` (메커니즘) + `locally-verified` (구현·테스트) | (1) whole-map 복사라 비-foundation MDC 키도 동승 — 의도적(로그 연속성), negative test 부재. (2) Observation scope 미전파 = trace parent-span linkage 단절; tracing bridge 가 span_id 를 MDC 에 안 쓰는 구성이면 worker 로그 span_id 공백 가능 — upgrade 경로로 해소 |
|
||
| D7 | executor pool sizing default = core=10, max=50, queue=200, saturation = AbortPolicy default (CallerRunsPolicy 는 use-case 선언 시) | caller = HTTP request thread + saturation 관찰 필요 + DLQ/retry 계약 존재 → AbortPolicy (TPE-JDK21-C5); caller 가 request thread 아님 + task 손실 불허 + DLQ 부재 → CallerRunsPolicy use-case 명시 선언 (TPE-JDK21-C6 의 감속 = request latency 침식); unbounded queue → FORBIDDEN (max pool 무효 — TPE-JDK21-C3, SF-TPTE-C1) | `[[raw/official-docs/jdk21-threadpoolexecutor-javadoc]]#TPE-JDK21-C1` (pool growth 3단계), `#TPE-JDK21-C2` (max 도달 시 거부), `#TPE-JDK21-C3` (unbounded queue 에서 max 무효), `#TPE-JDK21-C4` (bounded queue resource-exhaustion 방지), `#TPE-JDK21-C5` (AbortPolicy 기본값), `#TPE-JDK21-C6` (CallerRunsPolicy 피드백 감속); Boot 기본값 대비: `raw/official-docs/spring-boot-task-execution-scheduling-reference.md#SB-TASK-C1~C3`; Spring default unbounded: `raw/official-docs/spring-framework-threadpooltaskexecutor-javadoc.md#SF-TPTE-C1~C3` | `official-reference` (구조) | 구체적 수치(core=10/max=50/queue=200)는 `UNSUPPORTED_IMPL_DECISION` — 부하 테스트로 별도 검증 필요 (registry `APP_ASYNC_EXECUTOR_*` default 는 본 branch 결정의 반영이므로 외부 근거 아님) |
|
||
| D8 | graceful shutdown = executor await termination ≤ 19s (container 20s − 1s cleanup margin), 25s 는 forbidden | job p99 실행 시간 < 19s + 멱등 retry-on-next-startup 가능 → budget-fit await ≤ 19s; long-running job(> 19s) 이 정당한 비즈니스 요건 → grace period 연장 검토는 OUT_OF_BRANCH_SCOPE (parent project 운영 계약 소유자 승인 필요) | `raw/official-docs/spring-executor-configuration-support-javadoc.md#EXEC-CS-C1` (default=false → 명시 필수), `#EXEC-CS-C2` (true 시 running+queued 완료 후 종료), `#EXEC-CS-C3` (setAwaitTerminationSeconds 공식 API), `#EXEC-CS-C4` (significantly higher timeout rule-of-thumb); `raw/official-docs/spring-boot-graceful-shutdown-reference.md#SB-GS-C2/C5`; `raw/official-docs/kubernetes-pod-lifecycle-termination.md#K8S-POD-LC-C1` (terminationGracePeriodSeconds default=30s), `#K8S-POD-LC-C2` (grace period 초과 시 SIGKILL), `#K8S-POD-LC-C3` (kubelet → SIGTERM to process 1); 19s 수치는 `UNSUPPORTED_IMPL_DECISION` (20s app shutdown − 1s margin — app-level 20s 는 SB-GS-C4 + ca-tmpl 설정 확인 필요) | `official-vendor-doc` (Spring + k8s) + `UNSUPPORTED_IMPL_DECISION` (19s = 20s − 1s margin) | 19s 초과 금지 이유는 k8s grace period 초과 시 SIGKILL (K8S-POD-LC-C2) 로 직접 정당화됨. 20s app timeout 과 k8s 30s grace period 의 관계 — ca-tmpl 실제 `terminationGracePeriodSeconds` 설정 확인 필요 (§Audit A1 drift 참조) |
|
||
| D9 | outbox publisher baseline = SKIP LOCKED polling (대안 검토 후 채택) | lag 수 초 허용 + Kafka Connect 운영 인력 부재 + DB 가 SSOT → SKIP LOCKED polling; lag SLO 강화 또는 polling 비용 임계 초과 → Debezium CDC migration (D10) | `raw/official-docs/skip-locked-postgres-docs.md#SK-PG-C1`, `raw/official-docs/skip-locked-postgres-docs.md#SK-PG-C2`, `raw/official-docs/outbox-skip-locked-microservices-io.md#OUTBOX-MIO-C1`, `raw/official-docs/outbox-skip-locked-microservices-io.md#OUTBOX-MIO-C3`, `raw/official-docs/outbox-skip-locked-microservices-io.md#OUTBOX-MIO-C4` | `official-vendor-doc + needs-confirmation` | `OUTBOX-MIO-C3` 의 "frequently polling can be expensive" 한계 — polling interval 측정 필요. **owner 이관 권고 — §Audit A2** |
|
||
| D10 | 대안 1 (Debezium CDC) 비교 — Kafka Connect 운영 인력 부재 시 부적합 | Kafka Connect 운영 가능 + lag SLO 빡빡 → Debezium 재검토; 그 외 → polling 유지 | `raw/official-docs/outbox-debezium-official-docs.md#OUTBOX-DBZ-C1`, `raw/official-docs/outbox-debezium-official-docs.md#OUTBOX-DBZ-C2`, `raw/official-docs/outbox-debezium-official-docs.md#OUTBOX-DBZ-C4`, `raw/company-tech-blogs/outbox-wix-engineering-debezium.md#WIX-DEBEZIUM-C1` | `needs-confirmation + company-case-study(needs-confirmation)` | Debezium raw 전체가 `needs-confirmation` (WebFetch 403 차단) — verbatim 재확인 필요. Wix 인용은 사례, 공식 best practice 아님. **owner 이관 권고 — §Audit A2** |
|
||
| D11 | 대안 5 (Spring `@TransactionalEventListener`) = in-process only, 외부 broker 발행 부적합 | in-process 소비만 필요한 이벤트 → 사용 가능; 외부 broker 발행 필요 → outbox 필수 | `raw/official-docs/spring-transactional-event-listener.md#TX-EVT-C1`, `raw/official-docs/spring-transactional-event-listener.md#TX-EVT-C3`, `raw/official-docs/spring-transactional-event-listener.md#TX-EVT-C4` | `official-vendor-doc` | `TX-EVT-C4` 의 "no transaction → not invoked" 시맨틱 — fallbackExecution 사용 시 별도 검증 필요. **owner 이관 권고 — §Audit A2** |
|
||
| D12 | dual-write 금지 (outbox 도입 근거) | N/A — negative reference (금지 규칙, 분기 없음) | `raw/official-docs/dual-write-antipattern-microservices-io.md#DUAL-WRITE-C1`, `raw/official-docs/dual-write-antipattern-microservices-io.md#DUAL-WRITE-C2`, `raw/official-docs/dual-write-antipattern-microservices-io.md#DUAL-WRITE-C3` | `needs-confirmation` (microservices.io verbatim recheck 필요) | dual-write 의 inconsistency 형태 (lost vs phantom event) 별도 분류 필요. **owner 이관 권고 — §Audit A2** |
|
||
|
||
## 구현 가이드
|
||
|
||
> 작성일 2026-06-11 (명세). ca-tmpl ground truth (registry + src grep) 대조 완료 — 계약 값은 전부 registry 기존 값 재사용, invent 없음. **구현 상태는 §구현 기록(2026-06-13) 이 authoritative** — 아래 표의 `planned` 중 다수가 구현 완료로 갱신됨(executor bean / saturation / TaskDecorator / awaitTermination 등). §Audit A6 의 "전부 미구현" 은 명세 시점 스냅숏이며 §구현 기록으로 대체됨.
|
||
|
||
### 1. Executor 구성 + saturation (D7, D8)
|
||
|
||
> **Trace**: In-scope "executor saturation/rejection 기준" → D7 (TPE-JDK21-C1~C6, SB-TASK-C1~C3, SF-TPTE-C1~C3) + D8 (EXEC-CS-C1~C4).
|
||
>
|
||
> - **UNSUPPORTED_IMPL_DECISION**: ① 수치 core=10/max=50/queue=200 — 외부 doc 은 구조(bounded queue + max 발동 조건)만 권고, 수치는 부하테스트 전 사용자 trade-off. ② RejectedExecutionHandler 를 structured log + error code 로 wrapping 하는 패턴 — 공식 reference 부재, JOB_EXECUTOR_REJECTED 매핑은 registry 계약에서 도출.
|
||
|
||
| 항목 | 명세 | 상태 |
|
||
|---|---|---|
|
||
| bean 위치 | `src/app-bootstrap/.../bootstrap/` config 클래스 (app-bootstrap CLAUDE.md: 최종 cross-module wiring 책임 — `IdempotencyConfig` 선례 패턴) | `planned` |
|
||
| pool 설정 키 | `APP_ASYNC_EXECUTOR_CORE_SIZE`(10) / `APP_ASYNC_EXECUTOR_MAX_SIZE`(50) / `APP_ASYNC_EXECUTOR_QUEUE_CAPACITY`(200) — ca-tmpl `docs/registries/env-keys.yaml` 기존 값 (owner_branch = 본 branch, required_test 3종 포함) | registry 확정 / 코드 `planned` |
|
||
| queue | bounded 필수 — unbounded 는 max pool 무효 (TPE-JDK21-C3) + Spring default `Integer.MAX_VALUE` 금지 (SF-TPTE-C1) | `planned` |
|
||
| rejection | `AbortPolicy` → `RejectedExecutionException` catch → structured log + `JOB_EXECUTOR_REJECTED` (error-codes.yaml: TRANSIENT_DEPENDENCY / 503 / retryable / retry_after 5s) + `executor.rejected.total` counter (metrics.yaml, alert p1) | registry 확정 / 코드 `planned` |
|
||
| saturation 관측 | `executor.saturation` gauge (metrics.yaml: p2 queue > 80% / p1 rejection > 0 for 1m) | registry 확정 / 코드 `planned` |
|
||
| shutdown knob | `setWaitForTasksToCompleteOnShutdown(true)` + `setAwaitTerminationSeconds(19)` (EXEC-CS-C2/C3 — default 는 즉시 interrupt, EXEC-CS-C1) | `planned` |
|
||
|
||
### 2. Async context propagation (D5, D6)
|
||
|
||
> **Trace**: In-scope "job id/correlationId 기준" → D5/D6 (SF-OBS-C1~C4, MICRO-CP-C1~C5, SS-CONC-C3/C4, SF-TPTE-C4/C5).
|
||
>
|
||
> - **구현 현황 + 권고 (2026-06-13)**: "TaskDecorator 미설정이면 fail" 을 현 구현은 *decorator 를 executor @Bean 의 필수 생성자 의존성으로 주입*해 강제 — 단 이는 **이 executor bean 하나만** 보호한다(다른 곳에 bare `ThreadPoolTaskExecutor` 를 또 등록하면 통과). 스켈레톤은 drift guardrail 이 핵심이므로 **전역 가드로 승격 권고**: `ScheduledJobOverlapPolicyTest`·`CleanArchitectureTest` 와 같은 결의 ArchUnit/startup 검증으로 "등록된 모든 `TaskExecutor` bean 은 context decorator 보유"를 강제. 승급 완료 시 이 항목의 `UNSUPPORTED_IMPL_DECISION` 성격 제거.
|
||
|
||
| 항목 | 명세 | 상태 |
|
||
|---|---|---|
|
||
| 연결 seam | `shared-contract` `dev.caskeleton.shared.concurrency` `DomainContextPropagator.wrap(Runnable)` 를 `AsyncContextTaskDecorator` 가 실제 호출 | `actually-implemented` |
|
||
| MDC copy 대상 | submit 시점 `MDC.getCopyOfContextMap()` 전체 스냅숏 — foundation 4키(`request_id`/`trace_id`/`correlation_id`/`tenant_id`) 보장 + span_id 동승(문자열). `user_principal` 은 `propagation: [none]` 미전파. mdc-keys.yaml foundation 과 정합 | `locally-verified` |
|
||
| 구현 캐리어 | 현재 = 수동 MDC whole-map decorator(`AsyncContextTaskDecorator`). upgrade 경로 = `ContextPropagatingTaskDecorator` (Spring 6.1+, SF-OBS-C1) — `io.micrometer:context-propagation` 도입 시 Observation scope 까지 전파. D5 "TaskDecorator 1개" 는 Composite 1개 등록으로 충족 | 현 `locally-verified` / upgrade `planned` |
|
||
| SecurityContext | `DelegatingSecurityContextTaskExecutor` wrapper 로 use-case 별 explicit opt-in (SS-CONC-C3/C4). `MODE_INHERITABLETHREADLOCAL` 금지(thread pool stale context). 기본 비전파 | opt-in `planned` / 기본 비전파 `actually-implemented` |
|
||
| **미설정 fail 강제** | 현: decorator = executor @Bean 필수 생성자 의존성(이 bean 한정 — `AsyncExecutorConfigTest` 검증). **구현됨 (2026-06-13)**: 전역 ArchUnit 규칙 `every_task_executor_bean_has_context_decorator`(production `TaskExecutor` @Bean 은 factory method 안에서 `setTaskDecorator(...)` 호출 필수, `getMethodCallsFromSelf` 검사) — decorator 없는 executor @Bean 추가 시 ArchUnit fail. delegating wrapper(예: `DelegatingSecurityContextTaskExecutor`)는 명시적 예외 등록 필요(rule javadoc) | 생성자 강제 `locally-verified` / 전역 가드 `actually-implemented` (`TaskExecutorDecoratorPolicyTest`) |
|
||
| 예외 경로 주의 | submit() 경로의 TaskDecorator 예외는 FutureTask 로 래핑되어 자동 전파 안 됨 (SF-TPTE-C5) — async exception test 는 execute()/submit() 두 경로 모두 검증 | `locally-verified` (2경로 테스트) |
|
||
|
||
### 3. Retry / DLQ vocabulary (D4)
|
||
|
||
> **Trace**: In-scope "retry/backoff 기준" → D4 (WAF-REL05-C1~C5, SPRING-RETRY-C1/C2, AWS-JITTER-C1~C5 사례).
|
||
>
|
||
> - **UNSUPPORTED_IMPL_DECISION**: retry carrier 선택 (Spring Retry vs Resilience4j vs 자체 구현) — Spring Retry 는 maintenance mode 진입 (SPRING-RETRY-C4: "superseded by Spring Framework 7"), 사용자 trade-off 로 carrier 확정 전까지 vocabulary 만 SSOT 로 고정.
|
||
> - **소비자-활성화 계약 (2026-06-13)**: `job.retry.total`/`job.dlq.total` recorder 와 `JOB_TIMEOUT`/`JOB_DEAD_LETTER` 코드는 이 branch 가 *제공*(SSOT)하되 *활성화*는 **소비자 branch** 책임 — 1차 소비자 = [[raw/branch-notes/feature-domain-event-outbox-contract]] publisher 의 발행 소진(exhaustion) 경로. 따라서 이 branch 에서 recorder 가 live-invoke 되지 않는 것은 "미구현"이 아니라 "소비자 대기". **rot 방지 가드 권고**: outbox branch 에 "발행 소진 시 `job.dlq.total` invoke + `JOB_DEAD_LETTER` emit" contract test 를 둬 recorder 가 영원히 안 불리는 dead-contract 차단 — 이 가드 부재가 현 vocabulary 경계의 *유일한 잔여 리스크*.
|
||
|
||
| 항목 | 명세 | 상태 |
|
||
|---|---|---|
|
||
| retry 실패 코드 | `JOB_TIMEOUT` (TRANSIENT_DEPENDENCY / retryable), DLQ 진입 = `JOB_DEAD_LETTER` (INTERNAL / retryable=false) — `OperationalError` enum + error-codes.yaml 교차검증(BackgroundJobErrorCodeContractTest). NOTE: `JOB_TIMEOUT` http_status=500(503 아님) | `actually-implemented` |
|
||
| metric recorder | `BackgroundJobMetrics` 가 `job.retry.total{job_name,outcome}`·`job.dlq.total{job_name}` recorder seam 제공 — metrics.yaml 정합. `executor.rejected.total`·`executor.saturation` 은 live-invoke(saturation 경로) | recorder seam `actually-implemented` / job.* live-invoke 는 소비자 대기 |
|
||
| **소비자 활성화** | recorder seam(`recordRetryOutcome`/`recordDeadLetter`)는 제공만 — 활성화 owner = outbox publisher 발행 소진 경로. **contract test 가드(outbox branch)**: 발행 N회 소진 → `job.dlq.total{job_name}` +1 & status=DEAD_LETTER & `JOB_DEAD_LETTER` emit | seam `actually-implemented` / 소비자 invoke + 가드 `planned` (outbox branch) |
|
||
| retryable 분류 | non-transient (권한/도메인 규칙/스키마 불일치) → retry 없이 즉시 DLQ (WAF-REL05-C3); retry 는 단일 레이어 원칙 (WAF-REL05-C4) — outbound adapter 의 Resilience4j retry 와 중첩 금지; non-idempotent 작업 retry 금지 (WAF-REL05-C5). **런타임 분류 로직은 carrier 와 함께 미구현** — 현재는 enum `retryable` 정적 플래그만 | 설계 확정 / 런타임 분류 `planned` (carrier 동반) |
|
||
| backoff 설정 | exp+jitter 는 라이브러리 default 아님 — Spring Retry 라면 `multiplier > 1.0` + `random=true` 명시 (SPRING-RETRY-C2); jitter 종류는 Full Jitter 사례 우세 (AWS-JITTER-C1~C4 — company-case-study, 공식 단정 금지) | `planned` (carrier 동반) |
|
||
|
||
### 4. Scheduler / multi-instance lock (D3)
|
||
|
||
> **Trace**: In-scope "scheduled job overlap 기준" → D3 (SK-PG-C2, OUTBOX-MIO-C4) + ca-tmpl 코드 ground truth.
|
||
>
|
||
> - **UNSUPPORTED_IMPL_DECISION**: 없음 — 본 sub-section 은 전부 registry/코드 실측 값.
|
||
|
||
| 항목 | 명세 | 상태 |
|
||
|---|---|---|
|
||
| @EnableScheduling | `app-bootstrap` `IdempotencyConfig` 에 실재 | `actually-implemented` |
|
||
| @Scheduled 선례 | `adapter-persistence` `IdempotencyReaper` (`fixedDelayString = "${ca-skeleton.idempotency.reaper-interval:PT10M}"`) | `actually-implemented` |
|
||
| multi-instance 강제 | `APP_MULTI_INSTANCE_ENABLED=true` 시 `StartupSafetyValidator.REQUIRED_MULTI_INSTANCE_BEANS` 의 `"distributedLockProvider"` bean 부재 → startup fail (exit 72) — 키 owner 는 [[raw/branch-notes/feature-env-driven-runtime-configuration]] D8. lock bean 제공 owner = [[raw/branch-notes/feature-distributed-lock-contract]] D1/D3 (A7 ✅ RESOLVED 2026-06-12) | validator `actually-implemented` / 본 branch consume |
|
||
| lock provider (delegated) | bean 이름 `distributedLockProvider` 존재만 전제(consume) — 제공 owner = [[raw/branch-notes/feature-distributed-lock-contract]] D1/D3 (기본 `JdbcLockRegistry`, ShedLock *배제*; port `DistributedLockPort`). 검증: `StartupSafetyValidator` bean-presence(exit 72) | owner branch `planned` / 본 branch consume 계약 확정 |
|
||
|
||
### 5. Graceful shutdown 예산 계층 (D8)
|
||
|
||
> **Trace**: In-scope "shutdown 중 job 처리 기준" → D8 (K8S-POD-LC-C1~C3, SB-GS-C2/C4/C5, EXEC-CS-C1~C4).
|
||
>
|
||
> - **UNSUPPORTED_IMPL_DECISION**: 1s cleanup margin 값 — 외부 권고 없음, 사용자 trade-off (executor 종료 후 잔여 리소스 정리 시간 확보).
|
||
|
||
```text
|
||
executor awaitTermination (≤19s)
|
||
< app shutdown budget (20s — parent project 운영 계약 소유)
|
||
≤ spring.lifecycle.timeout-per-shutdown-phase (SB-GS-C4; APP_SERVER_SHUTDOWN_TIMEOUT — owner: feature-env-driven-runtime-configuration D2, registry default 30s ⚠ §Audit A1)
|
||
< terminationGracePeriodSeconds (k8s default 30s — K8S-POD-LC-C1; 초과 시 SIGKILL — K8S-POD-LC-C2)
|
||
```
|
||
|
||
- 신규 요청 차단은 SmartLifecycle earliest phase 의 web server graceful stop 이 선행 (SB-GS-C2/C5) — executor await 는 그 이후 phase.
|
||
- in-flight job 이 19s 초과 → interrupt → **retry-on-next-startup** (멱등 전제, §Claims To Verify).
|
||
- **구현/검증 (2026-06-13)**: `AsyncExecutorConfig` 가 `setWaitForTasksToCompleteOnShutdown(true)` + `setAwaitTerminationSeconds(19)` 설정. 검증 2종 — (1) 정확값 핀 `AsyncExecutorConfigTest`(reflection: `awaitTerminationMillis`=19000, `waitForTasksToCompleteOnShutdown`=true — 19s vs 25s 같은 *수치* 계약 보증), (2) **행위 검증 `AsyncGracefulShutdownBehaviorTest`**: in-flight job 이 context close 중 예산 내 drain 완료 + shutdown 후 신규 submit → `RejectedExecutionException` & `JOB_EXECUTOR_REJECTED` 구조화 로그. 자체 관리 `AnnotationConfigApplicationContext` 사용(=동일 `SmartLifecycle`/`DisposableBean` shutdown 경로) — `@SpringBootTest` 는 ① 테스트 중 context close 시 post-test listener 실패 ② application.yml `${SPRING_PROFILES_ACTIVE}` 등 dotenv 의존(bootRun 전용) 때문에 부적합. `locally-verified`.
|
||
|
||
## Audit & Findings (2026-06-11 — /branch-spec ground-truth 대조)
|
||
|
||
> ca-tmpl registry/코드와 본 노트의 정합 감사 결과. 사용자 작성 결정은 수정하지 않고 권고만 기록.
|
||
|
||
- **A1. `SHUTDOWN_BUDGET_DRIFT`** — 본 노트 D8 은 "container-runtime 의 app shutdown **20s**" 를 전제하나, ca-tmpl `docs/registries/env-keys.yaml` 의 `APP_SERVER_SHUTDOWN_TIMEOUT` default 는 **30s** (owner: [[raw/branch-notes/feature-env-driven-runtime-configuration]] D2, validation: `spring_duration_shorthand_le_termination_grace`). 19s await 는 어느 쪽 기준으로도 안전하지만, "20s" 의 출처(parent project 운영 계약)와 registry default 30s 의 관계를 owner branch 와 명문화 권고. 자동 수정하지 않음 (사용자 결정 영역).
|
||
- **A2. `OUT_OF_BRANCH_SCOPE` 권고 (D9~D12)** — outbox publisher 메커니즘 선택·대안 비교(D9~D12)는 registry 상 outbox 계약 owner 인 [[raw/branch-notes/feature-domain-event-outbox-contract]] 의 결정 영역 (outbox.* metrics 3종 + `outboxLeaderElection` bean 모두 그 branch 소유). 본 branch 의 소유는 retry/DLQ **vocabulary** (D2) 까지. D9~D12 와 §외부 근거/대안 조사의 outbox 부분은 사용자 작성분이므로 보존하되, owner branch 로의 이관을 권고. outbox 노트가 이미 본 branch D4 를 cross-reference 중 (양방향 확인됨).
|
||
- **A3. `REGISTRY_CONFIRMED`** — 본 노트의 계약 값 전수 registry 대조 통과 (invent 없음): `JOB_EXECUTOR_REJECTED`/`JOB_TIMEOUT`/`JOB_DEAD_LETTER` (error-codes.yaml, owner = 본 branch), `APP_ASYNC_EXECUTOR_CORE_SIZE/MAX_SIZE/QUEUE_CAPACITY` default 10/50/200 (env-keys.yaml — 노트 D7 수치와 일치), `executor.saturation`/`executor.rejected.total`/`job.retry.total`/`job.dlq.total` (metrics.yaml), mdc-keys.yaml foundation 6키 (D6 의 4+2 분류와 일치 — span_id `source: observation_context`, user_principal `propagation: [none]`).
|
||
- **A4. `RUNBOOK_MISSING`** — error-codes.yaml 이 참조하는 `runbook://job/executor-rejected`·`runbook://job/timeout`·`runbook://job/dead-letter` 의 실제 파일이 `docs/runbooks/` 에 부재 — `documented-only`. 구현 단계에서 작성 필요.
|
||
- **A5. `CLAIM_PREFIX_FIX`** — D5 행에 일시 기재됐던 `SPRING-OBS-C*` 표기를 실제 raw 파일 prefix `SF-OBS-C1~C4` 로 정정 (2026-06-11 자동조사 중 발생한 표기 불일치).
|
||
- **A6. `IMPLEMENTATION_STATUS`** — src grep 실측: TaskDecorator / ThreadPoolTaskExecutor bean / `awaitTermination` / ShedLock wiring / outbox 클래스 전부 **미구현** (`planned`). 실구현은 `@EnableScheduling` + `IdempotencyReaper` + `StartupSafetyValidator` 뿐. 본 노트의 계약은 전체적으로 documented-only 단계 — `actually-implemented` 로 표현 금지.
|
||
- **A7. `LOCK_BEAN_OWNER_UNRESOLVED`** (coverage-auditor 2026-06-11) — `distributedLockProvider` bean 의 제공 결정이 어느 branch 에도 없음. ca-tmpl `StartupSafetyValidator` 주석은 [[raw/branch-notes/feature-runtime-health-lifecycle-contract]] 를 가리키나 그 노트는 "consume only" 로 자기 서술. owner 를 확정해 해당 branch 결정으로 등록하기 전까지 본 branch 는 *bean 존재를 전제로 consume* 만 한다 (multi-instance contract test 는 bean 부재 시 fail 로 이 미확정을 노출).
|
||
- **✅ RESOLVED (2026-06-12)** — owner 확정: [[raw/branch-notes/feature-distributed-lock-contract]] D1 이 `distributedLockProvider` bean 계약을 소유 (기본 provider = Spring Integration `JdbcLockRegistry`, 그 branch D3). 본 branch 는 consume 관계 유지. ✅ 본 §테스트 계약·§구현 가이드 4 의 FQCN 표기를 `distributedLockProvider` bean(JdbcLockRegistry 기반, port `DistributedLockPort`) 기준으로 **갱신 완료 (2026-06-13)** — ShedLock `net.javacrumbs.shedlock.core.LockProvider` 타입 표기 폐기.
|
||
- **A8. `STALE_OWNER_FIXED`** (coverage-auditor 2026-06-11) — §엣지·의존 의 MDC 어휘 위임 대상을 `feature-log-management-contract`(consumer 오기) → `feature-operational-error-observability-foundation`(mdc-keys.yaml L4 SSOT 자기 선언) 으로 정정.
|
||
|
||
## 엣지·실패·의존
|
||
|
||
- **실패·엣지 경로**:
|
||
- shutdown 중 신규 job enqueue → REJECTED 상태 + `JOB_EXECUTOR_REJECTED` (§테스트 계약과 동일 기대 동작).
|
||
- in-flight job 19s 초과 → interrupt → retry-on-next-startup (멱등 전제 — 미검증, §Claims To Verify).
|
||
- interrupt 에 반응하지 않는 blocking call (JDBC 등) → awaitTermination 초과 → SIGKILL 노출 경로 (K8S-POD-LC-C2).
|
||
- queue drain: `waitForTasksToCompleteOnShutdown(true)` 는 queue 잔여 task 까지 전부 실행 (EXEC-CS-C2/C4) — queue=200 × 평균 job 시간이 19s 를 초과하는 burst 시나리오의 기대 동작 미정의 (§Claims To Verify).
|
||
- saturation: queue full + max pool 도달 → `RejectedExecutionException` — fire-and-forget `@Async` 호출이면 예외 소실 위험 → async exception 계약으로 흡수 필수.
|
||
- submit() 경로 예외는 FutureTask 에 래핑되어 uncaught handler 미통과 (SF-TPTE-C5) — async exception test 는 execute()/submit() 두 경로 모두 검증.
|
||
- non-transient 예외 → retry 없이 즉시 DLQ (WAF-REL05-C3) — retryable 분류기 누락 시 무한 재시도가 아니라 분류 실패로 fail 해야 함.
|
||
- **다른 계약 의존**:
|
||
- [[raw/branch-notes/feature-env-driven-runtime-configuration]] 의 D2(`APP_SERVER_SHUTDOWN_TIMEOUT`)·D8(`APP_MULTI_INSTANCE_ENABLED`) 에 의존 — 본 branch 는 consume. shutdown timeout default 변경 시 19s 예산 재검토, multi-instance 키 변경 시 lock 강제 테스트 영향 (⚠ A1 drift).
|
||
- [[raw/branch-notes/feature-domain-event-outbox-contract]] — 본 branch 의 retry/DLQ vocabulary (D2, D4) 를 consume. vocabulary 변경 시 비차단 전파 알림 필요 (consistency-contract).
|
||
- [[raw/branch-notes/feature-operational-error-observability-foundation]] — MDC key 어휘(mdc-keys.yaml L4 SSOT 자기 선언) + error Category enum owner (`TRANSIENT_DEPENDENCY`/`INTERNAL` 은 그 branch 계약의 재사용). foundation 키 변경 시 D5/D6 의 copy 대상 재산정. ([[raw/branch-notes/feature-log-management-contract]] 는 같은 어휘의 consumer — owner 아님, coverage-auditor STALE_OWNER 정정 2026-06-11)
|
||
- `distributedLockProvider` bean — owner = [[raw/branch-notes/feature-distributed-lock-contract]] D1/D3 (A7 ✅ RESOLVED, 기본 `JdbcLockRegistry`). 본 branch 는 multi-instance 시 그 bean 존재를 전제(consume); ca-tmpl `StartupSafetyValidator` 주석의 runtime-health 표기는 stale → owner branch 가 코드 주석 갱신 예정.
|
||
- [[raw/project-notes/ca-skeleton-operational-contract]] — 20s app shutdown 예산의 소유자. 예산 변경 시 D8 의 19s 도출 무효.
|
||
|
||
## 테스트 계약
|
||
|
||
- async exception이 조용히 삼켜지면 실패.
|
||
- executor rejection이 structured log 없이 발생하면 실패.
|
||
- scheduled job overlap 기준이 없으면 실패.
|
||
- shutdown 중 job 정책: in-flight job 은 await 예산(≤19s) 내 완료, 초과분은 interrupt 후 retry-on-next-startup. 측정 방법(2026-06-13 정정 — 내장 메커니즘 채택): `setWaitForTasksToCompleteOnShutdown(true)` + `setAwaitTerminationSeconds(19)` 적용 검증(현 `AsyncExecutorConfigTest` 가 `awaitTerminationMillis==19000` reflection 검증). **권고 보강**: reflection(설정값)을 *행위* 검증으로 승급 — 느린 job 제출 → context close → (a) job 이 예산 내 완료, (b) 종료 후 신규 제출은 `JOB_EXECUTOR_REJECTED` 로 거부. (이전판의 `ApplicationListener<ContextClosedEvent>` + ListAppender 명세는 ThreadPoolTaskExecutor 내장 메커니즘 채택으로 폐기 — custom listener 안 씀.)
|
||
- multi-instance lock = **delegated → [[raw/branch-notes/feature-distributed-lock-contract]]** (D1/D3, 기본 provider = Spring Integration `JdbcLockRegistry`; ShedLock 은 그 branch D3 에서 *배제*). 본 branch 는 *적용처*(D3 scheduler/outbox)로서 provider 존재를 전제로 consume 만. 측정 방법(2026-06-13 정정): `APP_MULTI_INSTANCE_ENABLED=true` 시 ca-tmpl `StartupSafetyValidator` 가 bean 이름 `distributedLockProvider` 존재를 강제(부재 시 exit 72) — 기존 `StartupSafetyValidatorTest` 가 검증. (이전판의 `net.javacrumbs.shedlock.core.LockProvider` 타입 기준은 ShedLock 가정 시절 표기 — owner D3 가 JdbcLockRegistry 로 확정해 폐기.)
|
||
|
||
## Async Context Propagation Contract
|
||
|
||
> 2026-06-13 구현 정합 갱신 — §구현 기록·D6 와 일치하도록 정정.
|
||
|
||
- `TaskDecorator` 1개를 ThreadPoolTaskExecutor 에 등록해 caller→worker thread 로 복사한다:
|
||
- MDC: submit 시점 **전체 스냅숏 복사**(`MDC.getCopyOfContextMap()`) — foundation 4키(`request_id`/`trace_id`/`correlation_id`/`tenant_id`) 보장, span_id 는 그 시점 MDC 에 있으면 문자열로 동승. worker 종료 시 대칭 복원(풀 스레드 MDC bleed 방지).
|
||
- Observation **scope** 는 전파하지 않음(context-propagation 라이브러리 미반입). parent-span linkage 필요 시 `ContextPropagatingTaskDecorator` 로 upgrade.
|
||
- SecurityContext / principal: **기본 비전파**. 필요 use case 만 `DelegatingSecurityContextTaskExecutor` 로 explicit opt-in.
|
||
- executor 등록 시 TaskDecorator 미설정이면 fail — 현재는 decorator 를 executor @Bean 의 필수 생성자 의존성으로 강제(이 bean 한정). 전역 강제는 §구현 가이드 2 의 ArchUnit 가드 권고 참조.
|
||
- 테스트 계약:
|
||
- @Async 메서드 안에서 `MDC.get("request_id")`/`trace_id`/`correlation_id`/`tenant_id` 가 caller 와 동일.
|
||
- **principal 은 worker 로 전파되지 *않는다*** (negative 검증) — opt-in executor 사용 시에만 전파.
|
||
|
||
## 검증해야 할 주장
|
||
|
||
| Claim | Why uncertain | How to verify | Status |
|
||
|---|---|---|---|
|
||
| TaskDecorator 1개로 MDC 4-key (request_id/trace_id/correlation_id/tenant_id) + Observation context 가 caller→worker 정확히 전파됨 | 공식 메커니즘 근거는 확보 (SF-OBS-C1/C2, MICRO-CP-C1) — 그러나 공식 doc 은 메커니즘만 보장, ca-tmpl 구성에서의 실 전파는 미검증 | `@Async` 메서드에서 `MDC.get("request_id")`, Micrometer `Observation.getCurrent()` 가 caller thread 와 동일한지 contract test | `needs-confirmation` |
|
||
| SLF4J-Micrometer tracing bridge 활성 시 span_id 가 worker thread MDC 에 자동 기입됨 (D6 전제) | bridge 공식 doc 인용 미확보 — SF-OBS-C4 는 이 페이지 범위 밖이라 명시 | bridge 활성 상태에서 `@Async` 내 `MDC.get("span_id")` non-null contract test + bridge 공식 doc 추가 아카이브 | `needs-confirmation` |
|
||
| Executor pool default (core=10, max=50, queue=200) + AbortPolicy 가 ca-tmpl 부하 프로파일에 적합 | 정량 trade-off 의 외부 reference 부재 (D7 — 구조만 공식 확보) | 부하테스트 (k6 / JMeter) 로 saturation 임계 측정 + rejection log 확인 | `planned` |
|
||
| Graceful shutdown 19s 내 executor await termination 이 실제 in-flight job 완료 보장 | k8s/Spring 공식 메커니즘 근거 확보 (K8S-POD-LC-C1/C2, EXEC-CS-C2/C3) — 잔여: job p99 실행 시간 < 19s 미측정 + queue drain 시간(queue=200 × 평균 job 시간) 미계산 | `ApplicationListener<ContextClosedEvent>` 등록 + ListAppender 로 shutdown phase reject log 검증 + job p99 측정 | `planned` |
|
||
| Multi-instance 환경에서 ShedLock 또는 DB advisory lock 이 publisher claim consistency 보장 | SKIP LOCKED 는 lock contention 회피만 보장 (`SK-PG-C2`), 순서/claim consistency 별도 | `@TestPropertySource("app.multi-instance.enabled=true")` 테스트에서 `LockProvider` bean 존재 verify | `needs-confirmation` |
|
||
| Exponential backoff + jitter + max=3 + DLQ 가 ca-tmpl 도메인 retry 성공률에 적합 | max=3 은 Spring Retry default 와 일치 (SPRING-RETRY-C1) 하나 ca-tmpl 도메인 적합성은 미측정 (WAF-REL05-C2 의 use-case 별 조정 권고) | DLQ 진입률 metric (`job.dlq.total`) 측정 + max attempts 조정 실험 | `planned` |
|
||
| Debezium CDC 가 본 프로젝트 lag SLO 충족 (수 초 lag 허용 가정 깨질 때) | `OUTBOX-DBZ-C2` 는 "polling 비용 회피" 까지만 보장, lag 수치는 침묵 | Debezium PoC + WAL lag metric 측정 (활성화 시) | `needs-confirmation` |
|
||
| SKIP LOCKED polling 의 순서 보장 안 됨이 ca-tmpl 도메인에 허용 가능 | `SK-PG-C2` 의 "inconsistent view" 경고 | partition key 별 단일 publisher 시 순서 보장되는지 contract test + 도메인 검토 | `needs-confirmation` |
|
||
|
||
## 관심사 커버리지 (coverage-auditor 자동 생성 — 2026-06-11)
|
||
|
||
> `/coverage` 생성물 — 손으로 유지하지 않는다. 기준: `rules/coverage-gate.md`. 판정: **Covered** (Blocking 0 / Should-fix 2 — A7·A8 로 처리 / Advisory 1 — A4 runbook).
|
||
|
||
| 관심사 | 상태 | owner | 심각도 | 근거 |
|
||
|--------|------|-------|--------|------|
|
||
| async exception handling (삼켜진 예외 금지, structured log + metric + runbook link) | covered-here | — | — | Decisionized Work Items "async exception" 행; D5; 테스트 계약 1항 |
|
||
| executor saturation/rejection (bounded queue 강제, AbortPolicy default, rejection log) | covered-here | — | — | D7; §구현 가이드 1; `executor.rejected.total` (metrics.yaml, owner = 본 branch) |
|
||
| scheduled job overlap (single-instance 기본, multi-instance 시 distributed lock) | covered-here | — | — | D3; §구현 가이드 4 |
|
||
| job id / correlationId (MDC 4-key + Observation context) | covered-here | — | — | D5, D6; §구현 가이드 2; mdc-keys.yaml `propagation: [async]` 대조 |
|
||
| retry / backoff (exp+jitter, max=3, DLQ after exhausted) | covered-here | — | — | D4; §구현 가이드 3; `JOB_TIMEOUT`/`JOB_DEAD_LETTER` (error-codes.yaml) |
|
||
| shutdown 중 job 처리 (await ≤ 19s, retry-on-next-startup) | covered-here | — | — | D8; §구현 가이드 5 |
|
||
| background failure logging (log/metric/alert 핵심 계약) | covered-here | — | — | Decisionized Work Items "async exception" 행; §진행 중 메모; JOB_* 3코드의 runbook_link |
|
||
| `APP_ASYNC_EXECUTOR_*` env 키 3종 / JOB_* error 코드 3종 / executor.*·job.* 메트릭 4종 (registry 본 branch 소유분) | covered-here | — | — | §Audit A3 (registry 전수 대조) |
|
||
| `APP_MULTI_INSTANCE_ENABLED`·`APP_SERVER_SHUTDOWN_TIMEOUT` env 키 정의 | delegated | [[raw/branch-notes/feature-env-driven-runtime-configuration]] (D8, D2) | OK | §엣지·의존 위임 링크 |
|
||
| outbox publisher 메커니즘 (D9~D12) | delegated | [[raw/branch-notes/feature-domain-event-outbox-contract]] | OK | §Audit A2 이관 권고 + 양방향 cross-ref 확인 |
|
||
| `distributedLockProvider` bean 제공 결정 | delegated | [[raw/branch-notes/feature-distributed-lock-contract]] (D1) | OK | §Audit A7 ✅ RESOLVED 2026-06-12 — [[raw/branch-notes/feature-distributed-lock-contract]] D1/D3 |
|
||
| MDC key 어휘 (mdc-keys.yaml) | delegated | [[raw/branch-notes/feature-operational-error-observability-foundation]] | OK (A8 정정 완료) | mdc-keys.yaml L4 SSOT 자기 선언 |
|
||
|
||
## 마주친 문제
|
||
|
||
- 2026-06-13: `:app-bootstrap:test` 전체 실행 시 선재(pre-existing) ArchUnit 실패 1건 발견 — `CleanArchitectureTest#outbound_adapter_method_returns_only_domain_or_primitives` (`OutboundHttpSettings.retry()`/`circuitBreaker()` 중첩 record accessor 가 B7 규칙 위반). `git stash -u` baseline 에서도 동일 실패 → 본 background-job 작업과 무관, owner 는 feature-outbound-http-client-baseline. 상세·권고 해결 → [[raw/errors/archunit-b7-configprops-nested-record-accessor-outbound-2026-06-13]].
|
||
|
||
## 묶음
|
||
|
||
<!-- GENERATED: sources:start -->
|
||
- [[raw/company-tech-blogs/outbox-confluent-kafka-connect-smt]]
|
||
- [[raw/company-tech-blogs/outbox-netflix-domain-events-cdc]]
|
||
- [[raw/company-tech-blogs/outbox-wix-engineering-debezium]]
|
||
- [[raw/company-tech-blogs/outbox-woowahan-techblog-pattern]]
|
||
- [[raw/company-tech-blogs/retry-aws-exponential-backoff-and-jitter]]
|
||
- [[raw/official-docs/dual-write-antipattern-microservices-io]]
|
||
- [[raw/official-docs/event-sourcing-vs-outbox-microservices-io]]
|
||
- [[raw/official-docs/jdk21-threadpoolexecutor-javadoc]]
|
||
- [[raw/official-docs/kubernetes-pod-lifecycle-termination]]
|
||
- [[raw/official-docs/lock-shedlock-readme]]
|
||
- [[raw/official-docs/micrometer-context-propagation-purpose-thread-local-accessor]]
|
||
- [[raw/official-docs/outbound-resilience4j-vs-spring-retry]]
|
||
- [[raw/official-docs/outbox-debezium-official-docs]]
|
||
- [[raw/official-docs/outbox-skip-locked-microservices-io]]
|
||
- [[raw/official-docs/retry-aws-well-architected-rel05-bp03]]
|
||
- [[raw/official-docs/retry-spring-retry-readme-backoff-defaults]]
|
||
- [[raw/official-docs/skip-locked-postgres-docs]]
|
||
- [[raw/official-docs/spring-boot-graceful-shutdown-reference]]
|
||
- [[raw/official-docs/spring-boot-task-execution-scheduling-reference]]
|
||
- [[raw/official-docs/spring-executor-configuration-support-javadoc]]
|
||
- [[raw/official-docs/spring-framework-observability-context-propagating-task-decorator]]
|
||
- [[raw/official-docs/spring-framework-threadpooltaskexecutor-javadoc]]
|
||
- [[raw/official-docs/spring-security-concurrency-delegating-security-context-executor]]
|
||
- [[raw/official-docs/spring-transactional-event-listener]]
|
||
<!-- GENERATED: sources:end -->
|
||
|
||
<!-- GENERATED: interviews:start -->
|
||
- [[raw/interviews/async-executor-saturation-context-propagation-2026-06-13]]
|
||
<!-- GENERATED: interviews:end -->
|
||
|
||
<!-- GENERATED: errors:start -->
|
||
- [[raw/errors/archunit-b7-configprops-nested-record-accessor-outbound-2026-06-13]]
|
||
<!-- GENERATED: errors:end -->
|
||
|
||
<!-- GENERATED: blog-topics:start -->
|
||
- [[raw/blog-topics/spring-async-taskdecorator-bounded-executor-saturation-shutdown-2026-06-13]]
|
||
<!-- GENERATED: blog-topics:end -->
|
||
|
||
> 2026-06-13 실 구현 단계에서 파생 자료 누적. 아래 errors/interview/blog-topics 는 본 branch 로 upward link 되어 있다.
|
||
|
||
### 오류 기록 (본 feature 작업 중 발생)
|
||
|
||
- [[raw/errors/archunit-b7-configprops-nested-record-accessor-outbound-2026-06-13]] — 전체 테스트 실행 중 발견한 선재 B7 위반(adapter-outbound 소유, 본 작업 무관).
|
||
|
||
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
|
||
|
||
- [[raw/interviews/async-executor-saturation-context-propagation-2026-06-13]] — bounded executor·saturation·MDC/도메인 컨텍스트 전파·graceful shutdown·retry metric cardinality 6문항.
|
||
|
||
### Blog topics (이 작업에서 나온 글감)
|
||
|
||
- [[raw/blog-topics/spring-async-taskdecorator-bounded-executor-saturation-shutdown-2026-06-13]] — @Async TaskDecorator + bounded executor + saturation/shutdown 운영 계약.
|
||
|
||
## 관련 일일 노트
|
||
|
||
> 이 브랜치를 작업한 날짜들. 양방향 nav 유지.
|
||
|
||
- (없음 — documented-only 단계. 실 구현 착수 시 `[[raw/daily-notes/YYYY-MM-DD]]` 형식으로 연결)
|
||
|
||
## 완료 후 정리
|
||
|
||
- PR 링크: (미생성 — 사용자가 직접 커밋/PR)
|
||
- 리뷰 메모: 3단 리뷰 체인 통과 — ca-architect-sentinel(PASS, 신규 위반 0), ca-spec-reviewer(22/22, plan 텍스트 2건 정정), ca-quality-reviewer(important 1 + minor 1 수정: tautological MDC-clear 테스트 보강, Supplier import).
|
||
- 머지 결과 / 배포 환경: (미머지 — 구현 git 브랜치 `feature/domain-event-outbox-contract`)
|
||
- **wiki 추출 대상** (verified만, `wiki/projects/`로만 추출):
|
||
- `actually-implemented` 항목: JOB_* error code 3종(registry 교차검증), `AsyncExecutorConfig` bounded executor + AbortPolicy saturation, `BackgroundJobMetrics` 4 메트릭 recorder, `ScheduledJobOverlapPolicyTest` overlap 규칙, runbook 3종.
|
||
- `locally-verified` 항목: `AsyncContextTaskDecorator` MDC+도메인 컨텍스트 전파(submit-time 캡처·대칭 복원), D8 awaitTermination 19s, async 예외 2경로(submit/execute) 미삼킴.
|
||
- `prod-verified` 항목: (없음 — 로컬 검증까지)
|
||
- **추출하지 않을 항목** (planned / documented-only / abandoned): retry **carrier**(Spring Retry/Resilience4j/자체 — §3 UNSUPPORTED_IMPL, vocabulary 만 고정), executor 수치 core=10/max=50/queue=200 부하 적합성(`planned`, 부하테스트 미실시), Observation **scope** 전파(라이브러리 미반입 — MDC 문자열까지만), SecurityContext principal 자동 전파(opt-in 문서화만).
|