39 KiB
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-persistence-auditing-contract | branch-note | raw | feature-persistence-auditing-contract |
|
|
|
2026-06-10 | in-progress | BR-CA-SKELETON-OPERATIONAL-CONTRACT-055 | project-work-item | ca-skeleton-operational-contract | WI-CA-SKELETON-OPERATIONAL-CONTRACT-055 |
|
|
1 | 82c57510c05700f3204c4b6da2ad9707172b3d695d0ced764b7f38c3c5d97099 |
branch: feature-persistence-auditing-contract
Layer:
raw/branch-notes/— 단일 브랜치의 TODO·결정·진행 기록. 머지/종료 후 verified 결과는/ingest로wiki/projects/에 추출. 원본은 raw에 영구 보관.status_label:in-progress|review|merged|abandoned계층 표기: "root branch" 라는 별도 개념은 없음. project 의 직접 자식 branch 는parent_branch:를 비워두고related_projects만 채움. 다른 branch 의 자식이면parent_branch: <부모 branch 이름>명시 +## Parent섹션의 부모 wikilink 필수.
부모 (필수)
이 branch 가 어느 작업 묶음에 속하는지. 모든 branch 는 예외 없이 upward link 보유.
이 branch 는 ca-skeleton 운영 계약 project 의 직접 자식 branch (project 분해표 §35 E영역 priority 8 row). parent_branch: 비어있음.
사용자가 "부모 브랜치 = CA Skeleton Operational Contract" 라고 표현했으나,
CA Skeleton Operational Contract는 branch 가 아니라 project hub 이다. 따라서 이 branch 는 다른 branch 의 자식 이 아니라 project 의 직접 자식 으로 모델링한다(parent_branch:공란 +related_projects= project).
- Project 의 직접 자식 branch: raw/project-notes/ca-skeleton-operational-contract (§35 E영역 priority 8:
feature-persistence-auditing-contract— "entity audit 컬럼 (CreatedBy/UpdatedBy) 도입 시점 / 도메인 오염 차단 메커니즘(AuditPort+ adapter 가로채기)")
본 branch 가 결정을 위임/소비 하는 형제 branch (Edge·Dependency 참조):
- raw/branch-notes/feature-runtime-context-propagation-contract —
created_by/updated_by액터 ID 를 공급하는 runtime context seam (그 branch D1 port + D5 위임 맵). 본 branch 는 그 seam 의 consumer. - raw/branch-notes/feature-persistence-failure-baseline — optimistic lock / conflict 분류 owner. 본 branch 의
version컬럼은 그 branch 로 위임(OUT_OF_BRANCH_SCOPE). - raw/branch-notes/feature-migration-startup-contract — audit 컬럼의 Flyway migration 이 그 startup 게이트를 통과해야 함.
브랜치 계약 패킷
- 생성 시 프로젝트 개정:
1 - 패킷 스키마:
contract_packet: 1 - 완료 조건: persistence audit actor·time·mapping·transaction contract test가 명시된다
상속한 프로젝트 결정
| 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-MAPPING-001@1 |
수기 mapper와 record canonical constructor가 default이며 MapStruct는 optional profile이다 | 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 |
|---|
목표
도메인 aggregate JPA 영속화 시 누가 / 언제 만들고 고쳤는지(created_at / updated_at / created_by / updated_by)를 일관되게 기록하되, 이 감사 메타데이터가 domain-core aggregate 를 오염시키지 않도록 adapter-persistence 계층에만 가두는 계약을 정한다.
핵심 긴장: Clean Architecture 에서 audit 메타데이터는 인프라 관심사다. 도메인 엔티티가 createdBy 필드를 들고 있으면 (1) 도메인이 "누가 로그인했나"라는 보안/요청 컨텍스트를 알게 되어 의존 방향이 뒤집히고, (2) JPA/Spring 어노테이션이 domain-core 로 새어 들어온다. 본 branch 는 audit 을 adapter 의 책임으로 못박는 경계를 설계한다.
- 이슈: (미생성 — project §35 E영역 priority 8 신규 branch 권고)
- PR: (미생성 — 코드 착수 전 결정 계약 단계)
- governing: raw/project-notes/ca-skeleton-operational-contract §35 L2086 / L2030
범위
포함 범위
- audit 컬럼 집합 결정:
created_at/updated_at/created_by/updated_by(D3) - 도메인 오염 차단 메커니즘: audit 필드를 adapter-persistence 의
@MappedSuperclass(또는 adapter 명시 set)에만 두고 domain-core 는 0 필드 (D2) - 캡처 메커니즘 선택 + wiring: Manual explicit-set(현 스켈레톤 선례) vs Spring Data JPA Auditing(엔티티 증가 시 성장 경로) (D1)
- 시간 소스: 기존
Clockbean 재사용 — Manual=adapter 주입, JPA-auditing=DateTimeProvider가 Clock wrapping (D4) - 액터 ID seam:
AuditorAware/AuditContextPort가 runtime-context-propagation seam consume +"system"fallback (D5) - 적용 범위: 도메인 aggregate persistence entity 만, infra/immutable record 제외 (D6)
제외 범위
의도적으로 제외한 것. 면접 등에서 "이건 범위에 없었습니다"라고 답할 근거.
version/ optimistic-lock 컬럼 — raw/branch-notes/feature-persistence-failure-baseline + raw/branch-notes/feature-transaction-concurrency-contract 가 owner (conflict 분류). audit 컬럼과 동거하나 본 branch 결정 아님.- 전체 변경 이력 / revision history (Hibernate Envers
*_AUD테이블) — 본 branch 는 "현재 행의 audit 메타 4필드"만. 시점별 스냅샷/삭제 이력은 별도(data-retention / 미래 Envers branch). - audit log(보안 이벤트 로그: actor/action/target/before_hash) — raw/branch-notes/feature-log-management-contract +
feature-data-retention-privacy-contractowner. 본 branch 는 DB 행 메타데이터이지 구조화 로그가 아님. (registrymdc-keys.yamlaudit 키는 그 branch 소유) Instant.now()직접호출 차단 ArchUnit rule +Clockport 추상화 — project §35 F영역 "Time/Clock 주입" 미래 branch. 본 branch 는 기존 Clock bean 재사용까지만.- principal 값 의미론(보안 주체가 산출하는 문자열 형식/소스) — raw/branch-notes/feature-authentication-authorization-contract owner. 본 branch 는 seam 타입(
AuditorAware<String>)과 fallback 만 결정. IdempotencyRecordEntity등 infra/immutable 엔티티 — 자체created_at관리(immutable, updated_at 없음). audit base 미적용 (D6).
근거 (필수, 최소 1개+)
이 branch의 구현·설계 결정의 근거가 되는 외부 자료. 공식 문서·대기업 기술 블로그·강의 등 raw 자료를 인용. 같은 자료가 여러 결정의 근거면 결정 표시와 함께 여러 번 등장 가능.
| Source | 정당화하는 결정 |
|---|---|
| raw/official-docs/spring-data-jpa-auditing-official | D1/D2: @CreatedDate/@LastModifiedDate/@CreatedBy/@LastModifiedBy + @EntityListeners(AuditingEntityListener.class) 를 @MappedSuperclass 에 선언하는 공식 패턴 (C1, C2, C4). D5: AuditorAware<T> SPI 구현 의무 (C3). @EnableJpaAuditing 활성화 (C5). |
| raw/company-tech-blogs/vlad-mihalcea-postgresql-audit-logging-triggers | DB-level trigger auditing (standalone) 대안 기각: 트리거가 액터 ID 를 읽으려면 앱이 매 DML 전 SET LOCAL var.logged_user 로 세션 변수를 주입해야 하는 propagation seam 이 강제되고, DB 타임소스가 앱 Clock bean 과 분리된다 |
| raw/company-tech-blogs/thorben-janssen-hibernate-timestamp-clock-limitation | Hibernate-native @CreationTimestamp/@UpdateTimestamp 대안 거부: Clock 주입 불가(JVM 시간 직접 사용, C1) + created_by/updated_by 미지원(C2) |
근거 자료가 raw에 아직 없다면 먼저 raw-source-template 또는 lecture-note-template 으로 raw에 등록한 뒤 여기서 링크.
TODO
각 항목 옆에 증거 등급 표기: actually-implemented | locally-verified | prod-verified | documented-only | planned | needs-confirmation
AuditableEntity@MappedSuperclass를 adapter-persistence 에 정의 (created_at/updated_at/created_by/updated_by) — 등급:locally-verified(adapter-persistence/.../audit/AuditableEntity.java+AuditableEntityTest)- 도메인 aggregate persistence entity 가
AuditableEntity상속 (sample-portfolioWorkLogEntity부터) — 등급:locally-verified(WorkLogEntity extends AuditableEntity,:sample-portfolio:testgreen) - 캡처 wiring: (현 스켈레톤) adapter explicit-set 패턴 구현 (D1 Manual = current default) — 등급:
locally-verified(WorkLogRepositoryAdapterClock+AuditContextPort, INSERT/UPDATE 분기 +WorkLogRepositoryAdapterTest). 성장 경로@EnableJpaAuditing+DateTimeProvider는 D1 deferred — 코드 javadoc + adapter-persistence CLAUDE.md 에 문서화, 미배선 — 등급:documented-only AuditContextPort+"system"fallback 구현 (runtime-context-propagation seam consume) — 등급:locally-verified(DomainContextAuditContextPort+DomainContextAuditContextPortTest: 부재/blank →"system", bound → actor). JPA-auditing path 의AuditorAware<String>는 deferred (D1 growth path).- audit 컬럼 Flyway migration (migration-startup 게이트 통과) — 등급:
documented-only(sample-portfolio/.../db/migration/V2__work_log.sql, work_log + audit 4컬럼, V1 이후 in-order). 이 repo 에는 Flyway 를 부팅하는 테스트가 없어(@WebMvcTest 슬라이스 + custom test app) 런타임 실행 미검증. - domain-core 가 audit 필드/
jakarta.persistence를 모름을 ArchUnit rule 로 강제 — 등급:locally-verified(기존domain_is_pure가jakarta.persistence../org.springframework..차단 + 신규domain_entities_do_not_carry_audit_fields가 createdAt/updatedAt/createdBy/updatedBy 필드 차단,:app-bootstrap:test --tests '*CleanArchitectureTest'green) - 결정 계약 + 근거 자료 5건 archive (이 branch-spec) — 등급:
actually-implemented
진행 중 메모
- ground truth(
/home/donghyeon/workspace/ca-tmpl): 현재 audit 어노테이션·@MappedSuperclass·AuditorAware전무. 유일한 시간 캡처 선례는IdempotencyStoreAdapter가 생성자에서clock.instant()를 명시 set 하는 패턴(= Manual 방식) +IdempotencyConfig.systemClock()(Clock.systemUTC()) bean. → D1 Manual path 는 지어낸 것이 아니라 이미 확립된 패턴의 일반화. IdempotencyRecordEntity는 immutable(Vernon Option A 재구성) +updated_at없음 → audit base 적용 대상 아님(D6).- registry
mdc-keys.yaml의audit키(actor/action/target)는 로그 계약이지 DB 컬럼 아님 — log-management/data-retention 소유. 혼동 주의(Out of scope). - 2026-06-10 구현 완료 (Manual path, D1 current default). 변경 파일:
adapter-persistence/.../audit/AuditableEntity.java(@MappedSuperclass, plain@Column4필드,initializeAudit/carryCreation/applyModification)adapter-persistence/.../audit/AuditContextPort.java(interfacecurrentActor())adapter-persistence/.../audit/DomainContextAuditContextPort.java(@Component,DomainContextPropagator소비 +"system"fallback)sample-portfolio/.../entity/WorkLogEntity.java(extends AuditableEntity)sample-portfolio/.../repository/WorkLogRepositoryAdapter.java(Clock+AuditContextPort주입, INSERT/UPDATE 분기 audit set)sample-portfolio/.../db/migration/V2__work_log.sql(work_log + audit 4컬럼)app-bootstrap/.../architecture/CleanArchitectureTest.java(domain_entities_do_not_carry_audit_fields신규 rule)adapter-persistence/CLAUDE.md(Persistence auditing contract 섹션 추가)- 테스트:
AuditableEntityTest,DomainContextAuditContextPortTest,WorkLogRepositoryAdapterTest(audit 케이스 추가) - 검증:
:adapter-persistence:test,:sample-portfolio:test,:app-bootstrap:test --tests '*CleanArchitectureTest',verifyCleanArchitectureDependencies,./gradlew check모두 green. - UPDATE 시
created_*보존: adapter 가jpa.findById(같은 tx → JPA L1 캐시 hit) 로 기존 행을 읽어 carry.created_*는updatable=false로 SQL 레벨에서도 이중 보호. - 구현자 임의 결정(spec UNSUPPORTED_IMPL_DECISION 충당): actor 키 이름 =
DomainContextKey.of("actor", String.class)(runtime-context branch 가 canonical 키 확정 시DomainContextAuditContextPort한 곳만 수정).
결정 사항
추후 면접/회고에서 "왜 이렇게 했나" 답할 근거. 대안과 함께 기록. 상세 매핑은 아래 §Decision Evidence Map.
- 2026-06-10 (D1): 감사 캡처 메커니즘 = 조건부 — 현 스켈레톤은 adapter explicit-set(Manual), 엔티티 증가 시 Spring Data JPA Auditing(
@MappedSuperclass+@EnableJpaAuditing). 이유: Manual 은 기존IdempotencyStoreAdapter패턴과 일관 +Clockbean 직접 재사용 + 명시성. JPA-auditing 은 엔티티 多 시 선언적 누락 방지. / 검토한 대안: Hibernate@CreationTimestamp(Clock 주입 불가로 기각), DB trigger 단독(actor seam 복잡 + clock 분리로 기각). / 근거: raw/official-docs/spring-data-jpa-auditing-official, raw/company-tech-blogs/thorben-janssen-hibernate-timestamp-clock-limitation, raw/company-tech-blogs/vlad-mihalcea-postgresql-audit-logging-triggers, 선례IdempotencyStoreAdapter. - 2026-06-10 (D2): audit 필드는 adapter-persistence
@MappedSuperclass(또는 adapter 명시 set)에만 — domain-core aggregate 0 필드. 이유: audit = 인프라 관심사, 도메인이 알면 의존 역전 + 어노테이션 누출. / 대안: 도메인 엔티티에 audit 필드(= 오염, 기각). / 근거: raw/company-tech-blogs/arhohuttunen-hexagonal-architecture-spring-boot, [raw/official-docs/spring-data-jpa-auditing-official], governing §35. - 2026-06-10 (D3): audit 컬럼 =
created_at/updated_at/created_by/updated_by4개.version(optimistic-lock)은 위임. 근거: governing §35 L2086(CreatedBy/UpdatedBy),spring-data-jpa-auditing-official#C1. - 2026-06-10 (D4): 시간 소스 = 기존
Clockbean 재사용. Manual=adapter 주입, JPA-auditing=DateTimeProviderbean 이 Clock wrapping 후@EnableJpaAuditing(dateTimeProviderRef=...). 근거:spring-data-jpa-enable-jpa-auditing-api#C2, 선례IdempotencyConfig.systemClock. - 2026-06-10 (D5): 액터 ID =
AuditorAware/AuditContextPort가 runtime-context-propagation seam consume +"system"fallback. principal 값 의미는 authn-authz 위임(UNSUPPORTED). 근거:spring-data-jpa-auditing-official#C3, cross-contract raw/branch-notes/feature-runtime-context-propagation-contract D1/D5. - 2026-06-10 (D6): 적용 범위 = 도메인 aggregate persistence entity 만. infra/immutable(
IdempotencyRecordEntity) 제외. 근거: repo ground-truth(immutable record + updated_at 부재).
결정-근거 매핑
각 결정이 어떤 raw source claim 으로 뒷받침되는지 명시한다.
Decision ID는 이 branch-note 안에서 안정적으로 유지한다. 예:D1,D2.Supporting Claims는raw/<category>/<slug>.md#C1형식으로 연결한다.
선택 조건열(R2): "이 조건일 때 이 결정, 다른 조건이면 어떤 대안". 분기 없으면N/A.
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|---|---|---|---|---|---|
| D1 | 감사 캡처 메커니즘: 현 스켈레톤 = adapter explicit-set(Manual), 엔티티 증가 시 = Spring Data JPA Auditing(@MappedSuperclass+@EnableJpaAuditing+AuditingEntityListener) |
엔티티 수 적고 명시성 우선 → Manual(선례 일관). 엔티티 증가/선언적 누락방지 필요 → JPA Auditing 으로 마이그레이션. Hibernate @CreationTimestamp = Clock 주입 불가로 기각, DB trigger 단독 = actor seam 복잡+clock 분리로 기각 |
raw/official-docs/spring-data-jpa-auditing-official.md#C1, #C4, #C5, raw/company-tech-blogs/thorben-janssen-hibernate-timestamp-clock-limitation.md#C1, #C2, raw/company-tech-blogs/vlad-mihalcea-postgresql-audit-logging-triggers.md#C1, #C3 + 선례 IdempotencyStoreAdapter + governing §35 |
official-vendor-doc + company-tech-blog + repo-precedent + governing |
Manual path 의 set 누락(선언적 보장 없음); Manual→JPA-auditing 마이그레이션 트리거 임계(엔티티 N개) 미정 |
| D2 | audit 필드를 adapter-persistence 의 @MappedSuperclass(또는 adapter 명시 set)에만 — domain-core aggregate 0 필드 |
항상 (핵심 mandate, 분기 N/A) | raw/company-tech-blogs/arhohuttunen-hexagonal-architecture-spring-boot.md#C2, raw/official-docs/spring-data-jpa-auditing-official.md#C2 + governing §35(도메인 오염 차단) + ca-tmpl/src/adapter-persistence/CLAUDE.md(CA layer rule) |
governing + official-vendor-doc + company-case-study |
domain↔entity 매핑 비용(arhohuttunen "cost of having to do mapping") |
| D3 | audit 컬럼 = created_at/updated_at/created_by/updated_by 4개. version은 제외 |
항상. optimistic-lock/conflict 필요 → feature-persistence-failure-baseline/feature-transaction-concurrency-contract 위임(OUT_OF_BRANCH_SCOPE) |
governing §35 L2086(CreatedBy/UpdatedBy), raw/official-docs/spring-data-jpa-auditing-official.md#C1 |
governing + official-vendor-doc |
updated_at INSERT 초기값(=created_at? modifyOnCreate 기본 true), 컬럼 타입(timestamptz) 미확정 |
| D4 | 시간 소스 = 기존 Clock bean 재사용. Manual=adapter 주입, JPA-auditing=DateTimeProvider(Clock wrapping)+@EnableJpaAuditing(dateTimeProviderRef=...) |
항상(Instant.now() 직접호출 금지). Clock port 추상화+차단 ArchUnit rule 은 F영역 future branch 위임(OUT_OF_BRANCH_SCOPE) |
raw/official-docs/spring-data-jpa-enable-jpa-auditing-api.md#C2 + 선례 IdempotencyConfig.systemClock/IdempotencyStoreAdapter.clock.instant() |
official-vendor-doc + repo-precedent |
JPA-auditing path 에서 dateTimeProviderRef 누락 시 LocalDateTime.now()(VM time) silent 회귀 |
| D5 | 액터 ID = AuditorAware<String>/AuditContextPort 가 runtime-context-propagation seam consume + "system" fallback |
도메인이 actor 추적 요구 시. principal 부재(scheduler/migration/anonymous) → "system". principal 값 의미론(보안 주체 문자열) = UNSUPPORTED_DECISION (authn-authz 미착수) |
raw/official-docs/spring-data-jpa-auditing-official.md#C3 + cross-contract raw/branch-notes/feature-runtime-context-propagation-contract D1/D5 |
official-vendor-doc + cross-contract (sibling) — principal 값은 none (unsupported) |
authn-authz 미착수로 principal 타입/의미 미정; "system" fallback 자동 아님(구현체 명시 분기 필요); runtime-context seam 자체도 미성숙(그 branch DomainContextKey = needs-confirmation) → AuditContextPort 어댑터 1개로 격리하고 값 타입은 String 고정해 흡수 |
| D6 | 적용 범위 = 도메인 aggregate persistence entity 만. infra/immutable(IdempotencyRecordEntity) 제외 |
새 aggregate JPA entity → AuditableEntity 적용. infra/immutable record(자체 created_at, updated_at 부재) → 제외 |
ca-tmpl ground-truth: IdempotencyRecordEntity(immutable, Vernon Option A), V1__idempotency_record.sql(updated_at 부재) |
repo-precedent |
"aggregate vs infra" 경계 판단 기준 모호 — 새 entity 추가 시 owner 가 분류 결정 필요 |
구현 가이드
결정 (Decisions) 이 "무엇 을 할 것인가" 라면, 본 §는 "어디에 어떻게 구현될 것인가" 의 사전 명세 — 문서가 모호해서 구현자가 임의로 정해야 했던 결정 카탈로그. 작성 목표는 다음 구현자가 되묻지 않아도 코드를 작성할 수 있는 수준.
본 §는 일률적 anchor list 를 강제하지 않는다. branch 마다 구현 내용·범위가 다르므로 sub-section 은 이 branch 의 결정과 근거에서 도출되는 것만 작성. 어떤 branch 는 error mapping 표 + 정적 강제 카탈로그, 어떤 branch 는 migration 단계 + wiring, 어떤 branch 는 sequence + state machine. 형식 예시는 raw/branch-notes/feature-boundary-validation-mapping-contract 의 §구현 가이드 참조.
3-rule meta principle (필수 준수):
- R1. Reference 필수 — 각 sub-section / row / cell 은 본 branch 의
Decision ID(예: D1, D2) + 그 결정의Supporting Claim ID(예:RAW-SLUG-C1) 를 reference. 근거 없는 결정 금지 — 모든 구현 detail 은 결정 + 근거의 도출 이어야 함.- R2. UNSUPPORTED_IMPL_DECISION 명시 — 근거 raw 가 원칙 만 권고하고 detail (메커니즘 선택 / 클래스/rule 명명 / glob 패턴 / algorithm / factory API 모양 등) 은 권고하지 않는 cell 은
UNSUPPORTED_IMPL_DECISION라벨 + 사용자 trade-off 근거 한 줄. 이게 근거 있는 결정 vs 사용자 임의 trade-off 의 경계.- R3. OUT_OF_BRANCH_SCOPE 정제 — 본 branch 결정 범위 밖 cell 은 §구현 가이드에 남기지 않음. 별도 branch 또는 canonical SSOT 로 이관 (이관 history 는 별도 § "Audit & Findings" 등에 보존). 도메인 특화 detail (ca-tmpl skeleton 범위 밖) 도 동일하게 정제.
각 sub-section 의 권장 헤더 패턴:
### N. <sub-section 제목> > **Trace**: <In-scope row 들 + Decision ID + Supporting Claim ID 의 매핑 (한 줄/한 단락)> > > - **UNSUPPORTED_IMPL_DECISION**: <근거 없는 사용자 임의 결정 항목들 + 각각의 trade-off 근거 한 줄> <표 또는 명확한 구조 — 자유 텍스트 = 모호함 = 되묻기 원인>
1. AuditableEntity @MappedSuperclass + 컬럼 명세
Trace: D2(도메인 오염 차단) + D3(컬럼 집합). Claims:
spring-data-jpa-auditing-official#C2(metadata in superclass),#C1(4 annotations),arhohuttunen#C2(domain 분리).
- UNSUPPORTED_IMPL_DECISION: 클래스명
AuditableEntity, 패키지 위치dev.caskeleton.adapter.persistence.audit, 컬럼 SQL 타입(timestamptz/varchar(256)) 은 근거 raw 가 원칙만 권고 → 임의 trade-off: ca-tmpl 기존 컨벤션(IdempotencyRecordEntity의timestamptz created_at,principal varchar(256))과 정합시켜 선택.
| 컬럼 | Java type | SQL type | nullable | listener/set 시점 |
|---|---|---|---|---|
created_at |
Instant |
timestamptz |
NOT NULL, updatable=false | INSERT (D4 Clock) |
updated_at |
Instant |
timestamptz |
NOT NULL | INSERT 시 = created_at, 매 UPDATE 갱신 (path별 보장 방식 ↓) |
created_by |
String |
varchar(256) |
NOT NULL, updatable=false | INSERT (D5 actor, fallback "system") |
updated_by |
String |
varchar(256) |
NOT NULL | INSERT 시 = created_by, 매 UPDATE 갱신 (path별 ↓) |
updated_*INSERT 초기값 보장 — path별 분리 (depth audit #2): JPA-auditing path 는@EnableJpaAuditing의modifyOnCreate기본true(C3 —spring-data-jpa-enable-jpa-auditing-api#C4)가 자동으로 INSERT 시updated_*를created_*와 동일 set. Manual path 에는 이 속성이 없으므로, adapter 가 entity 생성 시updated_at=created_at,updated_by=created_by를 명시 set 해야 NOT NULL 충족 (D1 Manual + D4 도출 —IdempotencyStoreAdapter의 생성자 명시 set 패턴 연장).
@MappedSuperclass+@EntityListeners(AuditingEntityListener.class)(JPA-auditing path) 또는 어노테이션 없는 plain 필드 + adapter set(Manual path). 두 path 모두 클래스는 adapter-persistence 모듈에만 위치 → domain-core 는 이 클래스를 import 불가(D2).
2. 캡처 메커니즘 wiring (Manual vs JPA Auditing)
Trace: D1(메커니즘) + D4(시간 소스). Claims:
spring-data-jpa-enable-jpa-auditing-api#C2(dateTimeProviderRef),spring-data-jpa-auditing-official#C5(@EnableJpaAuditing),thorben-janssen#C1(Hibernate Clock 불가).
- UNSUPPORTED_IMPL_DECISION:
@EnableJpaAuditing을 둘 config 클래스명/모듈(app-bootstrap의JpaAuditingConfig권고 —IdempotencyConfig선례 위치),DateTimeProviderbean 명(auditingDateTimeProvider) 은 임의 trade-off: 기존app-bootstrapconfig 패턴과 정합.
- Manual path (현 스켈레톤 default): persistence adapter 생성자에
Clock+AuditContextPort주입 → entity 생성/재구성 시clock.instant()+auditContextPort.currentActor()를 명시 set.IdempotencyStoreAdapter와 동일 패턴. - JPA Auditing path (성장 경로):
app-bootstrap에@EnableJpaAuditing(dateTimeProviderRef="auditingDateTimeProvider", auditorAwareRef="auditorAware")+DateTimeProviderbean(() -> Optional.of(clock.instant()), 기존systemClock재사용).dateTimeProviderRef누락 시 VM time 회귀(Open Risk D4) → §Claims 검증 대상.
3. 액터 ID seam (AuditorAware
Trace: D5(액터 소스). Claims:
spring-data-jpa-auditing-official#C3(AuditorAware SPI) + cross-contract raw/branch-notes/feature-runtime-context-propagation-contract D1/D5.
- UNSUPPORTED_IMPL_DECISION: principal 값의 의미/형식(user id? email? subject claim?)은 근거 없음 →
feature-authentication-authorization-contract착수 전까지AuditorAware<String>으로 타입만 고정하고 값 의미는 위임. seam 인터페이스명AuditContextPort는 임의(runtime-context 의DomainContextKey와 정합 검토).
AuditorAware<String>.getCurrentAuditor()→ runtime-context-propagation seam 에서 principal 조회. 부재 시Optional.of("system")반환(scheduler/Flyway migration/anonymous). 자동 아님 — 구현체가 명시 분기.- runtime-context-propagation branch 의 seam API 가 확정되기 전에는
AuditContextPort.currentActor()interface 1개로 추상화(그 branch D1 port 와 어댑터 연결).
4. 적용 범위 카탈로그
Trace: D6(적용 범위). Claims: ca-tmpl ground-truth.
| Entity | audit base 적용? | 사유 |
|---|---|---|
도메인 aggregate persistence entity (예: WorkLogEntity) |
✅ 적용 | 도메인 변경 추적 대상 |
IdempotencyRecordEntity |
❌ 제외 | immutable(Vernon Option A), 자체 created_at, updated_at 없음 |
| 신규 entity | 추가 시 owner 가 분류 | aggregate=적용 / infra·immutable=제외 (Open Risk D6) |
엣지·실패·의존
R4(깊이 게이트) 캡처용. 정상 경로 외에 구현 중 부딪힐 실패/엣지/다른 계약 의존을 미리 열거. 없으면 "해당 없음" 명시(공란 금지).
- 실패·엣지 경로:
- principal 부재 (scheduler / Flyway migration / anonymous / 시스템 작업):
AuditorAware가"system"fallback set (D5). 자동 아님 — 구현체 명시. - bulk/native UPDATE (
@QueryUPDATE, JDBC batch): JPA lifecycle listener 미발화 →@LastModifiedDate/updated_by미갱신. 기대 동작: bulk path 는 audit 미보장임을 문서화 + 필요 시 명시 set. dateTimeProviderRef미연결 (JPA-auditing path 설정 누락):LocalDateTime.now()(VM time) silent 회귀 → 결정론 테스트 깨짐. 기대: 부팅 검증 또는 테스트로 fail-fast.- immutable record (
IdempotencyRecordEntity): audit base 미적용 — 자체created_at관리,updated_at없음 (D6, 정상 경로). - INSERT 시
updated_at/updated_by초기값:modifyOnCreate기본 true → created 값과 동일하게 채워짐(NOT NULL 충족).
- principal 부재 (scheduler / Flyway migration / anonymous / 시스템 작업):
- 다른 계약 의존:
- raw/branch-notes/feature-runtime-context-propagation-contract 의
D1(context port) /D5(boundary→mechanism 위임 맵) 에 의존 —created_by/updated_byactor 를 그 seam 에서 consume. 그 port API 가 바뀌면AuditContextPort어댑터 수정 필요. - raw/branch-notes/feature-authentication-authorization-contract 에 의존 — principal 값 의미(무슨 문자열). 미착수 → D5 의 값 의미
UNSUPPORTED. - raw/branch-notes/feature-persistence-failure-baseline / raw/branch-notes/feature-transaction-concurrency-contract 위임 —
version/optimistic-lock 컬럼은 본 branch 밖. audit 컬럼과 같은 테이블에 공존하나 결정 주체 다름. - raw/branch-notes/feature-migration-startup-contract 에 의존 — audit 컬럼 추가 Flyway migration 이 그 startup 게이트(baseline-on-migrate / out-of-order 방지)를 통과해야 함.
- project §35 F영역 "Time/Clock 주입" 미래 branch —
Instant.now()차단 ArchUnit rule +Clockport 추상화는 그쪽 소유. 본 branch 는 기존 Clock bean 재사용까지만.
- raw/branch-notes/feature-runtime-context-propagation-contract 의
검증해야 할 주장
공식 문서나 사례는 근거지만, 내 프로젝트에서의 동작을 자동으로 보장하지 않는다. 구현 전/중/후에 실제로 검증해야 하는 주장을 분리한다.
| Claim | Why uncertain | How to verify | Status |
|---|---|---|---|
JPA-auditing path 에서 DateTimeProvider(Clock wrapping)가 실제로 @CreatedDate/@LastModifiedDate 를 주입 Clock 으로 채운다 |
dateTimeProviderRef 미연결 시 LocalDateTime.now()(VM) 로 silent 회귀 가능 (D4 Open Risk) |
Clock.fixed(...) bean 으로 교체 → entity persist → created_at 이 고정 instant 와 일치하는 통합 테스트 |
deferred — JPA-auditing 은 D1 growth path(미배선). Manual path 는 adapter 가 clock.instant() 를 직접 set 하므로 Clock.fixed 로 결정론 검증됨(WorkLogRepositoryAdapterTest) |
AuditContextPort 가 principal 부재 시 "system" 을 채운다 (자동 아님) |
Spring 은 Optional.empty() 면 필드를 비움 — "system" 은 구현체가 명시해야 (D5) |
runtime-context 비운 채 currentActor() → "system" 단위 테스트 |
locally-verified (DomainContextAuditContextPortTest: 부재/blank → "system") |
domain-core 가 audit 필드/jakarta.persistence 를 모른다 (오염 차단 D2 실제 강제) |
설계 의도일 뿐 컴파일이 막아주지 않음 — 누군가 도메인에 @CreatedDate 추가 가능 |
ArchUnit: domain-core 가 jakarta.persistence../org.springframework.data.. import 금지 rule + audit 필드명 금지 rule |
locally-verified (domain_is_pure + 신규 domain_entities_do_not_carry_audit_fields, CleanArchitectureTest green) |
bulk/native UPDATE 시 updated_at/updated_by 미갱신 (capture 우회) |
JPA lifecycle / adapter save 경로만 audit set — @Modifying @Query UPDATE 우회 |
@Modifying @Query UPDATE 실행 후 updated_at 불변 확인 + 문서화 |
documented-only — WorkLog 에 bulk UPDATE 쿼리 없음. adapter-persistence CLAUDE.md + AuditableEntity javadoc 에 "bulk path 는 명시 set 필요" 문서화 |
version(optimistic-lock)이 audit 테이블에 들어가더라도 본 branch 가 아닌 failure-baseline owner |
같은 @MappedSuperclass/테이블에 공존 시 owner 혼동 위험 |
failure-baseline §결정과 cross-check, audit base 에 @Version 미포함 확인 |
locally-verified — AuditableEntity 에 @Version 없음(audit 4필드만). WorkLogEntity 가 자체 @Version 보유(불변경). |
관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
/coverage가 채우는 생성물 — 손으로 유지하지 않는다. governing 문서(frontmattergoverning_docs)가 요구하는 관심사를 이 브랜치가 빠짐없이 덮는지의 결과. 기준:rules/coverage-gate.md. 상태:covered-here(이 브랜치 결정) /delegated(다른 owner 브랜치) /missing(아무도 안 맡음 → Blocking).
/coverage 결과 (2026-06-10): Covered (Blocking 0 / Should-fix 0 / Advisory 1). governing 적정성 OK (§35 E#8 + L2030 이 본 branch 명시 지정).
| 관심사 | 상태 | owner | 심각도 | 근거 |
|---|---|---|---|---|
| entity audit 컬럼 집합 (created_at/updated_at/created_by/updated_by) | covered-here | — | — | D3; governing L2086 |
| 도메인 오염 차단 메커니즘 (adapter-persistence 만, domain-core 0 필드) | covered-here | — | — | D2; governing L2086 (AuditPort + adapter 가로채기) |
| 감사 캡처 메커니즘 선택 + wiring (Manual vs JPA Auditing) | covered-here | — | — | D1; governing L2030 |
| 시간 소스 (기존 Clock bean 재사용) | covered-here | — | — | D4; IdempotencyConfig.systemClock() 선례 |
| 액터 ID seam (AuditorAware/AuditContextPort + "system" fallback) | covered-here | — | — | D5; spring-data-jpa-auditing-official#C3 |
| 적용 범위 (도메인 aggregate만, infra/immutable 제외) | covered-here | — | — | D6; V1__idempotency_record.sql no updated_at |
| version / optimistic-lock 컬럼 | delegated | raw/branch-notes/feature-persistence-failure-baseline (D6) + raw/branch-notes/feature-transaction-concurrency-contract (D5) | OK | Out of scope + §Edge 명시 |
| principal 값 의미론 | delegated | raw/branch-notes/feature-authentication-authorization-contract | OK | Out of scope + D5 UNSUPPORTED |
| audit log (actor/action/target structured log) | delegated | raw/branch-notes/feature-log-management-contract (D9) + mdc-keys.yaml audit 키 | OK | Out of scope 명시 |
| Flyway migration gate | delegated | raw/branch-notes/feature-migration-startup-contract | OK | §Edge 의존 명시 |
| runtime-context seam (actor 공급 포트) | delegated | raw/branch-notes/feature-runtime-context-propagation-contract (D1/D5) | OK | Parent + §Edge 명시 |
| Instant.now() 차단 ArchUnit rule + Clock port 추상화 | delegated | F영역 future branch (명시적 deferred) | Advisory | Out of scope; governing §35 F "Time/Clock 주입" |
마주친 문제
짧은 메모만. 깊이 있는 트러블슈팅은
raw/errors/로 분리하고 아래 Cluster에 연결.
- 이슈 1
- 원인:
- 시도:
- 해결: (또는 미해결이면
needs-confirmation) - 별도 에러 노트로 분리됨:
[[raw/errors/<...>]](생성 시)
묶음 (이 branch에서 파생된 자료)
- raw/company-tech-blogs/arhohuttunen-hexagonal-architecture-spring-boot
- raw/company-tech-blogs/thorben-janssen-hibernate-timestamp-clock-limitation
- raw/company-tech-blogs/vlad-mihalcea-postgresql-audit-logging-triggers
- raw/official-docs/spring-data-jpa-auditing-official
- raw/official-docs/spring-data-jpa-enable-jpa-auditing-api
이 branch는 단일 노트가 아니라 작업 묶음의 entry point. 이 branch에서 파생된 모든 raw 노트를 카테고리별로 명시. 자식 노트가 forward link만 박아도 Obsidian backlink로 자동 발견되지만, 읽기 흐름과 분류를 위해 hub가 명시적으로 그룹화한다.
근거 자료
- raw/official-docs/spring-data-jpa-enable-jpa-auditing-api — D4:
dateTimeProviderRef/auditorAwareRef/modifyOnCreate/setDates속성 계약 (C1~C4) - raw/company-tech-blogs/arhohuttunen-hexagonal-architecture-spring-boot — D2: JPA entity 와 domain model 분리 + persistence adapter 가 매핑 전담 패턴 (C1~C3)
- raw/company-tech-blogs/thorben-janssen-hibernate-timestamp-clock-limitation — Hibernate-native 대안 거부 근거: Clock 주입 불가(C1) +
created_by/updated_by미지원(C2)
Sub-branches (세부 작업)
- 없음 — 단일 branch 안에서 구현 완료(2026-06-10). 세부 분할 불요.
오류 기록 (이 branch 작업 중 발생)
- 없음 — 구현·검증 중 실패/차단/샌드박스 이슈 없음. 모든 gradle 명령 첫 시도에 green. 별도
raw/errors/노트 불필요.
면접 준비 (이 작업에서 나올 수 있는 면접 질문)
- 후보(노트 미생성): "Clean Architecture 에서
created_by/updated_by같은 감사 메타데이터를 도메인 엔티티에 두면 왜 의존 방향이 뒤집히는가, 그리고 Vernon Option A 재구성(도메인이 audit 무지) 환경에서 UPDATE 시created_*를 어떻게 보존하는가"(adapter 가 기존 행 read +updatable=false). 정직하게 본 작업에서 도출 가능 — 필요 시raw/interviews/로 승격.
강의 (이 작업을 위해 학습한 강의)
- 없음.
job-posting tie-ins (이 작업에서 파생된 글감)
- 후보(노트 미생성): "감사 컬럼을 도메인에서 몰아내기 — Manual explicit-set vs Spring Data JPA Auditing 의 트레이드오프와 Clock 주입/actor seam 설계". 본 branch 결정(D1/D2/D4/D5)에서 직접 도출되는 글감 — 필요 시
raw/blog-topics/로 승격. - derived blog: 생성 전. 생성 시
wiki/blog/<slug>-YYYY-MM-DD.md후보
관련 일일 노트
이 브랜치를 작업한 날짜들. 양방향 nav 유지.
[[raw/daily-notes/YYYY-MM-DD]][[raw/daily-notes/YYYY-MM-DD]]
완료 후 정리
머지/종료 시점에 채움.
/ingest가 이 섹션을 기준으로 wiki/projects/에 추출.
- PR 링크:
- 리뷰 메모:
- 머지 결과 / 배포 환경: (로컬/dev/staging/prod 어디까지 검증됐는지)
- wiki 추출 대상 (verified만,
wiki/projects/로만 추출):actually-implemented항목:locally-verified항목:prod-verified항목:
- 추출하지 않을 항목 (planned / documented-only / abandoned):