--- title: branch / feature-distributed-lock-contract source_type: branch-note status: raw branch: feature-distributed-lock-contract parent_branch: related_projects: [ca-skeleton] governing_docs: [raw/project-notes/ca-skeleton-operational-contract] tags: [branch, ca-skeleton, distributed-lock, advisory-lock, lock-registry] created: 2026-06-12 target_merge: status_label: in-progress id: BR-CA-SKELETON-OPERATIONAL-CONTRACT-052 kind: project-work-item project: ca-skeleton-operational-contract work_item: WI-CA-SKELETON-OPERATIONAL-CONTRACT-052 inherits: [DEC-CA-SKELETON-OPERATIONAL-CONTRACT-SCHEDULER-LOCK-001@1, DEC-CA-SKELETON-OPERATIONAL-CONTRACT-TRANSACTION-001@1, DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-DATABASE-001@1] refines: [] overrides: [] depends_on: [WI-CA-SKELETON-OPERATIONAL-CONTRACT-004, WI-CA-SKELETON-OPERATIONAL-CONTRACT-025, WI-CA-SKELETON-OPERATIONAL-CONTRACT-024, WI-CA-SKELETON-OPERATIONAL-CONTRACT-017, WI-CA-SKELETON-OPERATIONAL-CONTRACT-018] contract_packet: 1 contract_packet_sha256: 174d92e4290cde20c264638b526e3447c27c19eca0eab92d8023a03a66627754 --- # branch: feature-distributed-lock-contract > Layer: `raw/branch-notes/` — 단일 브랜치의 **TODO·결정·진행 기록**. 머지/종료 후 verified 결과는 `/ingest`로 `wiki/projects/`에 추출. 원본은 raw에 영구 보관. > `status_label`: `in-progress` | `review` | `merged` | `abandoned` ## 부모 (필수) - **Parent project (canonical SSOT)**: [[raw/project-notes/ca-skeleton-operational-contract]] > ca-skeleton 은 별도 root branch 없이 project-note 가 SSOT 역할. 본 feature branch 는 project-note §29.E 신규 branch 권고 #9 (`feature-distributed-lock-contract` — "Redisson / DB advisory lock + 트랜잭션 commit 정합") 영역의 결정/근거/금지 사항을 정제한다. 형제 branch (같은 부모의 다른 자식 — lock 인접 영역): - [[raw/branch-notes/feature-background-job-async-contract]] — scheduler/outbox 의 lock *적용처* owner (D3). 본 branch 의 `distributedLockProvider` bean 을 consume. - [[raw/branch-notes/feature-cache-consistency-contract]] — cache stampede lock (Redisson RLock) owner (D3/D4) - [[raw/branch-notes/feature-env-driven-runtime-configuration]] — `APP_MULTI_INSTANCE_ENABLED` flag + `StartupSafetyValidator` presence 강제 owner (D8) ## 브랜치 계약 패킷 - **생성 시 프로젝트 개정**: `1` - **패킷 스키마**: `contract_packet: 1` - **완료 조건**: lock provider·lease·transaction commit ordering과 failure test가 명시된다 ### 상속한 프로젝트 결정 | 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-TRANSACTION-001@1` | application은 Spring transaction annotation 대신 TransactionPort 또는 TransactionalUseCaseRunner를 사용한다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-operational-contract]] | | `DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-DATABASE-001@1` | database는 PostgreSQL 16 단일 stack이다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-operational-contract]] | ### 브랜치 지역 결정 > 기존 branch-local 결정은 아래 `## Decision Evidence Map / 결정-근거 매핑`의 D-row가 소유하며 이 packet에서 복제하지 않는다. | Decision ID | Decision | Relation | Supporting Claims | Status | |---|---|---|---|---| ### 선언한 예외 | Override ID | Overrides | Reason | Approval | Status | |---|---|---|---|---| ## 목표 multi-instance 배포(`APP_MULTI_INSTANCE_ENABLED=true`) 시 ca-tmpl `StartupSafetyValidator` 가 presence 를 강제하는 5개 instance-coordination bean 중 **`distributedLockProvider` 만 제공 결정의 owner branch 가 없었다** — [[raw/branch-notes/feature-background-job-async-contract]] §Audit **A7 `LOCK_BEAN_OWNER_UNRESOLVED`** (2026-06-11 coverage-auditor): ca-tmpl 코드 주석은 runtime-health 를 가리키나 그 노트는 "consume only" 자기 서술, 어느 branch 도 *bean 을 누가 어떤 메커니즘으로 제공하는지* 결정하지 않음. 본 branch 가 그 owner 가 되어 다음을 결정한다: **general-purpose 분산 락 제공 계약** — 메커니즘 선택(DB 기반 vs Redis 기반), port 추상화, **트랜잭션 commit 정합**(lock 해제 vs DB commit 순서), lease/timeout 계약, 실패 매핑, 정적 강제 요구. - 이슈: parent project §29.E row #9 / background-job §Audit A7 - PR: (없음 — 계약 단계) ## 범위 ### 포함 범위 - `distributedLockProvider` bean 계약의 SSOT ownership (A7 해소) — bean 이름은 ca-tmpl `StartupSafetyValidator.REQUIRED_MULTI_INSTANCE_BEANS` 기존 값 재사용 - general-purpose 분산 락 **메커니즘 선택** (PG advisory lock / ShedLock / Spring Integration LockRegistry / Redisson 비교) - **port 추상화** — domain/application 층에서 lock client 직접 사용 금지 - **트랜잭션 commit 정합** — lock 해제와 DB commit 의 순서 불변식 - **lease / timeout 획득 계약** — 무한 blocking 금지, 잔존 lock 자동 만료 - lock 획득 실패의 error code / metric **신규 제안** (registry-governance 절차 경유) - 정적 강제(ArchUnit) **요구사항** 등록 — rule 호스팅은 `feature-architecture-enforcement-rules` 에 위임 - multi-instance contract test 계약 (bean presence + 정합) ### 제외 범위 > 의도적으로 제외한 것. 면접 등에서 "이건 범위에 없었습니다"라고 답할 근거. - scheduler/outbox 의 lock **적용 정책** — [[raw/branch-notes/feature-background-job-async-contract]] D3 소유 (본 branch 는 provider 만 공급) - cache stampede 방지 lock — [[raw/branch-notes/feature-cache-consistency-contract]] D3/D4 소유 (Redisson RLock + `CACHE_STAMPEDE_LOCK_TIMEOUT`) - `APP_MULTI_INSTANCE_ENABLED` flag 정의와 `StartupSafetyValidator` 집행 — [[raw/branch-notes/feature-env-driven-runtime-configuration]] D8 소유 - distributed rate limiter (`distributedRateLimiter` bean) — `feature-rate-limit-idempotency-contract` 영역 - migration runner lock (`migrationStartupRunner` bean) — `feature-migration-startup-contract` 영역 - **fencing token 도입** — 미도입 결정 (D6). correctness 는 DB 제약으로 보장 - tenant 별 lock namespace — `feature-tenant-context-policy` 활성화 전까지 미정의 ## 근거 (필수, 최소 1개+) > 이 branch의 구현·설계 결정의 **근거가 되는 외부 자료**. 공식 문서·대기업 기술 블로그·강의 등 raw 자료를 인용. 같은 자료가 여러 결정의 근거면 결정 표시와 함께 여러 번 등장 가능. | Source | 정당화하는 결정 | |---|---| | [[raw/official-docs/lock-postgres-advisory-locks]] | ca-tmpl `distributedLockProvider` 의 default 메커니즘으로 PostgreSQL advisory lock 검토 — session-level vs transaction-level 해제 시맨틱(PG-ADV-C2, PG-ADV-C3)이 "lock 해제 vs DB commit 순서 정합" 결정(D4)의 1차 근거 + session-level 배제(D3)·non-blocking try 변형(D5) 근거 | | [[raw/official-docs/lock-spring-integration-lock-registry]] | ca-tmpl `distributedLockPort` 추상화의 reference 구현 후보로서 Spring Integration `LockRegistry`/`JdbcLockRegistry` 평가 — `java.util.concurrent.locks.Lock` 호환 추상화 + JDBC/Redis/Zookeeper/DynamoDB provider 교체 가능성이 "port 추상화 + provider 교체" 결정의 근거 (SI-LOCK-C1, SI-LOCK-C2, SI-LOCK-C3) + lease 갱신/만료 예외 계약(D5 — SI-LOCK-C4, SI-LOCK-C5) | | [[raw/official-docs/lock-shedlock-readme]] | ShedLock 평가(D3 배제) — 용도 정의 "scheduled tasks at most once"(SHEDLOCK-C1) + `lockAtMostFor`/`lockAtLeastFor` lease 시맨틱(D5 참조 원리 — SHEDLOCK-C3, SHEDLOCK-C4) + clock 동기화 가정(D6 한계 방증 — SHEDLOCK-C5) | | [[raw/official-docs/lock-shedlock-issue-899-non-scheduler-use]] | ShedLock 을 general-purpose lock 으로 쓰지 않는 결정(D3)의 직접 근거 — maintainer 가 generic lock 공식 선언 거부(SHEDLOCK-899-C1) + 획득 실패 시 *skip*(대기 없음) 시맨틱이라 blocking 계약과 불일치(SHEDLOCK-899-C2) | | [[raw/company-tech-blogs/lock-subskribe-advisory-lock-distributed-consensus]] | PostgreSQL advisory lock 의 production 사용 사례 — 추가 인프라 없이 DB 만으로 distributed mutual exclusion 을 달성한 사례(SUBSKRIBE-LOCK-C1) + "optimistic variant(try-lock) 만 사용, pessimistic blocking 은 비권장" 운영 교훈(SUBSKRIBE-LOCK-C2)이 `distributedLockProvider` 메커니즘 비교의 사례 근거 (공식 best practice 아님 — 사례/관점으로만 취급) | | [[raw/official-docs/cache-redisson-rlock-vs-setnx]] | Redis 기반 대안(D3 의 Redis-활성 분기) — Redisson RLock 의 j.u.c.Lock 호환 + watchdog(LOCK-C3, `needs-confirmation`), TTL 의 deadlock 회피 역할(D5 — LOCK-C2), efficiency vs correctness lock 분리(D6 — LOCK-C4). cache branch 와 공유 raw | 근거 자료가 raw에 아직 없다면 먼저 `raw-source-template` 또는 `lecture-note-template` 으로 raw에 등록한 뒤 여기서 링크. ## TODO 각 항목 옆에 증거 등급 표기: `actually-implemented` | `locally-verified` | `prod-verified` | `documented-only` | `planned` | `needs-confirmation` - [x] `OperationalError.LOCK_ACQUISITION_TIMEOUT` enum 상수 추가 (shared-contract) — 등급: `actually-implemented` / `locally-verified` (D7, 2026-06-13 이전 세션) - [x] lock port 인터페이스 3종 정의 (application-core): `DistributedLockPort`, `DistributedLock`, `LockAcquisitionTimeoutException` — 등급: `actually-implemented` / `locally-verified` (D2/D4/D5/D6, 2026-06-13) - [x] `LockAcquisitionTimeoutExceptionTest` + `DistributedLockPortContractTest` (application-core) — 등급: `locally-verified` (10/10 pass, 2026-06-13) - [x] `LockSettings` `@ConfigurationProperties("ca-skeleton.lock")` record (adapter-persistence) — 등급: `actually-implemented` / `locally-verified` (D3/D5, 2026-06-13) - [x] `LockRegistryDistributedLockAdapter implements DistributedLockPort` (adapter-persistence) — 등급: `actually-implemented` / `locally-verified` (D3/D4/D5, 2026-06-13) - [x] `DistributedLockPersistenceConfig` Spring wiring (adapter-persistence) — in-process (`@Primary`, matchIfMissing) + JDBC conditional beans — 등급: `actually-implemented` / `locally-verified` (D3, 2026-06-13) - [x] `V4__int_lock.sql` Flyway migration (adapter-persistence/db/migration) — SI 6.5 verbatim PostgreSQL DDL — 등급: `actually-implemented` (D3/D4, 2026-06-13; Testcontainers run-verify is app-bootstrap scope) - [x] `LockRegistryDistributedLockAdapterTest` 5종 단위 테스트 (DefaultLockRegistry, no Spring context) — 등급: `locally-verified` (5/5 PASS, 2026-06-13) - [x] `lock.acquisition` metric decorator `MeteredDistributedLockPort` (app-bootstrap) — 등급: `actually-implemented` / `locally-verified` (D7, 2026-06-13) - [x] `DistributedLockConfig` @ConditionalOnProperty bean wiring (app-bootstrap) — distributedLockProvider `@Primary`, multi-instance=true 시만 활성 — 등급: `actually-implemented` / `locally-verified` (D1/D3, 2026-06-13) - [x] ca-tmpl `StartupSafetyValidator` 의 `distributedLockProvider` 주석 owner 표기 갱신 (runtime-health → 본 branch) — 등급: `actually-implemented` / `locally-verified` (§Audit A1, 2026-06-13) - [x] `ca-skeleton.lock: wait-time: 3s / lease-ttl: 30s` application.yml 기본값 배선 (app-bootstrap) — 등급: `actually-implemented` (D5, 2026-06-13) - [x] `MeteredDistributedLockPortTest` 6종 단위 테스트 (app-bootstrap) — acquired/timeout/error + no-registry no-op — 등급: `locally-verified` (6/6 PASS, 2026-06-13) - [x] `LockAcquisitionTimeoutClassificationContractTest` 5종 계약 테스트 (app-bootstrap) — enum SSOT + skip-not-pass registry/metrics — 등급: `locally-verified` (5/5 PASS, 2026-06-13) - [x] `DistributedLockProviderContractTest` 4종 계약 테스트 (app-bootstrap) — D1 bean presence/absence + D3 mutual exclusion + D5 lease expiry (Testcontainers PG) — 등급: `locally-verified` (4/4 PASS, 2026-06-13) - [x] **Quality-review remediation (2026-06-13)**: SI-LOCK-C5 lease-expiry 처리 + D5 테스트 poll 개선 — 등급: `actually-implemented` / `locally-verified` - `MeteredDistributedLockPort`: `LOCK_LEASE_EXPIRED="lock.lease.expired"` 상수 + `closeHandlingLeaseExpiry()` + `incrementLeaseExpired()` 추가. `tryAcquire` 는 wrapping lambda 반환. - `MeteredDistributedLockPortTest`: 기존 identity(isSameAs) 어설션 제거(wrapping lambda로 변경됨) + 신규 4종: `LOCK_LEASE_EXPIRED` 상수 pinning + CME 삼킴 + non-CME 전파 + no-registry CME 삼킴 → 10/10 PASS - `DistributedLockProviderContractTest`: D5 sleep-then-single 취약점 → bounded poll 수정 + SI-LOCK-C5 2종 신규(raw CME 증명 + metered 삼킴+카운터) + intentional discard `@SuppressWarnings("unused")` + 총 6/6 PASS - [ ] ArchUnit rule 요구사항을 [[raw/branch-notes/feature-architecture-enforcement-rules]] 에 등록 — 등급: `planned` (D8) - [ ] background-job §테스트 계약의 ShedLock `LockProvider` FQCN 전파 알림 — 등급: `planned` (§Audit A4) ## 진행 중 메모 - 2026-06-12: /branch-spec 자동조사 — wiki-decision-researcher 1회(대안 5개 비교) + wiki-source-summarizer 5회(신규 raw 5건). 비교 매트릭스 축: 인프라 의존 / 트랜잭션 commit 정합 / lease·timeout / reentrancy / Spring 생태계 통합 / 운영 복잡도. - 대기업(국내) production 사례 공백 — Subskribe(미국 SaaS)·FireHydrant 영어권 사례만 확보. 토스/카카오/네이버 advisory-lock 사례는 검색 미발견 (추가 조사 후보). - 2026-06-13 **Layer 1 (shared-contract) 완료** (이전 세션): `OperationalError.LOCK_ACQUISITION_TIMEOUT(Category.CONFLICT, 409, true)` 추가. D7 §Decision Evidence Map row 상태 갱신 미완이었음 — 본 세션에서 TODO 행 `actually-implemented` 로 정정. - 2026-06-13 **Layer 2 (application-core) 완료** (ca-implementer): 3종 타입 신설 + 계약 테스트 10/10 통과. - `dev.caskeleton.application.lock.DistributedLockPort` — D2/D4/D5/D6 javadoc 포함 (canonical usage + forbidden inverse) - `dev.caskeleton.application.lock.DistributedLock extends AutoCloseable` — `close()` no checked exception - `dev.caskeleton.application.lock.LockAcquisitionTimeoutException` (final, RuntimeException) — `key()`, `waitTime()`, `errorCode()→LOCK_ACQUISITION_TIMEOUT` - TDD: `compileTestJava` 실패(29 error) 확인 후 구현 → `./gradlew :application-core:test` 10/10 PASS - 테스트 수정 1건: `message_contains_waitTime` — `Duration.ofMillis(500).toString()` = `"PT0.5S"` (ISO-8601), "500" 포함 아님. 어설션을 `contains(waitTime.toString())` 로 정정. - build.gradle 무수정 확인 (`:shared-contract` 이미 `implementation` 의존) - Spring/JPA import 0 — 순수 `java.time` + `shared.error` 만 사용 - 2026-06-13 **Layer 3 (adapter-persistence) 완료** (ca-implementer): LockSettings + adapter + Config + V4 migration. - `dev.caskeleton.adapter.persistence.lock.LockSettings` — `@Validated @ConfigurationProperties("ca-skeleton.lock")` record. compact-ctor: null→default(waitTime=3s, leaseTtl=30s), non-positive → `IllegalArgumentException`, cross-field leaseTtl < waitTime → `IllegalArgumentException`. - `dev.caskeleton.adapter.persistence.lock.LockRegistryDistributedLockAdapter implements DistributedLockPort` — wraps any SI `LockRegistry`. `tryAcquire`: leaseTtl > configuredTtl guard → `IllegalArgumentException`; `l.tryLock(waitTime.toMillis(), MILLISECONDS)`; InterruptedException → restore interrupt + throw timeout; returns `l::unlock` lambda. - `dev.caskeleton.adapter.persistence.lock.DistributedLockPersistenceConfig` — `@Configuration(proxyBeanMethods=false)`. in-process `@Primary @ConditionalOnProperty(... matchIfMissing=true)`; JDBC 3 beans `@ConditionalOnProperty(havingValue="true")`. SI types confined to adapter-persistence (implementation dep — invisible to app-bootstrap/application at compile time). `jdbcDistributedLock` intentionally NOT `@Primary` — app-bootstrap wraps in metrics decorator (cross-module contract). - `V4__int_lock.sql` — SI 6.5 verbatim PostgreSQL DDL with header comment (D3/D4 + TTL note). V1/V3 present, V2 absent; V4 is correct next. - `LockRegistryDistributedLockAdapterTest` — 5 unit tests over `DefaultLockRegistry` (no Spring context, no DB). TDD: red(`compileTestJava` 7 errors confirmed) → green(5/5 PASS). Key test: concurrent timeout via CountDownLatch (deterministic, no sleep). - SI 6.5 TTL finding: `DefaultLockRepository.setTimeToLive(int ms)` is repository-level; per-lock `lock(Duration)` API does not exist in 6.5 (SI 7.0+). configuredTtl guard in adapter prevents callers from overpromising per-call lease. - `verifyCleanArchitectureDependencies` not run (build.gradle not modified); `./gradlew :adapter-persistence:test` full suite PASS. - 2026-06-13 **Layer 4 (app-bootstrap) 완료** (ca-implementer): MeteredDistributedLockPort + DistributedLockConfig + StartupSafetyValidator 주석 + application.yml lock 기본값 + 3종 테스트. - `dev.caskeleton.bootstrap.lock.MeteredDistributedLockPort implements DistributedLockPort` — `ObjectProvider` no-op 패턴(`BackgroundJobMetrics` 동일). 상수: `LOCK_ACQUISITION="lock.acquisition"`, `TAG_OUTCOME="outcome"`, `OUTCOME_ACQUIRED/TIMEOUT/ERROR`. catch `LockAcquisitionTimeoutException`→TIMEOUT, catch other `RuntimeException`→ERROR, success→ACQUIRED; `increment()` swallows meter errors. - `dev.caskeleton.bootstrap.lock.DistributedLockConfig` — `@Configuration(proxyBeanMethods=false)`. `@Bean("distributedLockProvider") @Primary @ConditionalOnProperty(prefix="ca-skeleton.runtime", name="multi-instance-enabled", havingValue="true")`. `@Qualifier("jdbcDistributedLock")` 주입 → `MeteredDistributedLockPort` 래핑. - `StartupSafetyValidator.java` 주석 수정 — `distributedLockProvider` 행 코멘트를 runtime-health → `feature-distributed-lock-contract (D1/D3 — JdbcLockRegistry distributed lock; in-process default when single-instance)` 로 갱신. §Audit A1 해소. - `application.yml` lock 블록 추가 — `ca-skeleton.lock: wait-time: 3s / lease-ttl: 30s`. 코드 기본값과 일치(APP_* env 키 미등록 — env-driven-runtime-configuration 소관). `ca-skeleton.runtime:` 블록 아래. - `app-bootstrap/build.gradle` — `testImplementation 'org.springframework.integration:spring-integration-jdbc'` 추가. 이유: SI 타입(`DefaultLockRepository`/`JdbcLockRegistry`)이 adapter-persistence `implementation` 의존이라 app-bootstrap 컴파일 classpath 에 미노출. `DistributedLockProviderContractTest` 가 두 개의 독립 registry 인스턴스(두 앱 인스턴스 시뮬레이션)를 직접 빌드하는 데 필요. - TDD: `MeteredDistributedLockPortTest` 6개 먼저 작성(compileTestJava 실패) → 구현 → 6/6 PASS. `LockAcquisitionTimeoutClassificationContractTest` 5개 → 5/5 PASS. `DistributedLockProviderContractTest` 4개 → 4/4 PASS. - **핵심 발견: `DefaultLockRepository` Spring 컨텍스트 외부 초기화** — `readCommittedTransactionTemplate` 은 `InitializingBean.afterPropertiesSet()` 이 아니라 `SmartInitializingSingleton.afterSingletonsInstantiated()` 에서 생성된다. Spring 컨텍스트 없이 쓸 때는 `setTransactionManager()` → `afterPropertiesSet()` → `afterSingletonsInstantiated()` → `start()` 순서를 명시 호출해야 한다. 누락 시 D3/D5 테스트에서 `CannotAcquireLockException(NullPointerException: readCommittedTransactionTemplate)` 발생. - 사전 기존 ArchUnit 실패: `outbound_adapter_method_returns_only_domain_or_primitives` — `OutboundHttpSettings.retry()/.circuitBreaker()` 가 adapter.outbound 내 nested record 반환. commits d702572/2613561/907dfad (이 task 이전) 에서 발생. 본 task 범위 외. - `verifyCleanArchitectureDependencies verifyEnvKeys` PASS (build.gradle 수정 → verifyCleanArchitectureDependencies 필수). `app-bootstrap` 전체 suite: 274 tests, 1 pre-existing failure. - 2026-06-13 **Quality-review remediation (ca-implementer)**: Finding 1 (Critical SI-LOCK-C5) + Finding 2 (Important — D5 flaky sleep + SI-LOCK-C5 coverage) + Minor #4 해소. - `MeteredDistributedLockPort` 변경: `java.util.ConcurrentModificationException` import (JDK — no SI import in main src). `tryAcquire` 가 `() -> closeHandlingLeaseExpiry(key, handle)` wrapping lambda 반환. `closeHandlingLeaseExpiry`: CME 만 catch → log.warn + `incrementLeaseExpired()`; 다른 예외 전파. `incrementLeaseExpired()`: 동일 null-guard + try-catch-log-and-swallow 패턴. `LOCK_LEASE_EXPIRED="lock.lease.expired"` 상수 신설. - `MeteredDistributedLockPortTest` 변경: 기존 2개 테스트의 `isSameAs(expectedHandle)` 어설션 → wrapping lambda 인식하도록 `isNotNull() + close() 정상` 검증으로 교체. 신규 4종: ① `lock_lease_expired_constant_matches_registry_name` (pinning), ② `close_swallows_CME_and_increments_lease_expired_counter`, ③ `close_propagates_non_CME_exception_unchanged`, ④ `close_swallows_CME_when_no_registry_is_present`. → 10/10 PASS. - `DistributedLockProviderContractTest` 변경: D5 test — `Thread.sleep(+500)` 후 단일 시도 → 수면 후 bounded poll(최대 shortTtl×4, 200ms 간격). intentional discard `@SuppressWarnings("unused")` 변수 명명 추가(Minor #4). 신규 2종: `si_lock_c5_raw_adapter_close_throws_CME_after_lease_expires` (raw CME 문서화) + `si_lock_c5_metered_port_swallows_CME_and_increments_lease_expired_counter` (metered 흡수+카운터). cross-package로 `LOCK_LEASE_EXPIRED` 상수 접근 불가 → 리터럴 `"lock.lease.expired"` 사용 (MeteredDistributedLockPortTest 의 pinning test 가 drift 방지 역할). → 6/6 PASS. - `app-bootstrap` 전체 suite: 280 tests, 1 pre-existing failure (`outbound_adapter_method_returns_only_domain_or_primitives`). - `LOCK_LEASE_EXPIRED` 상수 visibility: package-private (기존 상수 패턴 유지). cross-package 테스트는 리터럴 직접 사용 + same-package pinning test 로 drift 방지. ## 결정 사항 > 추후 면접/회고에서 "왜 이렇게 했나" 답할 근거. 대안과 함께 기록. 각 결정의 근거는 위 Sources 또는 새로 추가된 raw 자료를 가리킬 것. - 2026-06-12 (D1): 본 branch 가 `distributedLockProvider` bean 계약의 SSOT owner — background-job §Audit A7 의 owner 공백 해소 / 이유: 5개 coordination bean 중 유일하게 owner 부재, 코드 주석의 runtime-health 표기는 stale / 대안: runtime-health 가 소유(그 노트가 consume-only 자기 서술이라 기각) / 근거: ca-tmpl `StartupSafetyValidator.java` 코드 + [[raw/branch-notes/feature-background-job-async-contract]] §Audit A7 - 2026-06-12 (D2): lock 접근은 application-core port 경유 — `obtain(key) → java.util.concurrent.locks.Lock` 시맨틱 / 이유: CA 레이어 규칙 + provider 교체 가능성 / 대안: 구현체 직접 사용(레이어 위반 기각) / 근거: [[raw/official-docs/lock-spring-integration-lock-registry]] - 2026-06-12 (D3): multi-instance 기본 provider = Spring Integration `JdbcLockRegistry`(PG baseline 재사용), Redis 활성 시 `RedisLockRegistry`/Redisson 교체 허용 / 검토 대안 5: PG session advisory(배제 — rollback 비해제·dangling), PG xact advisory(D4 의 보조 경로로 한정), JdbcLockRegistry(채택), ShedLock(배제 — maintainer 거부 + skip 시맨틱), Redisson(Redis-활성 분기) / 근거: [[raw/official-docs/lock-spring-integration-lock-registry]], [[raw/official-docs/lock-shedlock-issue-899-non-scheduler-use]], [[raw/official-docs/lock-postgres-advisory-locks]] - 2026-06-12 (D4): 트랜잭션 commit 정합 불변식 — lock 해제는 보호 대상 tx 의 commit *이후*에만. tx-scope 일치 use case 는 `pg_advisory_xact_lock` 허용(자동 해제), session-level advisory 는 도입 금지 / 근거: [[raw/official-docs/lock-postgres-advisory-locks]] (PG-ADV-C2/C3) - 2026-06-12 (D5): 획득 계약 = try-lock + 유한 waitTime + lease(TTL) 필수, 무한 blocking 금지 / 근거: PG-ADV-C4, LOCK-C2, SI-LOCK-C4/C5, SUBSKRIBE-LOCK-C2 - 2026-06-12 (D6): 본 lock 은 efficiency lock 전용 — correctness 는 DB 제약(unique/optimistic lock)으로, fencing token 미도입 / 근거: LOCK-C4 (Kleppmann, `engineering-blog` — 재확인 보류 상태 명시) - 2026-06-12 (D7): lock 획득 실패 error code `LOCK_ACQUISITION_TIMEOUT` (category `CONFLICT`, retryable true) + metric `lock.acquisition` — **registry 에 없는 신규 제안** (기존 값 단정 아님, registry-governance 절차 경유) - 2026-06-12 (D8): domain-core·application-core 에서 lock 구현체 패키지 의존 금지 (정적 강제 요구) — rule 호스팅은 `feature-architecture-enforcement-rules` SSOT 에 위임 ## 결정-근거 매핑 > 각 결정이 어떤 raw source claim 으로 뒷받침되는지 명시한다. > `Decision ID` 는 이 branch-note 안에서 안정적으로 유지한다. 예: `D1`, `D2`. > `Supporting Claims` 는 `raw//.md#C1` 형식으로 연결한다. > `선택 조건` 열(R2): "이 조건일 때 이 결정, 다른 조건이면 어떤 대안". 분기 없으면 `N/A`. | Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk | |---|---|---|---|---|---| | D1 | 본 branch = `distributedLockProvider` bean 계약 SSOT owner (A7 해소). bean 이름은 `StartupSafetyValidator.REQUIRED_MULTI_INSTANCE_BEANS` 기존 값 `"distributedLockProvider"` 재사용 | N/A — owner 공백 해소 (다른 branch 가 이미 소유했다면 본 branch 신설 불요였음) | ca-tmpl `src/app-bootstrap/.../StartupSafetyValidator.java` (code fact) + `raw/branch-notes/feature-background-job-async-contract.md` §Audit A7 | `internal-code-fact + sibling-audit` (외부 출처 비대상 — 내부 ownership 결정) | 코드 주석의 owner 표기가 runtime-health 로 stale (§Audit A1 — ca-tmpl 갱신 필요) | | D2 | lock 접근은 application-core port 경유, `obtain(key) → java.util.concurrent.locks.Lock` 시맨틱 (LockRegistry 모델 차용) | 구현체가 j.u.c.Lock 호환을 제공하는 한 이 결정. 호환 불가 provider 도입 시(예: skip-시맨틱) port 시그니처 재설계 | `raw/official-docs/lock-spring-integration-lock-registry.md#SI-LOCK-C1`, `raw/official-docs/cache-redisson-rlock-vs-setnx.md#LOCK-C3` (Redisson 도 j.u.c.Lock — 이식성 방증) | `official-vendor-doc` (SI) + `needs-confirmation` (LOCK-C3) | port 명명·메서드 모양은 `UNSUPPORTED_IMPL_DECISION` (§구현 가이드 1) | | D3 | multi-instance 기본 provider = `JdbcLockRegistry` (PG baseline 재사용, 추가 인프라 0). Redis 활성 시 `RedisLockRegistry`/Redisson 교체 허용. ShedLock·PG session-level advisory 배제 | `APP_MULTI_INSTANCE_ENABLED=true` + Redis 비활성 → JdbcLockRegistry; Redis 활성(cache 활성) → RedisLockRegistry/Redisson 교체 가능; flag=false(default) → bean 불요, in-process 구현으로 충분 | `raw/official-docs/lock-spring-integration-lock-registry.md#SI-LOCK-C2`, `#SI-LOCK-C3`, `raw/official-docs/lock-shedlock-issue-899-non-scheduler-use.md#SHEDLOCK-899-C1`, `#SHEDLOCK-899-C2` (ShedLock 배제), `raw/official-docs/lock-postgres-advisory-locks.md#PG-ADV-C2`, `#PG-ADV-C5` (session-level 배제), `raw/company-tech-blogs/lock-subskribe-advisory-lock-distributed-consensus.md#SUBSKRIBE-LOCK-C1` (DB-only 사례) | `official-vendor-doc + maintainer-statement + company-case-study` | `spring-integration-jdbc` 신규 의존성 + `INT_LOCK` DDL 관리 비용. SI 버전 ↔ Boot BOM 정합 미확인 (§Claims To Verify) | | D4 | 트랜잭션 commit 정합 불변식: lock 해제는 보호 대상 작업의 DB commit **이후**에만. lock 수명 = 단일 tx 인 use case 는 `pg_advisory_xact_lock` 허용(commit/rollback 자동 해제). session-level advisory 의 수동 unlock 경로는 도입 금지 | lock scope ⊆ 단일 tx → xact advisory lock (자동 정합); lock scope ⊃ tx (여러 tx/외부 호출 포함) → JdbcLockRegistry + "획득 → tx → commit 반환 후 unlock" 순서 강제 | `raw/official-docs/lock-postgres-advisory-locks.md#PG-ADV-C2` (session-level 은 tx 시맨틱 무시 — rollback 후에도 잔존), `#PG-ADV-C3` (xact-level 은 tx 종료 시 자동 해제), `raw/company-tech-blogs/lock-subskribe-advisory-lock-distributed-consensus.md#SUBSKRIBE-LOCK-C4` (사례 보강) | `official-vendor-doc + company-case-study` | Spring `@Transactional` proxy 와 xact lock 의 실제 정합은 `locally-verified` 필요 (§Claims To Verify) | | D5 | 획득 계약: try-lock + 유한 waitTime + lease(TTL) 필수. 무한 blocking 금지. lease 갱신은 보유 thread 만, lease 만료 후 unlock 은 예외 처리 의무 | N/A — 모든 획득 경로 공통. (lease 없는 lock 이 필요해지면 D6 correctness 경계 재검토가 선행) | `raw/official-docs/lock-postgres-advisory-locks.md#PG-ADV-C4` (try 변형 존재), `raw/official-docs/cache-redisson-rlock-vs-setnx.md#LOCK-C2` (TTL = crash 시 deadlock 회피), `raw/official-docs/lock-spring-integration-lock-registry.md#SI-LOCK-C4` (갱신은 보유 thread 만), `#SI-LOCK-C5` (만료 후 unlock → `ConcurrentModificationException`), `raw/official-docs/lock-shedlock-readme.md#SHEDLOCK-C3`, `#SHEDLOCK-C4` (lease 상·하한 원리 참조), `raw/company-tech-blogs/lock-subskribe-advisory-lock-distributed-consensus.md#SUBSKRIBE-LOCK-C2` (try-only 운영 사례) | `official-vendor-doc + official-reference + company-case-study` | 구체 default 값(waitTime/TTL)은 `UNSUPPORTED_IMPL_DECISION` (§구현 가이드 4) | | D6 | 본 lock 은 **efficiency lock 전용**. correctness 가 필요한 경로는 DB 제약(unique constraint = `DB_UNIQUE_VIOLATION`, optimistic lock = `PRECONDITION_FAILED` 기존 계약)으로 보장. fencing token 미도입 | 중복 *작업* 방지(비용 절감) 목적 → 본 lock; 중복 *결과* 차단(정합성) 필요 → DB 제약 사용. fencing token 이 필요한 외부 시스템 mutation 등장 시 본 결정 재검토 | `raw/official-docs/cache-redisson-rlock-vs-setnx.md#LOCK-C4` (Kleppmann: lease 기반 correctness 는 unsafe, efficiency 는 충분), `raw/official-docs/lock-shedlock-readme.md#SHEDLOCK-C5` (clock 동기화 *가정* — lease 기반의 전제 한계 방증) | `engineering-blog` (LOCK-C4 — verbatim 재확인 보류) + `official-reference` | LOCK-C4 의 verbatim 재확인 불가 상태 지속 (cache branch 와 공동 — archive.org 스냅샷 필요) | | D7 | lock 획득 실패/timeout 의 error code = `LOCK_ACQUISITION_TIMEOUT` (category `CONFLICT`, retryable true, client_safe true) + metric `lock.acquisition` (tag: outcome) — **registry 신규 제안** | N/A — 단 registry-governance 검토에서 기존 code 재사용 판정 시 그 code 채택 | `UNSUPPORTED_IMPL_DECISION` — registry(`error-codes.yaml`·`metrics.yaml`)에 일반 lock 항목 부재 확인(2026-06-12 grep). category `CONFLICT` 는 기존 enum(`shared/error/Category.java`) 재사용, code/metric *이름* 은 근거 없는 신규 제안 | `none` (신규 제안 — 기존 값 단정 금지) | registry-governance 절차 미통과 상태. cache 의 `CACHE_STAMPEDE_LOCK_TIMEOUT` 과 의미 경계 문서화 필요 | | D8 | domain-core·application-core 에서 lock 구현체 패키지(`org.springframework.integration..`, `org.redisson..`, `net.javacrumbs.shedlock..`) 의존 + advisory SQL 직접 호출 금지 — adapter 전용. rule 호스팅은 [[raw/branch-notes/feature-architecture-enforcement-rules]] SSOT 위임 (본 branch 는 요구사항만 등록) | N/A — D2 port 결정의 정적 강제 도출 | D2 의 도출 + ca-tmpl `CLAUDE.md` 의존 방향 매트릭스 (code fact). rule *명명* 은 `UNSUPPORTED_IMPL_DECISION` | `internal-code-fact` (모듈 매트릭스) | rule 이 architecture-enforcement-rules 에 실제 등록되기 전까지 `documented-only` | ## 구현 가이드 > *결정 (Decisions)* 이 "*무엇* 을 할 것인가" 라면, 본 §는 "*어디에 어떻게* 구현될 것인가" 의 사전 명세. 3-rule meta principle (R1 Reference / R2 UNSUPPORTED_IMPL_DECISION / R3 OUT_OF_BRANCH_SCOPE) 준수 — CLAUDE.md §15.5. > 구현 상태: 본 § 전체가 **`planned`** — src grep 실측(2026-06-12) 결과 lock 관련 구현은 `StartupSafetyValidator` 의 bean-presence 검사뿐, port/adapter/registry 코드는 전무. `actually-implemented` 로 표현 금지. ### 1. Port · adapter · wiring 배치 (D1 > **Trace**: D1 (bean 이름 = code 기존 값) + D2 (port 추상화 — SI-LOCK-C1) + D8 (구현체 격리) > > - **UNSUPPORTED_IMPL_DECISION**: ① port 명명 `DistributedLockPort` + 메서드 `tryAcquire(key, waitTime, ttl)` 모양 — 근거 raw 는 *추상화 원칙*(obtain→Lock)만 권고, 명명은 임의 (trade-off: sibling port 명명 패턴 `*Port` 정합). ② 모듈 배치 — adapter 구현을 `adapter-persistence` 에 두는 것은 "JDBC 기반"이라는 도출이지 raw 권고 아님 (trade-off: lock 저장소 = DB 이므로 persistence 인접이 의존 방향 최소). | 항목 | 명세 | 상태 | |---|---|---| | port 인터페이스 | `application-core` — `DistributedLockPort` (가칭): `tryAcquire(String key, Duration waitTime, Duration ttl)` → lock handle (j.u.c.Lock 호환) | `planned` | | adapter 구현 | `adapter-persistence` — `JdbcLockRegistry` wrapping (D3). Redis 분기 구현은 Redis 활성 모듈에 별도 | `planned` | | bean wiring | `app-bootstrap` — bean 이름 **`distributedLockProvider`** (code 기존 값 — `StartupSafetyValidator.REQUIRED_MULTI_INSTANCE_BEANS[0]`). `APP_MULTI_INSTANCE_ENABLED=true` 일 때만 등록 | `planned` | | single-instance 경로 | flag=false(default) 시 in-process 구현(SI `DefaultLockRegistry` 동등 시맨틱)으로 port 계약 유지 — bean presence 강제 대상 아님 (env D8 consume) | `planned` | ### 2. Provider 선택 분기 (D3) > **Trace**: D3 — SI-LOCK-C2 (4종 공식 구현체), SI-LOCK-C3 (JdbcLockRegistry 분산 락), SHEDLOCK-899-C1/C2 (ShedLock 배제), PG-ADV-C2/C5 (session-level 배제), SUBSKRIBE-LOCK-C1 (DB-only 사례) | 조건 | provider | 비고 | |---|---|---| | `APP_MULTI_INSTANCE_ENABLED=false` (default) | in-process (SI `DefaultLockRegistry` 동등) | 분산 조정 불요 — single-instance 계약 | | flag=true + Redis 비활성 | **`JdbcLockRegistry`** (채택 기본값) | PG baseline 재사용, 추가 인프라 0. `INT_LOCK` 테이블 필요 (DDL 은 migration-startup 계약 경유) | | flag=true + Redis 활성 | `RedisLockRegistry` 또는 Redisson RLock | port 불변, 구현체만 교체 (SI-LOCK-C2). Redisson 채택 시 cache branch 의존성 재사용 | | (배제) ShedLock | — | maintainer 가 generic lock 공식 선언 거부(SHEDLOCK-899-C1) + 획득 실패 시 *skip* 시맨틱으로 blocking 계약 불일치(SHEDLOCK-899-C2). scheduler 영역 사용은 background-job D3 소유로 불변 | | (배제) PG session-level advisory | — | tx rollback 에도 잔존(PG-ADV-C2) + dangling lock 위험(PG-ADV-C5) + pool 반납 시 leak 경로 | ### 3. 트랜잭션 commit 정합 패턴 카탈로그 (D4) > **Trace**: D4 — PG-ADV-C2 (session = tx 무시), PG-ADV-C3 (xact = 자동 해제), SUBSKRIBE-LOCK-C4 (사례) | 패턴 | 판정 | 이유 | |---|---|---| | lock 획득 → `@Transactional` 작업 → commit 반환 **후** finally unlock | ✅ 허용 (general 경로) | 해제가 commit 에 후행 — 임계 구역이 commit 전에 열리지 않음 | | `pg_advisory_xact_lock` 을 tx 내부에서 획득 | ✅ 허용 (tx-scope 경로) | commit/rollback 시 자동 해제 (PG-ADV-C3) — 정합을 DB 가 보장 | | tx **내부**에서 general lock 해제 (commit 전 unlock) | ❌ 금지 | 미commit 상태에서 다른 인스턴스가 임계 구역 진입 — lost update 류 race | | session-level advisory lock + 수동 unlock | ❌ 금지 | rollback 에도 잔존(PG-ADV-C2) + unlock 누락 시 pool 반납 leak. 본 계약에서 경로 자체 미도입 | ### 4. 획득·해제 계약 + 실패 매핑 (D5 > **Trace**: D5 — PG-ADV-C4, LOCK-C2, SI-LOCK-C4/C5, SHEDLOCK-C3/C4, SUBSKRIBE-LOCK-C2. D7 — registry 부재 확인(2026-06-12 grep). > > - **UNSUPPORTED_IMPL_DECISION**: waitTime/TTL default 값 (예: waitTime 3s / TTL 30s) — 어떤 raw 도 구체 값을 권고하지 않음 (trade-off: Redisson watchdog default 30s 와 LOCK-C1 의 PX 30000 을 관행 참고치로만 사용, 측정 후 조정). error code `LOCK_ACQUISITION_TIMEOUT`·metric `lock.acquisition` *이름* — registry 신규 제안 (기존 값 아님을 명시). Jdbc 분기 long-task 의 `renewLock` 호출 *주기* — SI 7.0+ 의 `lock(Duration ttl)` API 존재는 raw 가 보장하나 갱신 주기 값은 임의 (trade-off: TTL 의 1/3 주기 관행 참고, 측정 후 조정). | 항목 | 계약 | 상태 | |---|---|---| | 획득 | try-lock + 유한 waitTime 필수. 무한 blocking API 노출 금지 (PG-ADV-C4 의 try 변형 + SUBSKRIBE-LOCK-C2 운영 교훈) | `planned` | | lease | TTL 필수 — 보유자 crash 시 자동 만료 (LOCK-C2, SHEDLOCK-C3 원리) | `planned` | | 갱신 | 보유 thread 만 (SI-LOCK-C4). 자동 watchdog 은 Redisson 분기에서만 (LOCK-C3 — `needs-confirmation`) | `planned` | | Jdbc 분기 long-task 갱신 | **Jdbc 분기에는 자동 watchdog 이 없음** — lock 보유 시간이 TTL 을 넘을 수 있는 작업은 ① 명시적 `renewLock` 주기 호출(보유 thread, SI-LOCK-C4) 또는 ② TTL ≥ 최대 작업 시간 보장 중 하나를 선택. 주기 값은 `UNSUPPORTED_IMPL_DECISION` (위 헤더) | `planned` | | 만료 후 해제 | `ConcurrentModificationException` 처리 의무 (SI-LOCK-C5) — 삼킴 금지, 로그 + metric | `planned` | | 실패 매핑 | timeout → `LOCK_ACQUISITION_TIMEOUT` (**신규 제안** — category `CONFLICT` 기존 enum 재사용, retryable true). registry-governance 통과 전 코드 작성 금지 | `planned` (제안 단계) | | metric | `lock.acquisition` (tag: `outcome` = acquired/timeout/error) — **신규 제안**. 기존 `metrics.yaml` 에 lock 항목 없음 확인 | `planned` (제안 단계) | ### 5. Contract test 계약 (D1 > **Trace**: D1 (bean presence) + D3 (provider 분기). env D8 의 `StartupSafetyValidator` 집행을 consume — 검사 메커니즘 자체는 env branch 소유 (OUT_OF_BRANCH_SCOPE). | 테스트 | 검증 내용 | 상태 | |---|---|---| | bean presence | `APP_MULTI_INSTANCE_ENABLED=true` 시 `distributedLockProvider` bean 부재 → startup fail (기존 `StartupSafetyValidatorTest` 는 이름 기반 presence 만 검증 — 본 branch 는 *실제 bean 등록* 쪽 테스트 추가) | `planned` | | 상호 배제 | 동일 key 에 2 인스턴스(2 DataSource 컨텍스트) 경쟁 → 1개만 획득 | `planned` | | commit 정합 | tx 미commit 상태에서 두 번째 획득 시도가 성공하지 않음 (D4 패턴 ✅① 검증) | `planned` | | lease 만료 | TTL 경과 후 두 번째 인스턴스 획득 가능 + 원 보유자 unlock 시 CME 처리 (SI-LOCK-C5) | `planned` | ## Audit & Findings > 이관 history + drift 기록 (CLAUDE.md §15.5 R3). §구현 가이드에는 in-scope 만 남기고, 범위 밖/정정/전파는 여기 보존. - **A1. `STALE_CODE_COMMENT` (drift)** — ca-tmpl `StartupSafetyValidator.java` 의 `"distributedLockProvider"` 행 주석이 `feature-runtime-health-lifecycle-contract` 를 owner 로 표기 — 그 노트는 "consume only" 자기 서술(background-job §Audit A7 발견). 본 branch 가 owner 로 확정되었으므로 **코드 주석을 본 branch 로 갱신 권고** (ca-tmpl 측 변경 — 자동 수정 안 함, 정합 권고만). - **A2. `RESEARCH_CORRECTION`** — 선행 조사(wiki-decision-researcher)가 "ShedLock = scheduler 전용 *공식 입장*"으로 요약했으나 README verbatim(SHEDLOCK-C2 "it's just a lock")은 그 표현을 지지하지 않음. issue #899 verbatim 으로 정정: 배제의 실근거 = *generic lock 공식 선언 거부*(SHEDLOCK-899-C1) + *skip(비대기) 시맨틱*(SHEDLOCK-899-C2). 커뮤니티의 non-scheduler production 사용 보고(SHEDLOCK-899-C4)도 존재 — "기술적 불가"가 아니라 "공식 비지원 + 시맨틱 불일치"가 배제 이유. - **A3. `OUT_OF_BRANCH_SCOPE` 이관 기록** — ① scheduler/outbox lock 적용 정책 → background-job D3 (불변). ② cache stampede lock + `CACHE_STAMPEDE_LOCK_TIMEOUT` → cache-consistency D3/D4 (불변). ③ `APP_MULTI_INSTANCE_ENABLED` + validator 집행 → env-driven D8 (consume). ④ `INT_LOCK` DDL 의 migration *절차* → migration-startup-contract (본 branch 는 DDL 필요 사실만 제안). ⑤ ArchUnit rule 호스팅 → architecture-enforcement-rules (D8 은 요구사항만). - **A4. `PROPAGATION_NOTICE` (비차단)** — background-job §테스트 계약·§구현 가이드 4 의 테스트 FQCN `net.javacrumbs.shedlock.core.LockProvider` 는 "ShedLock 또는 동등 bean" 가정 시절의 표기. 본 branch D3 가 `JdbcLockRegistry` 를 기본 채택했으므로 그 테스트 계약의 FQCN 은 port/bean 기준으로 갱신 필요. 동일하게 project-note §27 의 "ShedLock + Redisson + …" 5종 나열도 "distributedLockProvider(본 branch D3)" 로 읽도록 전파 대상. **비차단** — owner(background-job·env·project note) 가 다음 편집 시 반영. - **A5. `NEW_BRANCH_REGISTRATION`** — parent project §29.E row #9 가 본 branch 를 `(없음)` 예정으로 표기 + §25 SSOT Owner Map 에 distributed lock row 부재. 본 branch 신설로 §31.1 Cluster list + §25 Owner Map + §29 row 상태 갱신 필요 (project-note 사용 절차 #4 의무 — 본 세션에서 최소 반영 또는 다음 project-note 편집 시). ## 엣지·실패·의존 > R4(깊이 게이트) 캡처용. 정상 경로 외에 *구현 중 부딪힐* 실패/엣지/다른 계약 의존을 미리 열거. - **실패·엣지 경로**: - 획득 timeout → `LOCK_ACQUISITION_TIMEOUT`(신규 제안) 반환, retryable true — 호출측 재시도 정책은 호출 branch 소유 - lease 만료 *중* 작업 진행 — 두 보유자 동시 진입 가능. D6 efficiency 경계로 *허용*하되 correctness 필요 경로는 DB 제약이 최종 방어 (LOCK-C4) - lease 만료 후 unlock → `ConcurrentModificationException` (SI-LOCK-C5) — 삼킴 금지, 로그+metric 후 정상 흐름 복귀 - JVM crash → lock row 는 TTL 로 자동 만료 (LOCK-C2/SHEDLOCK-C3 원리) — 잔존 lock 수동 정리 runbook 불요 설계 - clock skew — lease 판정이 노드 시계에 의존하면 SHEDLOCK-C5 의 동기화 가정 필요 → DB 시간 기준 여부 확인 (§Claims To Verify) - 동일 thread 재진입 — `JdbcLockRegistry` 의 reentrancy 보장 미확인 (§Claims To Verify) — 보장 확인 전까지 재진입 금지 계약 - connection pool 고갈 — lock 대기가 DB connection 을 점유하는 구현(advisory blocking)은 배제됨(D3/D5) — JdbcLockRegistry 의 lock 당 connection 사용 패턴은 확인 필요 - **다른 계약 의존**: - [[raw/branch-notes/feature-env-driven-runtime-configuration]] D8 — `APP_MULTI_INSTANCE_ENABLED` flag + `StartupSafetyValidator` presence 강제를 consume. flag 의미/집행 변경 시 본 branch bean 등록 조건 영향 - [[raw/branch-notes/feature-background-job-async-contract]] D3 — scheduler/outbox 가 본 branch 의 provider 를 consume (§Audit A4 전파) - [[raw/branch-notes/feature-cache-consistency-contract]] D3 — Redis 활성 분기에서 Redisson 의존성 공유. cache 가 Redisson 을 제거하면 본 branch Redis 분기 재검토 - `feature-migration-startup-contract` — `INT_LOCK` DDL 의 Flyway 반영 절차 - [[raw/branch-notes/feature-architecture-enforcement-rules]] — D8 rule 호스팅 ## 검증해야 할 주장 > 공식 문서나 사례는 근거지만, 내 프로젝트에서의 동작을 자동으로 보장하지 않는다. > 구현 전/중/후에 실제로 검증해야 하는 주장을 분리한다. | Claim | Why uncertain | How to verify | Status | |---|---|---|---| | `spring-integration-jdbc` 가 ca-tmpl Boot BOM 과 호환 + TTL API(`lock(Duration ttl)`, SI 7.0+) 사용 가능 | SI 버전·TTL API 도입 시점과 현재 BOM 미대조 | `build.gradle` 의존성 추가 후 컴파일 + `JdbcLock` TTL 메서드 존재 확인 | `needs-confirmation` | | `INT_LOCK` 테이블 DDL 은 자동 생성되지 않아 Flyway 수동 migration 필요 | 공식 문서에서 schema 자동 생성 여부 미확인 | SI 배포 schema 스크립트 위치 확인 + 로컬 기동 테스트 | `needs-confirmation` | | `pg_advisory_xact_lock` 이 Spring `@Transactional` commit 시점에 자동 해제 (D4 ✅② 경로) | proxy 기반 tx 경계와 PG 세션의 실제 상호작용 미검증 | 2-connection 경쟁 통합 테스트: tx A 보유 중 tx B 획득 실패 → A commit 후 B 획득 성공 | `needs-confirmation` | | `JdbcLockRegistry` 의 동일 thread 재진입 보장 여부 | SI-LOCK-C1 은 j.u.c.Lock 반환만 보장, reentrancy 는 "Does not prove" 명시 | 공식 Javadoc/소스 확인 + 재진입 단위 테스트 | `needs-confirmation` | | Redisson RLock watchdog 시맨틱 (LOCK-C3) | redisson.org → redisson.pro redirect 차단으로 verbatim 재확인 불가 (cache branch 공동 관심) | Redisson Javadoc 직접 다운로드 또는 GitHub wiki 로 verbatim 격상 | `needs-confirmation` | | `JdbcLockRegistry` 의 lock 대기가 DB connection 을 점유하는지 (polling 마다 반납 vs holding) | retry-polling(idleBetweenTries) 구조라 점유 패턴 미확인 — holding 이면 pool 고갈 시 self-deadlock 경로 | SI 소스/Javadoc 확인 + pool size 1 로 죄인 통합 테스트에서 동시 lock 대기 시 고갈 여부 관찰 | `needs-confirmation` | | flag=true + bean 등록 시 `StartupSafetyValidator` 가 실제 통과 (이름 기반 presence) | 현재 테스트는 *부재 → fail* 만 검증, *등록 → pass* 는 bean 타입 무관 이름만 매칭 | `StartupSafetyValidatorTest` 확장 + 실제 adapter bean 으로 기동 테스트 | `planned` | ## 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때) > `/coverage` 가 채우는 **생성물** — 손으로 유지하지 않는다. governing 문서(frontmatter `governing_docs`)가 요구하는 관심사를 이 브랜치가 빠짐없이 덮는지의 결과. 기준: `rules/coverage-gate.md`. > 상태: `covered-here`(이 브랜치 결정) / `delegated`(다른 owner 브랜치) / `missing`(아무도 안 맡음 → Blocking). | 관심사 | 상태 | owner | 심각도 | 근거 | |--------|------|-------|--------|------| | (생성 전 — `/coverage feature-distributed-lock-contract` 실행 대기) | — | — | — | — | ## 마주친 문제 > 짧은 메모만. 깊이 있는 트러블슈팅은 `raw/errors/` 로 분리하고 아래 Cluster에 연결. - 2026-06-13 (Layer 2): `LockAcquisitionTimeoutExceptionTest.message_contains_waitTime` 첫 실행 실패. 원인: `Duration.ofMillis(500).toString()` 은 `"PT0.5S"` (ISO-8601) — `"500"` 을 포함하지 않음. 어설션을 `contains(waitTime.toString())` 로 수정 후 통과. raw/errors 별도 분리 불필요 (trivial one-liner 수정). - 2026-06-13 (Layer 4): `DistributedLockProviderContractTest` D3/D5 테스트 — `CannotAcquireLockException(NullPointerException: readCommittedTransactionTemplate)`. `DefaultLockRepository` 를 Spring 컨텍스트 없이 사용할 때 `SmartInitializingSingleton.afterSingletonsInstantiated()` 를 명시 호출해야 함을 발견. 상세: [[raw/errors/spring-integration-defaultlockrepository-aftersingletons-null-template-2026-06-13]]. ## 묶음 (이 branch에서 파생된 자료) - [[raw/company-tech-blogs/lock-subskribe-advisory-lock-distributed-consensus]] - [[raw/official-docs/lock-postgres-advisory-locks]] - [[raw/official-docs/lock-shedlock-issue-899-non-scheduler-use]] - [[raw/official-docs/lock-shedlock-readme]] - [[raw/official-docs/lock-spring-integration-lock-registry]] - [[raw/errors/spring-integration-defaultlockrepository-aftersingletons-null-template-2026-06-13]] - [[raw/blog-topics/distributed-lock-transaction-commit-boundary-2026-07-02]] > 이 branch는 단일 노트가 아니라 **작업 묶음의 entry point**. 이 branch에서 파생된 모든 raw 노트를 카테고리별로 명시. ### 근거 자료 - [[raw/official-docs/lock-postgres-advisory-locks]] — PostgreSQL §13.3.5 Advisory Locks + §9.28.10 함수 레퍼런스 (session-level vs transaction-level 시맨틱, non-blocking 변형) - [[raw/official-docs/lock-shedlock-readme]] — ShedLock README: scheduled task 전용 락 / not full-fledged scheduler 공식 경계, `lockAtMostFor`/`lockAtLeastFor` lease 시맨틱, clock 동기화 전제 조건 - [[raw/official-docs/lock-shedlock-issue-899-non-scheduler-use]] — ShedLock Issue #899: maintainer 가 generic lock 공식 선언 거부 + skip semantics 명시 (SHEDLOCK-899-C1, SHEDLOCK-899-C2) — `distributedLockProvider` 후보에서 ShedLock 배제/허용 결정의 근거 - [[raw/official-docs/lock-spring-integration-lock-registry]] — Spring Integration LockRegistry/JdbcLockRegistry 공식 레퍼런스 (j.u.c.Lock 추상화, 4종 구현체, TTL/renewal/CME 시맨틱) - [[raw/company-tech-blogs/lock-subskribe-advisory-lock-distributed-consensus]] — Subskribe production 사례: advisory lock 만으로 distributed mutual exclusion + optimistic try-lock only 교훈 (company-case-study — 공식 승격 금지) - [[raw/official-docs/cache-redisson-rlock-vs-setnx]] — (cache branch 와 공유) Redisson RLock/SETNX/Redlock 비교 + Kleppmann efficiency vs correctness (LOCK-C1~C4) ### Sub-branches (세부 작업) - (아직 없음) ### 오류 기록 (이 branch 작업 중 발생) - [[raw/errors/spring-integration-defaultlockrepository-aftersingletons-null-template-2026-06-13]] — `DefaultLockRepository` Spring 컨텍스트 외부 초기화 시 `afterSingletonsInstantiated()` 누락 → `readCommittedTransactionTemplate` NPE. Layer 4 `DistributedLockProviderContractTest` 작성 중 발생, resolved. ### 면접 준비 (이 작업에서 나올 수 있는 면접 질문) - "분산 락에서 lock 해제와 DB commit 의 순서가 왜 중요한가? lost-update race 를 설명하라" (D4 canonical pattern / forbidden inverse) - "efficiency lock 과 correctness lock 의 차이는 무엇인가? 왜 DB unique constraint 가 최종 방어선인가?" (D6) - "AutoCloseable 의 `close()` 가 `throws Exception` 인데, 왜 이 인터페이스는 그것을 재정의하여 unchecked 로 만들었는가?" - "tryLock(waitTime) + leaseTtl 조합이 무한 blocking 과 deadlock 을 어떻게 방지하는가?" (D5) - "finally 블록에서 예외를 던지면 왜 위험한가? 분산 락 해제 중 CME 를 re-throw 하지 않는 이유는?" (SI-LOCK-C5 / 정상 흐름 복귀) - "Decorator 패턴에서 wrapping lambda 로 handle 을 교체할 때 기존 동일성 테스트(`isSameAs`)가 왜 깨지는가?" (quality-review remediation — MeteredDistributedLockPort) ### 강의 (이 작업을 위해 학습한 강의) - (아직 없음) ### job-posting tie-ins (이 작업에서 파생된 글감) - "ShedLock 은 분산 락이 아니다 — maintainer 의 입으로 확인한 skip 시맨틱" (SHEDLOCK-899-C1/C2) - "분산 락과 트랜잭션: lock.close() 를 finally 에 두는 것만으로는 부족한 이유" (D4 forbidden inverse — commit 전 해제의 lost-update race) - "Clean Architecture 에서 분산 락 추상화 — DistributedLockPort 가 JdbcLockRegistry 를 숨기는 방법" (D2/D8 port 설계) - "Spring의 SmartInitializingSingleton: Spring 컨텍스트 없이 bean을 사용할 때 afterSingletonsInstantiated()를 직접 호출해야 하는 이유" (Layer 4 troubleshooting — DefaultLockRepository NPE) - "finally 블록에서 예외를 삼키는 게 맞을 때도 있다 — JdbcLock lease-expiry CME 처리와 정상 흐름 복귀" (SI-LOCK-C5 / quality-review finding 1) ## 관련 일일 노트 > 이 브랜치를 작업한 날짜들. 양방향 nav 유지. - (2026-06-12 생성 — daily 노트 미작성) ## 완료 후 정리 > 머지/종료 시점에 채움. `/ingest`가 이 섹션을 기준으로 wiki/projects/에 추출. - PR 링크: (미생성 — 사용자가 커밋·PR 수행) - 리뷰 메모: 2026-06-13 3단계 리뷰 체인 전부 `ready` — ca-architect-sentinel(PASS, 0 blocking/0 advisory: SI 가 adapter-persistence `implementation` 으로만 격리, app-bootstrap main 에 SI import 0, D8 모듈매트릭스 충족), ca-spec-reviewer(PASS, 요구 20/20 met, missing/extra/misinterpreted 0), ca-quality-reviewer(1차 NEEDS_FIX: Critical 1[SI-LOCK-C5] + Important 2 + Minor 2 → remediation 후 재리뷰 PASS, 0/0/0). - 머지 결과 / 배포 환경: **로컬 검증 완료** (Testcontainers PG Docker 가용 — 통합 테스트 SKIP 아님, 실제 실행). 최종 gradle 검증(2026-06-13): - `:shared-contract:test` / `:application-core:test` / `:adapter-persistence:test` — 전부 PASS - `:app-bootstrap:test` — 280개 중 lock 관련 21개(Metered 10 + Provider 6 + Classification 5) 전부 PASS. 유일한 실패는 **선행 커밋(d702572 등)에서 유래한 무관한 ArchUnit 위반** `outbound_adapter_method_returns_only_domain_or_primitives` (`OutboundHttpSettings.retry()/.circuitBreaker()` nested record) — `git stash` 후 clean HEAD 에서도 동일 실패 확인 → 본 branch 변경과 무관, 미수정(범위 밖, outbound branch 소유). - `verifyCleanArchitectureDependencies` / `verifyEnvKeys` — PASS (env 키 신규 0; `ca-skeleton.lock.*` 은 APP_ 비매핑 plain yaml). - registry 추가: `error-codes.yaml` `LOCK_ACQUISITION_TIMEOUT`(CONFLICT/409/retryable, D7) + `metrics.yaml` `lock.acquisition`(D7) + `lock.lease.expired`(§Edge/SI-LOCK-C5 — quality-review 후 추가, tagless counter). - **wiki 추출 대상** (verified만, `wiki/projects/`로만 추출): - `actually-implemented` + `locally-verified` 항목 (2026-06-13 현재): - `OperationalError.LOCK_ACQUISITION_TIMEOUT` (shared-contract) — Layer 1 - `DistributedLockPort` / `DistributedLock` / `LockAcquisitionTimeoutException` (application-core) — Layer 2 - 계약 테스트 10종 (application-core) — Layer 2 - `LockSettings` / `LockRegistryDistributedLockAdapter` / `DistributedLockPersistenceConfig` (adapter-persistence) — Layer 3 - `V4__int_lock.sql` (adapter-persistence) — Layer 3 - `LockRegistryDistributedLockAdapterTest` 5종 (adapter-persistence) — Layer 3 - `prod-verified` 항목: (없음) - **추출하지 않을 항목** (planned / documented-only / abandoned): - ArchUnit rule 호스팅 (`feature-architecture-enforcement-rules`) — planned - background-job ShedLock FQCN 전파 알림 — planned - **wiki/projects 추출 추가 대상** (quality-review remediation 이후 `actually-implemented` + `locally-verified`): - Layer 4 완료분: `MeteredDistributedLockPort` (SI-LOCK-C5 포함) + `DistributedLockConfig` + `DistributedLockProviderContractTest` 6종 (2026-06-13)