Files
llm-wiki/raw/branch-notes/feature-persistence-auditing-contract.md

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
ca-skeleton-operational-contract
raw/project-notes/ca-skeleton-operational-contract.md
branch
persistence
auditing
2026-06-10 in-progress BR-CA-SKELETON-OPERATIONAL-CONTRACT-055 project-work-item ca-skeleton-operational-contract WI-CA-SKELETON-OPERATIONAL-CONTRACT-055
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-TRANSACTION-001@1
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-MAPPING-001@1
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-DATABASE-001@1
WI-CA-SKELETON-OPERATIONAL-CONTRACT-056
WI-CA-SKELETON-OPERATIONAL-CONTRACT-048
WI-CA-SKELETON-OPERATIONAL-CONTRACT-006
WI-CA-SKELETON-OPERATIONAL-CONTRACT-012
WI-CA-SKELETON-OPERATIONAL-CONTRACT-017
1 82c57510c05700f3204c4b6da2ad9707172b3d695d0ced764b7f38c3c5d97099

branch: feature-persistence-auditing-contract

Layer: raw/branch-notes/ — 단일 브랜치의 TODO·결정·진행 기록. 머지/종료 후 verified 결과는 /ingestwiki/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 Contractbranch 가 아니라 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 참조):

브랜치 계약 패킷

  • 생성 시 프로젝트 개정: 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 의 책임으로 못박는 경계를 설계한다.

범위

포함 범위

  • 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)
  • 시간 소스: 기존 Clock bean 재사용 — 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-contract owner. 본 branch 는 DB 행 메타데이터이지 구조화 로그가 아님. (registry mdc-keys.yaml audit 키는 그 branch 소유)
  • Instant.now() 직접호출 차단 ArchUnit rule + Clock port 추상화 — 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-portfolio WorkLogEntity 부터) — 등급: locally-verified (WorkLogEntity extends AuditableEntity, :sample-portfolio:test green)
  • 캡처 wiring: (현 스켈레톤) adapter explicit-set 패턴 구현 (D1 Manual = current default) — 등급: locally-verified (WorkLogRepositoryAdapter Clock+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_purejakarta.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.yamlaudit 키(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 @Column 4필드, initializeAudit/carryCreation/applyModification)
    • adapter-persistence/.../audit/AuditContextPort.java (interface currentActor())
    • 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 패턴과 일관 + Clock bean 직접 재사용 + 명시성. 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_by 4개. version(optimistic-lock)은 위임. 근거: governing §35 L2086(CreatedBy/UpdatedBy), spring-data-jpa-auditing-official#C1.
  • 2026-06-10 (D4): 시간 소스 = 기존 Clock bean 재사용. Manual=adapter 주입, JPA-auditing=DateTimeProvider bean 이 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 Claimsraw/<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 (필수 준수):

  1. R1. Reference 필수 — 각 sub-section / row / cell 은 본 branch 의 Decision ID (예: D1, D2) + 그 결정의 Supporting Claim ID (예: RAW-SLUG-C1) 를 reference. 근거 없는 결정 금지 — 모든 구현 detail 은 결정 + 근거의 도출 이어야 함.
  2. R2. UNSUPPORTED_IMPL_DECISION 명시 — 근거 raw 가 원칙 만 권고하고 detail (메커니즘 선택 / 클래스/rule 명명 / glob 패턴 / algorithm / factory API 모양 등) 은 권고하지 않는 cell 은 UNSUPPORTED_IMPL_DECISION 라벨 + 사용자 trade-off 근거 한 줄. 이게 근거 있는 결정 vs 사용자 임의 trade-off 의 경계.
  3. 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 기존 컨벤션(IdempotencyRecordEntitytimestamptz 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 는 @EnableJpaAuditingmodifyOnCreate 기본 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-bootstrapJpaAuditingConfig 권고 — IdempotencyConfig 선례 위치), DateTimeProvider bean 명(auditingDateTimeProvider) 은 임의 trade-off: 기존 app-bootstrap config 패턴과 정합.
  • Manual path (현 스켈레톤 default): persistence adapter 생성자에 Clock + AuditContextPort 주입 → entity 생성/재구성 시 clock.instant() + auditContextPort.currentActor() 를 명시 set. IdempotencyStoreAdapter 와 동일 패턴.
  • JPA Auditing path (성장 경로): app-bootstrap@EnableJpaAuditing(dateTimeProviderRef="auditingDateTimeProvider", auditorAwareRef="auditorAware") + DateTimeProvider bean(() -> 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 (@Query UPDATE, 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 충족).
  • 다른 계약 의존:

검증해야 할 주장

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

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-corejakarta.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-verifiedAuditableEntity@Version 없음(audit 4필드만). WorkLogEntity 가 자체 @Version 보유(불변경).

관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)

/coverage 가 채우는 생성물 — 손으로 유지하지 않는다. governing 문서(frontmatter governing_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에서 파생된 자료)

이 branch는 단일 노트가 아니라 작업 묶음의 entry point. 이 branch에서 파생된 모든 raw 노트를 카테고리별로 명시. 자식 노트가 forward link만 박아도 Obsidian backlink로 자동 발견되지만, 읽기 흐름과 분류를 위해 hub가 명시적으로 그룹화한다.

근거 자료

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):