Files
llm-wiki/raw/branch-notes/feature-distributed-lock-contract.md

56 KiB
Raw Permalink Blame History

title, source_type, status, branch, parent_branch, related_projects, governing_docs, tags, created, target_merge, status_label, id, kind, project, work_item, inherits, refines, overrides, depends_on, contract_packet, contract_packet_sha256
title source_type status branch parent_branch related_projects governing_docs tags created target_merge status_label id kind project work_item inherits refines overrides depends_on contract_packet contract_packet_sha256
branch / feature-distributed-lock-contract branch-note raw feature-distributed-lock-contract
ca-skeleton
raw/project-notes/ca-skeleton-operational-contract
branch
ca-skeleton
distributed-lock
advisory-lock
lock-registry
2026-06-12 in-progress BR-CA-SKELETON-OPERATIONAL-CONTRACT-052 project-work-item ca-skeleton-operational-contract WI-CA-SKELETON-OPERATIONAL-CONTRACT-052
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
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
1 174d92e4290cde20c264638b526e3447c27c19eca0eab92d8023a03a66627754

branch: feature-distributed-lock-contract

Layer: raw/branch-notes/ — 단일 브랜치의 TODO·결정·진행 기록. 머지/종료 후 verified 결과는 /ingestwiki/projects/에 추출. 원본은 raw에 영구 보관. status_label: in-progress | review | merged | abandoned

[!important] 주도권 이전 고지 (2026-07-28) 아래 관심사의 owner 가 신규 branch 로 이동했다. 근거·절차: docs/superpowers/specs/2026-07-28-ca-skeleton-production-capability-feature-decomposition-design.md §5.

이전 ID 대상 D-row 이전한 관심사 신규 owner
H5 D6 correctness lock·fencing token (efficiency mutex 는 본 branch 유지) raw/branch-notes/feature-fenced-lock-coordination-contract

본문은 아직 제거하지 않았다. 신규 branch 는 현재 스캐폴딩 상태(D-row 없음)이므로, 지금 본문을 지우면 결정 근거가 소실된다. 위 D-row 는 신규 branch 의 /branch-spec 이 재판정을 마칠 때까지 잠정 근거로만 유효하며, 그 시점에 설계 §5.2 6단계에 따라 포인터(superseded → [[<신규 branch>]] D<m>)로 치환한다.

이 기간 중 새 결정을 본 branch 에 추가하지 않는다 — 신규 owner 에 작성한다.

부모 (필수)

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 인접 영역):

브랜치 계약 패킷

  • 생성 시 프로젝트 개정: 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

  • OperationalError.LOCK_ACQUISITION_TIMEOUT enum 상수 추가 (shared-contract) — 등급: actually-implemented / locally-verified (D7, 2026-06-13 이전 세션)
  • lock port 인터페이스 3종 정의 (application-core): DistributedLockPort, DistributedLock, LockAcquisitionTimeoutException — 등급: actually-implemented / locally-verified (D2/D4/D5/D6, 2026-06-13)
  • LockAcquisitionTimeoutExceptionTest + DistributedLockPortContractTest (application-core) — 등급: locally-verified (10/10 pass, 2026-06-13)
  • LockSettings @ConfigurationProperties("ca-skeleton.lock") record (adapter-persistence) — 등급: actually-implemented / locally-verified (D3/D5, 2026-06-13)
  • LockRegistryDistributedLockAdapter implements DistributedLockPort (adapter-persistence) — 등급: actually-implemented / locally-verified (D3/D4/D5, 2026-06-13)
  • DistributedLockPersistenceConfig Spring wiring (adapter-persistence) — in-process (@Primary, matchIfMissing) + JDBC conditional beans — 등급: actually-implemented / locally-verified (D3, 2026-06-13)
  • 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)
  • LockRegistryDistributedLockAdapterTest 5종 단위 테스트 (DefaultLockRegistry, no Spring context) — 등급: locally-verified (5/5 PASS, 2026-06-13)
  • lock.acquisition metric decorator MeteredDistributedLockPort (app-bootstrap) — 등급: actually-implemented / locally-verified (D7, 2026-06-13)
  • DistributedLockConfig @ConditionalOnProperty bean wiring (app-bootstrap) — distributedLockProvider @Primary, multi-instance=true 시만 활성 — 등급: actually-implemented / locally-verified (D1/D3, 2026-06-13)
  • ca-tmpl StartupSafetyValidatordistributedLockProvider 주석 owner 표기 갱신 (runtime-health → 본 branch) — 등급: actually-implemented / locally-verified (§Audit A1, 2026-06-13)
  • ca-skeleton.lock: wait-time: 3s / lease-ttl: 30s application.yml 기본값 배선 (app-bootstrap) — 등급: actually-implemented (D5, 2026-06-13)
  • MeteredDistributedLockPortTest 6종 단위 테스트 (app-bootstrap) — acquired/timeout/error + no-registry no-op — 등급: locally-verified (6/6 PASS, 2026-06-13)
  • LockAcquisitionTimeoutClassificationContractTest 5종 계약 테스트 (app-bootstrap) — enum SSOT + skip-not-pass registry/metrics — 등급: locally-verified (5/5 PASS, 2026-06-13)
  • DistributedLockProviderContractTest 4종 계약 테스트 (app-bootstrap) — D1 bean presence/absence + D3 mutual exclusion + D5 lease expiry (Testcontainers PG) — 등급: locally-verified (4/4 PASS, 2026-06-13)
  • 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 AutoCloseableclose() 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_waitTimeDuration.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 DistributedLockPortObjectProvider<MeterRegistry> 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.gradletestImplementation '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 컨텍스트 외부 초기화readCommittedTransactionTemplateInitializingBean.afterPropertiesSet() 이 아니라 SmartInitializingSingleton.afterSingletonsInstantiated() 에서 생성된다. Spring 컨텍스트 없이 쓸 때는 setTransactionManager()afterPropertiesSet()afterSingletonsInstantiated()start() 순서를 명시 호출해야 한다. 누락 시 D3/D5 테스트에서 CannotAcquireLockException(NullPointerException: readCommittedTransactionTemplate) 발생.
    • 사전 기존 ArchUnit 실패: outbound_adapter_method_returns_only_domain_or_primitivesOutboundHttpSettings.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.acquisitionregistry 에 없는 신규 제안 (기존 값 단정 아님, 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 Claimsraw/<category>/<slug>.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-coreDistributedLockPort (가칭): tryAcquire(String key, Duration waitTime, Duration ttl) → lock handle (j.u.c.Lock 호환) planned
adapter 구현 adapter-persistenceJdbcLockRegistry 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=truedistributedLockProvider 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 사용 패턴은 확인 필요
  • 다른 계약 의존:

검증해야 할 주장

공식 문서나 사례는 근거지만, 내 프로젝트에서의 동작을 자동으로 보장하지 않는다. 구현 전/중/후에 실제로 검증해야 하는 주장을 분리한다.

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에서 파생된 자료)

이 branch는 단일 노트가 아니라 작업 묶음의 entry point. 이 branch에서 파생된 모든 raw 노트를 카테고리별로 명시.

근거 자료

Sub-branches (세부 작업)

  • (아직 없음)

오류 기록 (이 branch 작업 중 발생)

면접 준비 (이 작업에서 나올 수 있는 면접 질문)

  • "분산 락에서 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)