--- title: branch / feature-transaction-concurrency-contract source_type: branch-note status: raw branch: feature-transaction-concurrency-contract parent_branch: related_projects: [ca-skeleton] governing_docs: [wiki/projects/ca-tmpl/transaction-boundary-abstraction] tags: [branch, ca-skeleton, transaction, concurrency, idempotency] created: 2026-05-21 target_merge: status_label: in-progress id: BR-CA-SKELETON-OPERATIONAL-CONTRACT-012 kind: project-work-item project: ca-skeleton-operational-contract work_item: WI-CA-SKELETON-OPERATIONAL-CONTRACT-012 inherits: [DEC-CA-SKELETON-OPERATIONAL-CONTRACT-TRANSACTION-001@1, DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-DATABASE-001@1] refines: [] overrides: [] depends_on: [] contract_packet: 1 contract_packet_sha256: f8840ebc8c3775ace9287ef6e88d907803b2e8d13db1a9a4c1da841d1f3abc29 --- # branch: feature-transaction-concurrency-contract > Layer: `raw/branch-notes/` — transaction boundary와 concurrency 실패 계약을 정의합니다. ## 부모 (필수) - **Parent project (canonical SSOT)**: [[raw/project-notes/ca-skeleton-operational-contract]] > ca-skeleton 은 별도 root branch 없이 project-note 가 SSOT 역할. 본 feature branch 는 project-note 의 운영 계약 중 해당 영역 (§14 Transaction/Concurrency) 의 결정/근거/금지 사항을 정제한다. > > **범위 정합 (2026-06-09 ground-truth 대조)**: TransactionPort abstraction 자체(`inWrite`/`inRead`/`inNew`, callback signature, `@Transactional` 금지 ArchUnit rule, `inNew` pool sizing)는 **[[raw/branch-notes/feature-application-port-usecase-contract]] (D3/D11/D12) 가 SSOT 이며 이미 구현·검증 완료(Phase C2)**. 본 branch 는 그 위에 얹는 **isolation 정책(D3) · lock-failure 분류 정책(D5) · idempotency 요구 정책(D6) · outbox trigger 정책(D7)** 의 *소비자/정책 계층*이다. D1/D2/D4 는 소비자 관점 재진술이며 원본 계약은 app-port branch 소유 (§Audit & Findings 참조). ## 브랜치 계약 패킷 - **생성 시 프로젝트 개정**: `1` - **패킷 스키마**: `contract_packet: 1` - **완료 조건**: transaction·concurrency failure fixture가 통과한다 ### 상속한 프로젝트 결정 | Decision Ref | Project Summary | Branch Application | Source | |---|---|---|---| | `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 | |---|---|---|---|---| ## 목표 운영 장애는 단순 DB unavailable보다 transaction boundary, lock, deadlock, duplicate command, retry 중복 write에서 자주 발생합니다. CA skeleton은 application use case 기준의 transaction/concurrency 규칙을 가져야 합니다. - 이슈: - PR: ## 범위 ### 포함 범위 - application use case transaction boundary. - read-only transaction 기준. - optimistic/pessimistic lock 실패 분류. - deadlock/lock timeout 분류. - duplicate command와 idempotent command 처리 기준. - retry 중복 write 방지 기준. - outbox pattern 도입 기준. ### 제외 범위 - business transaction 상세 설계. - distributed transaction 구현. - event sourcing 기본 탑재. ## 근거 (필수, 최소 1개+) > 본 branch의 결정 근거. 상세 비교는 §외부 근거 / 대안 조사 참조. | Source | 정당화하는 결정 | |---|---| | [[raw/official-docs/postgres-transaction-isolation-official]] | D3 — PostgreSQL READ COMMITTED 기본값 + statement/transaction-level snapshot 시맨틱 (`#PG-ISO-C1`~`#PG-ISO-C6`) | | [[raw/official-docs/mysql-innodb-transaction-isolation-official]] | D3 — MySQL InnoDB **기본값 = REPEATABLE READ** (Postgres 와 상이) + consistent/locking read 시맨틱 (`#MYSQL-ISO-C1`~`#MYSQL-ISO-C6`) | | [[raw/official-docs/spring-tx-management-reference]] | D1 자체-호출 함정(`#SPRING-TX-MGR-C5`) + D4 propagation REQUIRED default(`#SPRING-TX-MGR-C3`) + isolation/readOnly/timeout 적용 범위(`#SPRING-TX-MGR-C6`) | | [[raw/official-docs/spring-tx-propagation-required-new-nested-official]] | D4 — REQUIRED/REQUIRES_NEW/NESTED propagation 정확한 시맨틱 | | [[raw/company-tech-blogs/transaction-port-clean-ddd-spring-medium]] | D1 — UNIL의 동일 진화 경로 (2024-05, company-case-study) | | [[raw/company-tech-blogs/transaction-port-vassilis-soum-github-readme]] | D1 — TransactionPort 참고 구현 (company-case-study) | | [[raw/official-docs/at-transactional-spring-official]] | D1 — `@Transactional` 직접 부착 대안 + proxy self-invocation 함정 | | [[raw/company-tech-blogs/hexagonal-reflectoring-transactional-placement]] | D1 — Hexagonal 표준 다수파 (`@Transactional` 직접 부착, company-case-study) | | [[raw/official-docs/transaction-template-spring-official]] | D2 — programmatic `TransactionTemplate` 권장 패턴 | | [[raw/official-docs/functional-tx-arrow-kt-resource-docs]] | D1 대안 — Functional Resource monad (Arrow Kt) | | [[raw/company-tech-blogs/custom-transaction-interceptor-catnipcoder]] | D1 대안 — Custom TransactionInterceptor (AOP) | | [[raw/company-tech-blogs/woowahan-hexagonal-multimodule]] | 보완 — multi-module 분리 | 근거 자료가 raw에 아직 없다면 먼저 `raw-source-template` 또는 `lecture-note-template` 으로 raw에 등록한 뒤 여기서 링크. ## 외부 근거 / 대안 조사 (2026-05-22 — Topic 2; isolation 보강 2026-06-09) 본 branch의 transaction boundary + isolation + propagation 결정에 대한 외부 source 조사. 5종 대안 비교는 (예정) `wiki/concepts/transaction-boundary-abstraction.md` 참조. - **채택 결정 (TransactionPort abstraction)**: - [[raw/company-tech-blogs/transaction-port-clean-ddd-spring-medium]] — UNIL의 동일 진화 경로 (2024-05) - [[raw/company-tech-blogs/transaction-port-vassilis-soum-github-readme]] — TransactionPort 참고 구현 - **검토한 대안**: - **대안 1: @Transactional direct** — [[raw/official-docs/at-transactional-spring-official]], [[raw/company-tech-blogs/hexagonal-reflectoring-transactional-placement]] (Hexagonal 표준 다수파) - **대안 2: TransactionTemplate programmatic** — [[raw/official-docs/transaction-template-spring-official]] - **대안 3: Functional Resource monad** — [[raw/official-docs/functional-tx-arrow-kt-resource-docs]] (Arrow Kt) - **대안 4: Custom TransactionInterceptor (AOP)** — [[raw/company-tech-blogs/custom-transaction-interceptor-catnipcoder]] - **대안 5: TransactionalUseCaseRunner (별도 runner abstraction)** — **검토 후 미채택**. ca-tmpl 은 단일 `TransactionPort` abstraction 만 채택했고, 코드에 `TransactionalUseCaseRunner` 는 존재하지 않음 (governing doc `transaction-boundary-abstraction` L79 + ca-tmpl `src/` grep 으로 확인). D1 본문의 `TransactionalUseCaseRunner` 표현은 stale → §Audit & Findings `DRIFT-1`. - **보완 (대체 X)**: [[raw/company-tech-blogs/woowahan-hexagonal-multimodule]] — multi-module 분리 - **isolation 보강 (2026-06-09)**: D3 의 isolation default 근거가 cited raw 8종에 없어 vendor 공식 doc 2종 신규 수집 → [[raw/official-docs/postgres-transaction-isolation-official]] (`#PG-ISO-C1`: Postgres 기본 = READ COMMITTED) + [[raw/official-docs/mysql-innodb-transaction-isolation-official]] (`#MYSQL-ISO-C1`: MySQL InnoDB 기본 = REPEATABLE READ). **두 vendor 의 기본 isolation 이 다르다는 사실** 이 "묵시적 vendor default 사용 forbidden, 명시 pin 강제" 정책의 핵심 근거. - **비교 핵심**: ca-tmpl 결정은 "Spring 의존 숨김" 소수파. 다수파는 `@Transactional` 직접 부착(testability 낮지만 boilerplate 최저). Functional monad는 testability 최고지만 팀 학습 비용 큼. concurrency 관점에서 isolation default(READ_COMMITTED), propagation REQUIRED 1택은 5종 abstraction 대안 어디서도 직접 비교 source 부재 — Spring 공식 기본값 + vendor 공식 isolation 시맨틱을 따른 결정. ## TODO > TODO drained — 결정은 아래 표/결정 사항 참조. ## 진행 중 메모 - transaction policy는 repository capability와 연결되어야 합니다. ## 결정 사항 (decisions) - 2026-05-21: transaction boundary는 application use case 기준으로 검토. - 2026-05-22: transaction abstraction의 SSOT는 `feature-application-port-usecase-contract`이며, 이 branch는 lock/isolation/retry/idempotency 분류를 소비자 관점에서 정의. - 2026-05-22: application package의 Spring `@Transactional` 직접 import는 금지. transaction 실행은 `TransactionPort` 또는 `TransactionalUseCaseRunner` 구현체를 통해 수행. - 2026-05-22: isolation level default = `READ_COMMITTED` (PostgreSQL/MySQL 양쪽 동일 의미). write-heavy use case는 명시 선언으로 REPEATABLE_READ/SERIALIZABLE 허용. 묵시적 vendor default 사용은 forbidden. - 2026-05-22: propagation default = REQUIRED 1택. REQUIRES_NEW는 outbox/audit row 분리 케이스에 한해 명시 선언 시만 허용. NESTED/NEVER 등 묵시 사용은 forbidden. - 2026-06-09 (정합 보강): `TransactionalUseCaseRunner` 는 미채택 대안 — 코드 미존재(§Audit `DRIFT-1`). isolation "PostgreSQL/MySQL 양쪽 동일 의미" 는 부정확 — 두 DB **기본값이 다름**(Postgres=READ COMMITTED, MySQL InnoDB=REPEATABLE READ)이라서 명시 pin 이 필요하다는 것이 정확한 근거(§Audit `DRIFT-2`). ## 결정-근거 매핑 > 각 결정이 어떤 raw source claim 으로 뒷받침되는지 명시. company-tech-blog 는 `company-case-study` 로만 라벨 (official best practice 단정 금지). > `선택 조건` 열: "이 조건일 때 이 결정, 다른 조건이면 어떤 대안". 분기 없으면 N/A. | Decision ID | Decision (요약) | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk | |---|---|---|---|---|---| | D1 | transaction boundary 는 application use case 기준. application package 의 Spring `@Transactional` 직접 import 금지 — `TransactionPort` / `TransactionalUseCaseRunner` 구현체로만 실행 | N/A (모든 application use case 항상) | `raw/official-docs/at-transactional-spring-official.md#AT-TX-C1` (concrete class 부착 권장), `#AT-TX-C2` (interface annotation AspectJ silently ignored), `#AT-TX-C5` (proxy self-invocation 함정), `raw/official-docs/transaction-template-spring-official.md#TX-TMPL-C1`, `#TX-TMPL-C2` (programmatic callback 권장), `raw/official-docs/spring-tx-management-reference.md#SPRING-TX-MGR-C2` (`PlatformTransactionManager` 는 SPI — application code 에서 직접 사용 + mock/stub 가능), `#SPRING-TX-MGR-C5` (proxy mode default 에서 self-invocation 은 `@Transactional` 우회 — UseCase 외부 호출 강제 근거), `raw/company-tech-blogs/transaction-port-clean-ddd-spring-medium.md`, `raw/company-tech-blogs/transaction-port-vassilis-soum-github-readme.md` (company-case-study — UNIL 동일 진화 경로 + TransactionPort 참고 구현) | `official-vendor-doc` (AT-TX-C1/C2/C5, TX-TMPL-C1/C2, SPRING-TX-MGR-C2/C5) + `company-case-study` (UNIL / Vassilis Soum) | **OWNERSHIP**: TransactionPort + `@Transactional` 금지 ArchUnit rule 은 [[raw/branch-notes/feature-application-port-usecase-contract]] D3 가 SSOT 이며 *이미 구현·검증 완료* (code: `application-core/.../transaction/TransactionPort.java`, `app-bootstrap/.../CleanArchitectureTest.java` L167-175 — 주석에 "[[raw/branch-notes/feature-application-port-usecase-contract]] D3"). 본 row 는 소비자 재진술. `TransactionalUseCaseRunner` 는 코드 미존재(§Audit `DRIFT-1`). Spring 공식은 `@Transactional` 함정만 명시 — clean/hexagonal 양립성 평가는 cited raw 범위 밖. TransactionPort 채택은 소수파. `SPRING-TX-MGR-C5` 는 AspectJ mode 동일 우회 의미 아님 | | D2 | TransactionPort adapter 는 내부적으로 `TransactionTemplate.execute(...)` 사용 (programmatic 권장 패턴) | N/A (adapter 구현 항상) | `raw/official-docs/transaction-template-spring-official.md#TX-TMPL-C1` (callback 접근법으로 boilerplate 제거), `#TX-TMPL-C2` (Spring 팀 권장: imperative=TransactionTemplate, reactive=TransactionalOperator), `#TX-TMPL-C3` (TransactionCallback + execute() 패턴), `#TX-TMPL-C4` (setRollbackOnly() 명시적 rollback) | `official-vendor-doc` | **OWNERSHIP**: `SpringTransactionPort` (adapter-persistence) 가 모드별 `TransactionTemplate` 3개를 미리 빌드 — 코드 확인(actually-implemented), app-port branch 소유. 본 row 는 소비자 재진술. adapter 내부 self-invocation 함정(D1 `#AT-TX-C5`) 이 TransactionTemplate 경로에서 어떻게 처리되는지 별도 검증 필요 | | D3 | isolation level default = `READ_COMMITTED` (명시 pin). write-heavy use case 는 명시 선언으로 REPEATABLE_READ/SERIALIZABLE 허용. 묵시적 vendor default 사용 forbidden | write-heavy / read-consistency 필요 use case → 명시 REPEATABLE_READ/SERIALIZABLE; 그 외 모든 use case → READ_COMMITTED default. READ_UNCOMMITTED → forbidden | `raw/official-docs/postgres-transaction-isolation-official.md#PG-ISO-C1` (Postgres 기본 = READ COMMITTED), `#PG-ISO-C2` (statement-level snapshot), `#PG-ISO-C3` (REPEATABLE READ = tx-level snapshot), `#PG-ISO-C4` (serialize 실패 에러), `#PG-ISO-C5` (SERIALIZABLE = SSI), `#PG-ISO-C6` (내부 3 레벨, READ UNCOMMITTED=READ COMMITTED); `raw/official-docs/mysql-innodb-transaction-isolation-official.md#MYSQL-ISO-C1` (**InnoDB 기본 = REPEATABLE READ**), `#MYSQL-ISO-C4` (READ COMMITTED = fresh snapshot per read), `#MYSQL-ISO-C2/C3` (REPEATABLE READ snapshot + gap lock) | `official-vendor-doc` (PostgreSQL + MySQL 공식) | 두 vendor **기본값이 다름**(Postgres READ COMMITTED vs MySQL InnoDB REPEATABLE READ)이 명시 pin 필요성의 근거. ca-tmpl `Isolation` enum 은 현재 `READ_COMMITTED` **단일값만 노출**(code 확인) — REPEATABLE_READ/SERIALIZABLE 노출 + per-use-case 선택 메커니즘은 본 branch 미구현(`planned`). READ_COMMITTED 의 non-repeatable read/phantom 허용 trade-off 는 read-then-write use case 에서 lost-update 위험 (§구현 가이드 1) | | D4 | propagation default = REQUIRED 1택. REQUIRES_NEW 는 outbox/audit row 분리 명시 선언 시만. NESTED/NEVER 묵시 사용 forbidden | 일반 use case → REQUIRED; outbox/audit row 분리 필요 → 명시 REQUIRES_NEW (`inNew`); NESTED/NEVER → forbidden | `raw/official-docs/spring-tx-management-reference.md#SPRING-TX-MGR-C3` (`@Transactional` default propagation = `PROPAGATION_REQUIRED` verbatim), `#SPRING-TX-MGR-C6` (isolation/readOnly/timeout 은 REQUIRED/REQUIRES_NEW 한정), `raw/official-docs/spring-tx-propagation-required-new-nested-official.md` (REQUIRED/REQUIRES_NEW/NESTED 정확한 시맨틱) | `official-vendor-doc` (Spring Framework Reference verbatim) | **OWNERSHIP**: code 확인 — `SpringTransactionPort` inWrite/inRead=REQUIRED, inNew=REQUIRES_NEW (actually-implemented); `inNew` pool-sizing 공식은 app-port D12 소유. NESTED/NEVER 금지 자체는 ca-tmpl 내부 결정 — Spring 공식 prescribe 아님 | | D5 | optimistic lock conflict 409 vs deadlock/timeout retryable by policy. all locks generic 500 금지 | optimistic(@Version) 충돌 → 409 client non-retryable; deadlock(40P01)/serialization(40001) → retryable by policy; pessimistic lock → 명시 시만 | `raw/official-docs/postgres-transaction-isolation-official.md#PG-ISO-C4` (REPEATABLE READ serialize 실패 → 재시도), `raw/official-docs/transaction-template-spring-official.md#TX-TMPL-C4` (명시적 rollback) + **위임**: [[raw/branch-notes/feature-persistence-failure-baseline]] D6 (`raw/official-docs/persistence-spring-dataaccessexception-hierarchy.md#SDA-EX-C5` optimistic locking failure 예시; SQLState 40001→`DB_SERIALIZATION_FAILURE`, 40P01→`DB_DEADLOCK`, 둘 다 category `CONFLICT`·retryable, 23505→`DB_UNIQUE_VIOLATION`) | `official-vendor-doc` (transaction boundary) + `cross-branch-delegation` (persistence-failure-baseline D6 — exception→error-code 매핑 SSOT) | 본 branch 는 **정책(409 vs retryable)** 만 소유 — exception→error-code 매핑은 persistence baseline 소유. error-codes.yaml 에 *optimistic-lock 전용* code 부재(`DB_SERIALIZATION_FAILURE`/`DB_DEADLOCK`/`DB_UNIQUE_VIOLATION` 만) → optimistic `@Version` 충돌의 정확한 code 매핑은 registry gap(§구현 가이드 2). code 확인: `@Version` on `WorkLogEntity` (actually-implemented); pessimistic lock / lock-timeout 코드 NOT FOUND | | D6 | duplicate command → idempotency branch key scope. retryable write without idempotency forbidden | 동일 idempotency key 재도착 → dedupe(sibling 소유); key 없는 mutating command 의 retryable write → forbidden(본 branch 정책) | **위임**: [[raw/branch-notes/feature-rate-limit-idempotency-contract]] D2 (key scope = `(authenticatedPrincipal, idempotencyKey, useCaseName)` triple + tenant), D3 (dedup 저장), D6 (TTL 24h), D7 (in-flight → 409 `IDEMPOTENT_IN_FLIGHT`), D8 (fingerprint mismatch → 422 `IDEMPOTENT_REQUEST_MISMATCH`). error-codes.yaml: `IDEMPOTENT_IN_FLIGHT`(409)·`IDEMPOTENT_REQUEST_MISMATCH`(422) owner_layer application | `cross-branch-delegation` (rate-limit-idempotency 가 key scope/TTL/dedup/in-flight/mismatch 메커니즘 SSOT) | 본 branch 는 **"non-idempotent retryable write 금지" 정책만** 소유 — idempotency 메커니즘은 sibling SSOT. code: `@UseCaseCapability(idempotency=IDEMPOTENT\|KEYED\|NOT_IDEMPOTENT)` enum 존재, `KEYED` 는 rate-limit merge 전까지 ArchUnit 으로 freeze. 어떤 use case 가 idempotency 선언을 *요구*하는지는 도메인 결정(§구현 가이드 3) | | D7 | outbox required for atomic external publish. DB commit then lossy publish 금지 | external publish 필요 use case → outbox; internal-only domain event → outbox 불필요 | **위임**: [[raw/branch-notes/feature-domain-event-outbox-contract]] D2 (transaction+publish atomicity = outbox default), D4 (SKIP LOCKED leadership), D9 (publisher claim tx = READ_COMMITTED + FOR UPDATE SKIP LOCKED) | `internal-cross-reference` (outbox 메커니즘 SSOT = domain-event-outbox-contract) | outbox 메커니즘 (SKIP LOCKED polling vs CDC) 의 근거는 [[raw/branch-notes/feature-domain-event-outbox-contract]] Decision Evidence Map 참조. D9 의 claim tx isolation(READ_COMMITTED) 이 본 branch D3 default 와 일치 — cross-vendor 일관성 확인 완료 | ## 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` 승급 위치 | ## 판정 기준 | 구분 | 기준 | | --- | --- | | Decision | transaction execution은 application port abstraction으로 통과 | | Allowed | read-only query는 `readOnly` mode만 선언 가능. infra implementation은 Spring transaction 사용 가능 | | Forbidden | application use case의 direct `@Transactional`, hidden write transaction, idempotency 없는 retryable write | | Required fields | transaction mode, isolation exception 여부, retryable 여부, idempotency key scope | | Failure condition | transaction/capability/idempotency 선언 없이 write repository 접근이 가능하면 실패 | ## Decisionized Work Items | item | Decision | Allowed | Forbidden | Required test | | --- | --- | --- | --- | --- | | boundary | application use case via TransactionPort | infra adapter uses Spring tx | direct application `@Transactional` | forbidden import test | | read-only | query mode `readOnly` | no transaction for pure in-memory query | write in read-only use case | read-only test | | lock failures | optimistic conflict vs retryable deadlock/timeout | explicit pessimistic lock | all locks generic 500 | lock mapping test | | duplicate command | idempotency branch key scope | non-idempotent command explicit conflict | retryable write without idempotency | duplicate write test | | outbox | required for atomic external publish | internal-only domain event no outbox | DB commit then lossy publish | outbox atomicity test | | isolation | READ_COMMITTED default | explicit REPEATABLE_READ/SERIALIZABLE for write-heavy | vendor default 묵시 사용 | isolation contract test | | @Transactional propagation | REQUIRED | 명시된 REQUIRES_NEW (outbox/audit row 분리) | NESTED/NEVER 묵시 사용 | propagation contract test | ## 구현 가이드 > *결정* 이 "*무엇*" 이라면 본 §는 "*어디에 어떻게*" 의 사전 명세. 본 branch 의 *고유 소유 결정(D3·D5·D6·D7)* 만 in-scope. boundary/template/propagation 메커니즘(D1·D2·D4)은 `feature-application-port-usecase-contract` 가 SSOT 이므로 §구현 가이드에 명세하지 않고 §엣지·의존 + §Audit 에 위임 기록만 남긴다 (R3 OUT_OF_BRANCH_SCOPE). > > code anchor 는 2026-06-09 ca-tmpl ground-truth grep 으로 확인. `actually-implemented` 는 `src/` 에서 확인된 것, 그 외는 `planned`. ### 1. Isolation level 선택 메커니즘 (D3 — 본 branch 핵심 소유) > **Trace**: D3 ← `#PG-ISO-C1`~`C6`, `#MYSQL-ISO-C1`~`C4`. 현재 code: `application-core/.../transaction/Isolation.java` = `READ_COMMITTED` 단일값(actually-implemented); `adapter-persistence/.../transaction/SpringTransactionPort.java` L76 = 3 template 모두 `ISOLATION_READ_COMMITTED` pin (actually-implemented). > > - **UNSUPPORTED_IMPL_DECISION**: REPEATABLE_READ/SERIALIZABLE 을 *어떻게 노출* 할지(① `Isolation` enum 확장 + `TransactionPort.inWrite` 에 isolation 파라미터 추가, ② `@UseCaseCapability(isolation=...)` 속성 추가, ③ 새 TransactionPort 오버로드) — vendor doc 은 *어떤 레벨이 존재/무엇을 보장* 하는지만 근거. ca-tmpl 노출 API 모양은 근거 없음. trade-off: capability 속성 = ArchUnit 정적 강제 가능하나 use-case 단위 coarse; 메서드 파라미터 = fine-grained 하나 런타임. **권고 기본값: ② capability 속성** (기존 `transactionMode` 와 동일한 정적 강제 경로 재사용). > - **변경 파일 후보** (착수 시 헤매지 않도록): `application-core/.../transaction/Isolation.java`(enum 확장 — 현재 `READ_COMMITTED` 단일 상수), `adapter-persistence/.../transaction/SpringTransactionPort.java`(현재 3개 `TransactionTemplate` 이 `ISOLATION_READ_COMMITTED` 고정 pin → isolation 별 라우팅 필요), `application-core/.../capability/UseCaseCapability.java`(② 채택 시 속성 추가) + 대응 ArchUnit rule. **이 abstraction 은 app-port branch 가 SSOT 이므로 REPEATABLE_READ/SERIALIZABLE 실제 노출은 `feature-application-port-usecase-contract` 와 공동 PR 필요** — 그 전까지 호출 경로는 `planned`. | level | 언제 | Postgres 시맨틱 (claim) | MySQL InnoDB 시맨틱 (claim) | ca-tmpl 상태 | |---|---|---|---|---| | READ_COMMITTED | default (모든 use case) | statement 시작 시점 snapshot (`#PG-ISO-C2`) | 매 consistent read 마다 fresh snapshot (`#MYSQL-ISO-C4`) | `actually-implemented` (enum + pin) | | REPEATABLE_READ | write-heavy / read 일관성 필요, 명시 | tx 시작 snapshot 고정; write 충돌 시 serialize 에러 (`#PG-ISO-C3`,`#PG-ISO-C4`) | tx 첫 read snapshot 재사용; locking read 시 gap/next-key lock (`#MYSQL-ISO-C2`,`#MYSQL-ISO-C3`) | `actually-implemented` (enum 노출, 2026-06-09); call-path 라우팅은 `planned` (app-port 공동 PR) | | SERIALIZABLE | 최강 격리, 명시 | SSI — anomaly 시 serialization failure (`#PG-ISO-C5`) | autocommit=0 시 plain SELECT→`FOR SHARE` 묵시 변환 (`#MYSQL-ISO-C6`) | `actually-implemented` (enum 노출, 2026-06-09); call-path 라우팅은 `planned` | | READ_UNCOMMITTED | **forbidden** | 내부적으로 READ COMMITTED 로 매핑 (`#PG-ISO-C6`) | (해당) | `forbidden` (enum 제외) | - **핵심 근거**: Postgres 기본 = READ COMMITTED(`#PG-ISO-C1`), MySQL InnoDB 기본 = REPEATABLE READ(`#MYSQL-ISO-C1`) → **기본값이 vendor 마다 다름** → 묵시 vendor default 위임 시 동일 코드가 DB 따라 다른 격리 → 명시 pin 강제. 이것이 D3 forbidden 정책의 근거. ### 2. Lock-failure 분류 정책 (D5 — persistence baseline 소비) > **Trace**: D5 ← [[raw/branch-notes/feature-persistence-failure-baseline]] D6 (`#SDA-EX-C5`) + `#PG-ISO-C4`. 본 branch 는 *분류 정책* 만 소유; exception→error-code *매핑* 은 persistence baseline 소유. > > - **UNSUPPORTED_IMPL_DECISION**: optimistic `@Version` 충돌의 정확한 error code — error-codes.yaml 에 optimistic 전용 code 부재(`DB_SERIALIZATION_FAILURE`/`DB_DEADLOCK`/`DB_UNIQUE_VIOLATION` 만, owner=persistence-baseline). 신규 `OPTIMISTIC_LOCK_CONFLICT` code 추가 vs 기존 generic CONFLICT 재사용 — registry 결정이며 owner_branch=persistence-baseline 이므로 **본 branch 는 정책 요구만, code 신설은 persistence baseline 으로 이관**(R3). | 실패 유형 | 정책 (본 branch 소유) | error-code 매핑 (persistence baseline 소유) | code 확인 | |---|---|---|---| | optimistic lock (`@Version`) | 409, client non-retryable | (registry gap — 신규 제안 필요) | `@Version` on `WorkLogEntity` = `actually-implemented`. ⚠️ JPA `@Version` flush 시 `OptimisticLockingFailureException` 변환 경로는 persistence-baseline D6 `#SDA-EX-C7`(sql-error-codes.xml 매핑) needs-confirmation 해소 전까지 `planned` — integration test 로만 검증 가능 | | deadlock | retryable by policy | `40P01`→`DB_DEADLOCK` (CONFLICT, 409, retryable) | error-codes.yaml = `actually-implemented` | | serialization failure | retryable by policy | `40001`→`DB_SERIALIZATION_FAILURE` (CONFLICT, retryable) | error-codes.yaml = `actually-implemented` | | unique violation | 충돌 (non-retryable) | `23505`→`DB_UNIQUE_VIOLATION` (CONFLICT, non-retryable) | error-codes.yaml = `actually-implemented` | | pessimistic lock / lock-timeout | 명시 선언 시만 | (코드/registry 부재) | `planned` (`NOT FOUND` in src/) | | **forbidden** | 모든 lock 실패를 generic 500 으로 뭉갬 | — | — | ### 3. Idempotency 요구 정책 (D6 — rate-limit-idempotency 소비) > **Trace**: D6 ← [[raw/branch-notes/feature-rate-limit-idempotency-contract]] D2/D3/D6/D7/D8. 본 branch 는 *"non-idempotent retryable write 금지"* 정책만 소유; key scope/TTL/dedup/in-flight/mismatch 메커니즘은 rate-limit branch SSOT. > > - **UNSUPPORTED_IMPL_DECISION**: *어떤 use case 가* idempotency 선언을 요구하는지 — 도메인 결정이며 ca-tmpl skeleton 이 prescribe 불가. 신규 use case 작성 시 `@UseCaseCapability(idempotency=...)` 선언을 ArchUnit 으로 강제하되 값 선택은 도메인 작성자. trade-off: 전 use case 강제 선언 = 누락 방지하나 NOT_IDEMPOTENT 보일러플레이트; 옵트인 = 가볍지만 누락 위험. **권고: 전 use case 선언 강제**(기존 `inbound_port_implementations_declare_capability` rule 과 일치). - code 확인: `@UseCaseCapability(idempotency = IDEMPOTENT | KEYED | NOT_IDEMPOTENT)` enum = `actually-implemented`. `KEYED` 는 rate-limit merge 전까지 ArchUnit `inbound_port_implementations_do_not_declare_keyed_idempotency` 로 freeze (`planned`/의도적 차단). - 본 branch 책임: "retryable 로 분류된 write use case 가 idempotency 선언 없이 재시도 경로에 노출되면 실패" 계약 test (아래 §테스트 계약). ### 4. Outbox trigger 정책 (D7 — domain-event-outbox 소비) > **Trace**: D7 ← [[raw/branch-notes/feature-domain-event-outbox-contract]] D2/D9. 본 branch 는 *"external publish 는 outbox 경유, DB commit 후 lossy publish 금지"* trigger 정책만 소유; outbox 메커니즘(SKIP LOCKED/CDC)은 outbox branch SSOT. - outbox publisher claim transaction 이 READ_COMMITTED(outbox D9) 를 쓰므로 본 branch D3 default 와 일치 — isolation 일관성 확인됨. - `planned` — outbox 메커니즘 미구현(`feature-domain-event-outbox-contract` status=raw). ## 엣지·실패·의존 > R4(깊이 게이트) 캡처용. 정상 경로 외 *구현 중 부딪힐* 실패/엣지/다른 계약 의존. - **실패·엣지 경로**: - READ_COMMITTED 하 read-then-write use case → non-repeatable read/phantom 으로 **lost update** 위험(`#PG-ISO-C2`,`#MYSQL-ISO-C4`). 기대 동작: 명시 REPEATABLE_READ 선언 또는 `SELECT ... FOR UPDATE`(pessimistic) 로 보호. skeleton 은 위험만 문서화, 도메인 use case 가 선택. - REPEATABLE_READ/SERIALIZABLE 선택 시 serialization failure(Postgres "could not serialize access", `#PG-ISO-C4`/`#PG-ISO-C5`) → retryable. 기대 동작: 호출측 retry 정책 필요(현재 미구현 `planned`). - MySQL REPEATABLE_READ locking read 의 gap/next-key lock(`#MYSQL-ISO-C3`) → deadlock 빈도 증가. 기대 동작: D5 deadlock 분류(retryable) 로 흡수. - `inNew`(REQUIRES_NEW) 를 loop 내 호출 → connection pool 고갈(app-port D12 anti-pattern). 기대 동작: ArchUnit/리뷰로 차단(app-port 소유). - optimistic `@Version` 충돌이 generic 500 으로 뭉개짐 → D5 위반, 계약 test 실패. - **REPEATABLE_READ/SERIALIZABLE serialization failure 재시도 ↔ D6 idempotency 충돌**: serialization failure(`#PG-ISO-C4`) 의 retry 가 idempotency key 없는 mutating command 에서 발화하면 D6 "non-idempotent retryable write forbidden" 에 해당. 기대 동작: KEYED idempotency 선언된 use case 에 한해 재시도 허용 — `NOT_IDEMPOTENT` use case 의 REPEATABLE_READ/SERIALIZABLE 선언 + 자동 retry 는 사실상 forbidden. - **다른 계약 의존**: - [[raw/branch-notes/feature-application-port-usecase-contract]] 의 `D3`(TransactionPort abstraction)·`D11`(callback signature)·`D12`(`inNew` REQUIRES_NEW + pool sizing) 에 의존 — 본 branch 는 그 위에 isolation 정책만 추가. 그 계약이 바뀌면 본 branch D3/D4 영향. - [[raw/branch-notes/feature-persistence-failure-baseline]] 의 `D6`(40001/40P01→error-code, optimistic `#SDA-EX-C5`) 에 의존 — D5 가 exception→code 매핑 consume. 매핑이 바뀌면 D5 분류 표 영향. - [[raw/branch-notes/feature-rate-limit-idempotency-contract]] 의 `D2`(key scope)·`D7`(in-flight 409)·`D8`(mismatch 422) 에 의존 — D6 가 idempotency 메커니즘 consume. - [[raw/branch-notes/feature-domain-event-outbox-contract]] 의 `D2`/`D9`(outbox + claim tx READ_COMMITTED) 에 의존 — D7 가 outbox trigger consume. - [[raw/branch-notes/feature-repository-access-permission-contract]] 의 `@UseCaseCapability(transactionMode/repositoryAccess)` 에 의존 — read-only(readOnly) + write repository 정책 consume. ## 테스트 계약 - write use case가 transaction 없이 repository write를 수행하면 실패. - application use case가 Spring transaction annotation을 직접 import하면 실패. - read-only use case가 write repository를 사용하면 실패. - optimistic lock 실패가 internal error로 뭉개지면 실패. - idempotent command 재시도 시 중복 row/write가 발생하면 실패. - TransactionPort 사용 use case에서 isolation을 명시하지 않은 채 vendor default에 위임하면 실패. - application use case의 @Transactional propagation이 NESTED 또는 NEVER로 명시되면 실패. ## 검증해야 할 주장 > 공식 문서 인용이 결정의 근거이지만, ca-tmpl 자체 구현에서의 동작은 별도 검증이 필요한 주장. | Claim | Why uncertain | How to verify | Status | |---|---|---|---| | TransactionPort adapter 가 Spring bean 외부에서 호출되어 self-invocation 함정 (`#AT-TX-C5`) 회피 | `#AT-TX-C5` 는 proxy mode 의 self-invocation 함정만 명시 — adapter call path 가 실제로 외부 호출인지 별도 보장 필요 | adapter bean 호출 경로 trace + Spring AOP proxy 적용 여부 단언 integration test | `planned` | | application 패키지가 `org.springframework.transaction.annotation.Transactional` 또는 `org.springframework.transaction.support.TransactionTemplate` 을 import 하지 않음 | cited raw 는 framework 의 권고만 보장 — ca-tmpl 내부 강제는 별도. **code 확인: `CleanArchitectureTest.application_does_not_use_spring_transactional_annotation` (L167-175) = actually-implemented (app-port D3 소유)** | ArchUnit rule 존재 확인 완료; 의도적 위반 fixture 로 fail 검출은 `ArchitectureViolationFixtureTest` 에서 확인 | `locally-verified` (app-port branch) | | isolation level `READ_COMMITTED` 가 PostgreSQL 과 MySQL InnoDB 에서 ca-tmpl 이 가정한 시맨틱과 동일 동작 (D3) | ~~UNSUPPORTED~~ **해소** — vendor doc verbatim 수집 완료. 단 "양쪽 동일 의미" 는 **부정확**: 기본값이 다름(Postgres READ COMMITTED `#PG-ISO-C1` vs InnoDB REPEATABLE READ `#MYSQL-ISO-C1`). ca-tmpl 은 명시 pin 으로 vendor 차이 무력화 | code 확인: `SpringTransactionPort` 가 `ISOLATION_READ_COMMITTED` pin (actually-implemented). 실 DB 에서 READ_COMMITTED 시맨틱(non-repeatable read 허용) 재현은 Testcontainers integration test 로 검증 필요 | `needs-confirmation` (vendor 시맨틱 verified, ca-tmpl 실 DB 동작 미검증) | | propagation REQUIRED 가 모든 ca-tmpl use case 의 default 시맨틱과 일치 (D4) | ~~UNSUPPORTED~~ **해소** — `#SPRING-TX-MGR-C3` (`PROPAGATION_REQUIRED` default verbatim) + `spring-tx-propagation-required-new-nested-official` 수집. code: inWrite/inRead=REQUIRED (actually-implemented) | `SpringTransactionPortTest` 가 모드별 propagation 설정값 단언(app-port branch, locally-verified) | `locally-verified` (app-port branch) | | optimistic lock 실패가 application use case 에서 `OptimisticLockingFailureException` (또는 동등) 으로 식별되어 409 매핑 (D5) | cited transaction raw 범위 밖 — persistence raw 의 `#SDA-EX-C5` 와 cross-reference. error-codes.yaml 에 optimistic 전용 code 부재(registry gap) | integration test: `@Version` 충돌 시나리오에서 `OptimisticLockingFailureException` 발생 + handler 가 409 매핑 단언 | `planned` | | duplicate command idempotency 검증 (D6: 동일 idempotency key 로 retry 시 중복 row/write 없음) | ~~UNSUPPORTED~~ **위임** — 메커니즘은 [[raw/branch-notes/feature-rate-limit-idempotency-contract]] D2/D7/D8 SSOT. 본 branch 는 "non-idempotent retryable write 금지" 정책만 | contract test: 동일 idempotency key 로 5회 retry → DB row 1개만 생성 + 응답 동일 단언 (rate-limit branch 구현 후) | `planned` | | ArchUnit forbidden import test (application 의 `@Transactional` direct annotation) 가 실제 위반 검출 | rule 정의 자체는 명확하지만 실제 적용 미검증. **code 확인: rule + violation fixture 존재** | `ArchitectureViolationFixtureTest` 가 의도된 위반 fixture 를 잡아냄 (app-port branch) | `locally-verified` (app-port branch) | | Vassilis Soum / UNIL TransactionPort 참고 구현 (D1 의 company-case-study) 이 ca-tmpl 환경에서 동작 보장 | company-case-study 는 한 조직의 사례 — 우리 환경에서의 적합성 별도 검증 필요. **code 확인: `TransactionPort` + `SpringTransactionPort` 실재(actually-implemented, app-port branch)** | 모든 use case 가 `TransactionPort.inWrite/inRead/inNew(...)` 경유 — `SpringTransactionPortTest` 통과 (app-port branch, locally-verified) | `locally-verified` (app-port branch) | ## 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때) > `/coverage` 가 채우는 **생성물** — 손으로 유지하지 않는다. governing 문서(frontmatter `governing_docs`: `wiki/projects/ca-tmpl/transaction-boundary-abstraction`)가 요구하는 관심사를 이 브랜치가 빠짐없이 덮는지의 결과. 기준: `rules/coverage-gate.md`. > 상태: `covered-here`(이 브랜치 결정) / `delegated`(다른 owner 브랜치) / `missing`(아무도 안 맡음 → Blocking). | 관심사 | 상태 | owner | 심각도 | 근거 | |--------|------|-------|--------|------| | C1: TransactionPort abstraction (`inWrite`/`inRead`/`inNew`) + `@Transactional` 금지 ArchUnit rule | delegated | [[raw/branch-notes/feature-application-port-usecase-contract]] (D3/D11/D12) | OK | D1 Open Risk OWNERSHIP + §엣지·의존 링크 | | C2: SpringTransactionPort 내부 `TransactionTemplate` 사용 | delegated | [[raw/branch-notes/feature-application-port-usecase-contract]] (D3) | OK | D2 Open Risk OWNERSHIP + §엣지·의존 링크 | | C3: `Isolation` enum — READ_COMMITTED pin, READ_UNCOMMITTED forbidden | covered-here | — | — | D3 + §구현가이드 1; `Isolation.java` actually-implemented (code) | | C4: Propagation 정책 — REQUIRED default, REQUIRES_NEW 조건, NESTED/NEVER forbidden | delegated | [[raw/branch-notes/feature-application-port-usecase-contract]] (D3/D12) | OK | D4 Open Risk OWNERSHIP; §엣지·의존 링크 | | C5: Isolation 선택 정책 — 명시 pin 강제, vendor default forbidden, REPEATABLE_READ/SERIALIZABLE 경로 | covered-here | — | — | D3 + §구현가이드 1 (UNSUPPORTED_IMPL_DECISION 3옵션 기록) | | C6: Lock-failure 분류 정책 — optimistic 409, deadlock/serialization retryable, generic-500 forbidden | covered-here | — | — | D5 + §구현가이드 2; `@Version` WorkLogEntity actually-implemented (code) | | C7: exception→error-code 매핑 (40001/40P01/optimistic `@Version`) | delegated | [[raw/branch-notes/feature-persistence-failure-baseline]] (D6) | OK | D5 위임 명시; error-codes.yaml DB_SERIALIZATION_FAILURE/DB_DEADLOCK actually-implemented (code) | | C8: Idempotency 요구 정책 — non-idempotent retryable write 금지 | covered-here | — | — | D6 고유 소유; `@UseCaseCapability(idempotency=...)` actually-implemented (code) | | C9: Idempotency 메커니즘 — key scope/TTL/dedup/in-flight/mismatch | delegated | [[raw/branch-notes/feature-rate-limit-idempotency-contract]] (D2/D3/D6/D7/D8) | OK | D6 위임 명시; IdempotencyExecutor/IdempotencyStoreAdapter actually-implemented (code) | | C10: Outbox trigger 정책 — external publish outbox 경유, lossy publish 금지 | covered-here | — | — | D7 고유 소유 | | C11: Outbox 메커니즘 — SKIP LOCKED, at-least-once, publisher leadership | delegated | [[raw/branch-notes/feature-domain-event-outbox-contract]] (D2/D4/D9) | OK | D7 위임 명시; §엣지·의존 링크 | | C12: `@UseCaseCapability(transactionMode/repositoryAccess)` 어휘 + coherence ArchUnit rule | delegated | [[raw/branch-notes/feature-repository-access-permission-contract]] (D2/D11/D12) | OK | §엣지·의존 링크; capabilities.yaml TRANSACTION_REQUIRED owner 코드 확인 | ## Audit & Findings (2026-06-09 ground-truth 대조) > ca-tmpl `src/` + `docs/registries/` + sibling branch-notes 대조로 발견한 drift/ownership. **사용자 작성 결정 영역은 자동 rewrite 하지 않고 정합 권고만** 기록(CLAUDE.md §2 ground-truth 절차). - **DRIFT-1 — `TransactionalUseCaseRunner` 미존재**: D1·§결정사항·(이전)§외부근거 가 `TransactionalUseCaseRunner` 를 실행 경로로 언급하나, ca-tmpl `src/` grep + governing doc `transaction-boundary-abstraction` L79 ("검토 후 미채택... 코드에 존재하지 않는다") 로 **미채택 대안**임을 확인. 권고: 실행 경로 표현에서 제거하고 "미채택 대안"으로만 유지. (§외부근거 대안 5 로 정정 기록함; D1 본문은 사용자 결정이라 verbatim 보존 + 본 finding 으로 정합 표시.) - **DRIFT-2 — isolation "양쪽 동일 의미" 부정확**: D3 의 "PostgreSQL/MySQL 양쪽 동일 의미" 는 vendor 공식과 불일치 — 기본값이 다름(Postgres=READ COMMITTED `#PG-ISO-C1`, MySQL InnoDB=REPEATABLE READ `#MYSQL-ISO-C1`). 정확한 명제: "*명시 pin* 하면 양쪽에서 READ COMMITTED 동작을 강제할 수 있고, 묵시 default 는 vendor 마다 달라 위험". D3 row/§결정사항 보강으로 정정 반영. - **OWNERSHIP-1 — TransactionPort 계약은 app-port branch 소유**: TransactionPort abstraction + `@Transactional` 금지 ArchUnit rule + propagation 모드는 [[raw/branch-notes/feature-application-port-usecase-contract]] (D3/D11/D12) 가 SSOT 이며 *이미 구현·로컬검증 완료*(CleanArchitectureTest L167-175 주석이 "[[raw/branch-notes/feature-application-port-usecase-contract]] D3" 로 귀속). 본 branch D1/D2/D4 는 소비자 재진술 — §구현 가이드에서 OUT_OF_BRANCH_SCOPE 로 정제(메커니즘 명세는 app-port 로 위임, 본 branch 는 isolation/lock/idempotency/outbox 정책만). - **REGISTRY-GAP-1 — optimistic-lock 전용 error code 부재**: error-codes.yaml 에 `DB_SERIALIZATION_FAILURE`/`DB_DEADLOCK`/`DB_UNIQUE_VIOLATION` 만 존재, optimistic `@Version` 충돌 전용 code 없음. D5 의 "optimistic→409" 매핑의 정확한 code 는 owner_branch=`feature-persistence-failure-baseline` 결정 영역 → 그 branch 로 신규 제안 이관 권고. ## 구현 진행 (2026-06-09 — Phase C2, 본 branch 고유 소유분) > 위임분(D1/D2/D4 = app-port, C7 = persistence-baseline, C9 = rate-limit, C11 = outbox, C12 = repo-access, REGISTRY-GAP-1)은 구현 제외 — sibling SSOT 소유. 본 branch 고유 소유(C3/C5 isolation, C6 lock-policy)만 ca-tmpl 코드에 반영. - **C3/C5 (D3) — `actually-implemented`**: `application-core/.../transaction/Isolation.java` enum 을 `READ_COMMITTED` 단일값 → `READ_COMMITTED` / `REPEATABLE_READ` / `SERIALIZABLE` 3값으로 확장(app-port `Isolation.java` javadoc 이 본 contract 로 위임한 항목). `READ_UNCOMMITTED` 는 미선언(forbidden) 유지. **call-path 라우팅(TransactionPort 시그니처/SpringTransactionPort isolation 별 라우팅)은 app-port 공동 PR 필요 → `planned` 유지**, vocabulary 만 ship. - test: `IsolationTest`(app-core) — 3값 존재 + `READ_UNCOMMITTED` 미선언 검증. - test: `SpringTransactionPortTest.every_mode_pins_an_explicit_isolation_never_the_vendor_default` — 3 template 모두 `ISOLATION_DEFAULT` 아님(vendor default forbidden, D3 핵심 정책) 검증. - **C6 (D5) — `actually-implemented` (정책 test)**: `app-bootstrap/.../contract/LockFailureClassificationContractTest` — deadlock/serialization = retryable CONFLICT, unique = non-retryable CONFLICT, DB conflict code 어느 것도 generic INTERNAL/500 아님(D5 forbidden "all locks generic 500") 검증 + REGISTRY-GAP-1(optimistic 전용 code 부재) 을 known-absent 로 pin. exception→code 매핑은 persistence-baseline 소유(소비만). - **검증**: `:application-core:test`, `:adapter-persistence:test`, `:app-bootstrap:test`(ArchUnit 포함), `verifyCleanArchitectureDependencies` 전부 green (2026-06-09). - **제외(미구현, 의도적)**: REPEATABLE_READ/SERIALIZABLE call-path 라우팅(app-port 공동 PR), optimistic 전용 error code 신설(persistence-baseline), C8 non-idempotent-retryable-write 자동 금지(retry infra `planned`), C10 outbox trigger(outbox branch `raw`). ## 마주친 문제 - 아직 없음. ## 묶음 - [[raw/company-tech-blogs/cache-woowahan-after-commit-invalidation]] - [[raw/company-tech-blogs/custom-transaction-interceptor-catnipcoder]] - [[raw/company-tech-blogs/hexagonal-reflectoring-transactional-placement]] - [[raw/company-tech-blogs/transaction-port-clean-ddd-spring-medium]] - [[raw/company-tech-blogs/transaction-port-vassilis-soum-github-readme]] - [[raw/company-tech-blogs/woowahan-hexagonal-multimodule]] - [[raw/official-docs/at-transactional-spring-official]] - [[raw/official-docs/functional-tx-arrow-kt-resource-docs]] - [[raw/official-docs/mysql-innodb-transaction-isolation-official]] - [[raw/official-docs/postgres-transaction-isolation-official]] - [[raw/official-docs/spring-tx-management-reference]] - [[raw/official-docs/transaction-template-spring-official]] - [[raw/blog-topics/transaction-isolation-vendor-default-pin-2026-07-02]] > 본 feature branch 는 leaf — 자식 자료 없음. Phase C2 실 코드 작성 단계에서 errors / interview prep / lectures 가 누적되면 본 섹션에서 그룹화. ### 근거 자료 - [[raw/official-docs/postgres-transaction-isolation-official]] — PostgreSQL READ COMMITTED / REPEATABLE READ / SERIALIZABLE 보장 범위 vendor SSOT (D3 근거 — statement-level vs transaction-level snapshot, 직렬화 실패 에러) - [[raw/official-docs/mysql-innodb-transaction-isolation-official]] — MySQL InnoDB vendor default (REPEATABLE READ) + READ COMMITTED / REPEATABLE READ consistent-read / locking-read 시맨틱 SSOT (D3 UNSUPPORTED_DECISION 해소) ### 오류 기록 (본 feature 작업 중 발생) - (없음 — 2026-06-09 C3/C5/C6 구현 시 빌드/테스트 에러 없음. `raw/errors` 파생 노트 **not needed**.) ### 면접 준비 (이 작업에서 나올 수 있는 면접 질문) - (후보, 미정제 — `raw/interviews` 파생 노트 not needed 현 시점) "isolation default 를 코드에서 명시 pin 하는 이유는?" → vendor 기본값 상이(Postgres READ COMMITTED vs MySQL InnoDB REPEATABLE READ), 묵시 위임 시 동일 코드가 DB 따라 다른 격리. ## 관련 일일 노트 > 이 브랜치를 작업한 날짜들. 양방향 nav 유지. - 2026-06-09 — Phase C2 본 branch 고유 소유분(C3/C5 isolation enum + vendor-default-forbidden test, C6 lock-failure 분류 정책 test) 구현. 위임분 제외. 전 verification green. 상세 §구현 진행 (2026-06-09). ## 완료 후 정리 - PR 링크: - 리뷰 메모: - 머지 결과 / 배포 환경: - **wiki 추출 대상** (verified만, `wiki/projects/`로만 추출): - `actually-implemented` 항목: - `locally-verified` 항목: - `prod-verified` 항목: - **추출하지 않을 항목** (planned / documented-only / abandoned):