394 lines
46 KiB
Markdown
394 lines
46 KiB
Markdown
---
|
|
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 실패 계약을 정의합니다.
|
|
|
|
<!-- section-id: branch-parent -->
|
|
## 부모 (필수)
|
|
|
|
- **Parent project (canonical SSOT)**: [[raw/project-notes/ca-skeleton-operational-contract]]
|
|
|
|
> ca-skeleton 은 별도 root branch 없이 project-note 가 SSOT 역할. 본 feature branch 는 project-note 의 운영 계약 중 해당 영역 (§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 참조).
|
|
|
|
<!-- GENERATED: branch-contract:start -->
|
|
<!-- section-id: branch-contract-packet -->
|
|
## 브랜치 계약 패킷
|
|
|
|
- **생성 시 프로젝트 개정**: `1`
|
|
- **패킷 스키마**: `contract_packet: 1`
|
|
- **완료 조건**: transaction·concurrency failure fixture가 통과한다
|
|
|
|
<!-- section-id: inherited-project-decisions -->
|
|
### 상속한 프로젝트 결정
|
|
|
|
| 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]] |
|
|
|
|
<!-- section-id: branch-local-decisions -->
|
|
### 브랜치 지역 결정
|
|
|
|
> 기존 branch-local 결정은 아래 `## Decision Evidence Map / 결정-근거 매핑`의 D-row가 소유하며 이 packet에서 복제하지 않는다.
|
|
|
|
| Decision ID | Decision | Relation | Supporting Claims | Status |
|
|
|---|---|---|---|---|
|
|
|
|
<!-- section-id: declared-overrides -->
|
|
### 선언한 예외
|
|
|
|
| Override ID | Overrides | Reason | Approval | Status |
|
|
|---|---|---|---|---|
|
|
<!-- GENERATED: branch-contract:end -->
|
|
|
|
<!-- section-id: branch-goal -->
|
|
## 목표
|
|
|
|
운영 장애는 단순 DB unavailable보다 transaction boundary, lock, deadlock, duplicate command, retry 중복 write에서 자주 발생합니다. CA skeleton은 application use case 기준의 transaction/concurrency 규칙을 가져야 합니다.
|
|
|
|
- 이슈:
|
|
- PR:
|
|
|
|
<!-- section-id: branch-scope -->
|
|
## 범위
|
|
|
|
### 포함 범위
|
|
|
|
- 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`).
|
|
|
|
## 마주친 문제
|
|
|
|
- 아직 없음.
|
|
|
|
## 묶음
|
|
|
|
<!-- GENERATED: sources:start -->
|
|
- [[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]]
|
|
<!-- GENERATED: sources:end -->
|
|
|
|
<!-- GENERATED: blog-topics:start -->
|
|
- [[raw/blog-topics/transaction-isolation-vendor-default-pin-2026-07-02]]
|
|
<!-- GENERATED: blog-topics:end -->
|
|
|
|
> 본 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):
|