--- title: ca-tmpl - Data Layer Baseline 결정 (Persistence + Cache + Outbound) source_type: project status: verified confidence: high tags: [ca-tmpl, persistence, jpa, cache, http-client, actually-implemented, locally-verified] related_projects: [ca-tmpl] last_reviewed: 2026-07-02 --- > **UPDATE 2026-06-15 (스코프: Outbound HTTP 한정):** 본 문서가 2026-05-22 에 기록한 *"Phase C2 미진입 / 코드 없음"* 전제는 **Outbound HTTP 영역에 한해 더 이상 사실이 아니다.** `src/adapter-outbound/.../httpclient/` 에 outbound HTTP 클라이언트가 구현 + 로컬 테스트로 검증되어 있다(아래 "Outbound HTTP Client" 절). 이 절은 [[wiki/explainer/adapter-outbound]] 가 코드 사실의 근거로 인용한다. > > **UPDATE 2026-07-02:** `/home/donghyeon/workspace/ca-tmpl/src` 대조 및 `./gradlew check` 통과로 이 문서를 `verified`로 승격했다. Outbound HTTP, lower-layer cache SPI/router/fail-open, idempotency/outbox persistence, OSIV/Hikari startup guard는 구현·로컬 검증됐다. 단 본 문서의 원래 "Cache 결정"(cache-aside + Caffeine + Redisson 분산 lock + after-commit invalidation)은 일부가 lower-layer cache SPI와 다른 층이므로 구현 범위를 분리해서 읽어야 한다. # ca-tmpl - Data Layer Baseline 결정 (Persistence + Cache + Outbound) > Layer: `wiki/projects/` — ca-tmpl skeleton의 data layer baseline 결정 사실 기록. 일반 개념·근거는 [[wiki/concepts/data-layer-persistence-cache-outbound]]에 둠. 본 문서는 "내 프로젝트에서 무엇을 결정했고, 어디까지 진행되었는가"만 다룬다. ## 프로젝트 컨텍스트 ca-tmpl은 Clean Architecture 기반 백엔드 skeleton 템플릿이다. 본 문서는 그 안에서 **data layer baseline 3축**(Persistence / Cache / Outbound HTTP)을 어떻게 결정했는지를 기록한다. - 결정한 baseline: - **Persistence**: SQLState 9-row classifier matrix + Hibernate **OSIV off** + HikariCP pool wait/exhaustion alert + read replica lag threshold ([[raw/project-notes/ca-skeleton-operational-contract]] §6). - **Cache**: cache-aside default + Caffeine local lock(single-instance) + Redisson `RLock` distributed mutex(multi-instance HPA) + after-commit invalidation + eventual consistency window **5s** (canonical §11). - **Outbound HTTP**: Spring **RestClient** baseline + Resilience4j CircuitBreaker · TimeLimiter · Retry + timeout **connect 2s / read 5s / global 10s** + retry **default disabled** (canonical §11, §29 G-C). - 진행 단계: **C2 부분 구현 + 로컬 검증 완료.** 본 문서의 범위는 구현된 outbound/cache/persistence slice와 아직 planned로 남은 cache-aside/replica-lag/운영 tuning 경계를 분리한다. ## 실제 구현 내용 (`actually-implemented`) **Persistence / Cache / Outbound 일부 구현됨.** SQLState classifier와 read-replica lag metric은 별도 확인이 필요하지만, idempotency/outbox persistence adapter와 Flyway migration, OSIV/Hikari startup guard, lower-layer cache SPI/router/fail-open/Redis adapter, outbound HTTP baseline은 코드에 존재한다. ### Outbound HTTP Client (`actually-implemented`) 모듈 `src/adapter-outbound/.../httpclient/`, 단일 업스트림 의존성 1개당 인스턴스 1개. 진입 클래스 `OutboundHttpClient`. **입출력 (공개 API)** | 메서드 | 입력 | 반환 | 비고 | |---|---|---|---| | `static OutboundHttpClient baseline(name, baseUrl, settings, guard, resilience, retryPolicy, errorMapper, logger)` | 협력자 8개 | `OutboundHttpClient` | 정적 팩토리 — **빈으로 등록하지 않음**. fork 프로젝트가 의존성마다 named 인스턴스 생성. static 인 이유: ArchUnit B7(어댑터 타입 반환 public *비*static 메서드 금지) seam | | ` T get(String uri, Class type)` | URI, 응답 타입 | `T` | `exchange(GET, uri, null, type)` 위임 | | ` T exchange(HttpMethod m, String uri, Object body, Class type)` | 메서드/URI/요청바디/응답타입 | `T` | 전체 파이프라인(아래) | | ` T stream(HttpMethod m, String uri, Function reader)` | 메서드/URI/스트림 리더 | `T` | **리트라이 없음 · size 인터셉터 없음**. 대용량 응답 전용 | **호출 파이프라인 (`exchange` 정상 경로)** 1. **셧다운 fast-fail** — `guard.isShuttingDown()` 이면 네트워크를 맺지 않고 즉시 `DependencyFailureException(DEPENDENCY_CIRCUIT_OPEN, name, "shutdown in progress — outbound call rejected fail-fast (D8)")` throw, 로그 outcome=`REJECTED`. 2. `deadline = Instant.now().plus(globalCallTimeout)` 산정 → `retryPolicy.beginCall(method, deadline)` (ThreadLocal 에 적재). 3. 데코레이션 합성: `CircuitBreaker.decorateSupplier(cb, Retry.decorateSupplier(retry, countingSupplier))` → **합성 순서 = CB(바깥) → Retry(안) → 실제 호출**. 리트라이가 CB 안쪽이라 각 재시도가 독립적으로 CB 윈도우에 카운트됨. 4. 예외 분기: `OutboundResponseSizeExceededException` 는 **분류하지 않고 그대로 재throw**(업스트림 장애가 아니라 "버퍼 API 오용" 계약 위반); 그 외 모든 `Throwable` → `errorMapper.classify(name, t)` 로 매핑 → 로그 → throw. **호출자는 항상 `DependencyFailureException`(또는 size 예외)만 본다.** 5. `finally` 에서 `retryPolicy.endCall()` 항상 실행(ThreadLocal 누수 방지). **예외 / 오류코드 매핑 (`OutboundHttpErrorMapper.classify`, cause chain 순회 → 첫 매치 채택)** 진단 메시지는 **server-log-only** — status code + 예외 클래스명만 담고 업스트림 raw 응답 body 는 절대 미포함(D12 PII 안전, `DependencyFailureException` javadoc 계약). 오류코드는 `OperationalError`(SSOT `docs/registries/error-codes.yaml`): | 매치 (cause chain) | 코드 | HTTP / retryable | |---|---|---| | `CallNotPermittedException`(R4j) | `DEPENDENCY_CIRCUIT_OPEN` | 503 / true | | `UnknownHostException`·`UnresolvedAddressException` | `DEPENDENCY_DNS_FAILED` | 503 / true | | `HttpConnectTimeoutException` | `DEPENDENCY_CONNECT_FAILED` | 503 / true | | `ConnectException`(DNS cause 포함) | `DEPENDENCY_DNS_FAILED` | 503 / true | | `ConnectException`(그 외) | `DEPENDENCY_CONNECT_FAILED` | 503 / true | | `HttpTimeout`·`SocketTimeout`·`TimeoutException` | `DEPENDENCY_TIMEOUT` | 504 / true | | `RestClientResponseException` 4xx | `DEPENDENCY_4XX_CLIENT` | 502 / **false** (401→"check credential", 403→"check scope" 힌트) | | `RestClientResponseException` 5xx | `DEPENDENCY_5XX_SERVER` | 502 / true | | 매치 없음(fallback) | `DEPENDENCY_CONNECT_FAILED` | 503 / true | > 순서 주의: `HttpConnectTimeoutException extends HttpTimeoutException` 이라 connect 를 read timeout 보다 먼저 검사. **Open Risk(D12):** 408/429 는 의미상 재시도 가능하지만 현재 모든 4xx 가 non-retryable. **자료구조 + 선택 이유** | 구조 | 위치 | 이유 | |---|---|---| | `AtomicBoolean running/shuttingDown` | `OutboundHttpShutdownGuard` | 셧다운 스레드 write ↔ 요청 스레드 read 간 가시성 | | `ThreadLocal` + `record CallContext(HttpMethod, Instant deadline)` | `OutboundRetryPolicy` | 동기 클라이언트라 호출이 한 스레드를 타고 가므로 deadline·method 를 스레드별 격리 | | `Set.of(GET,HEAD,PUT,DELETE)` | `OutboundRetryPolicy.IDEMPOTENT_METHODS` | 불변 + O(1) 멱등 판정. POST/PATCH 의도적 제외 | | `int[] attemptCount = {0}` | `OutboundHttpClient.exchange` | 람다가 캡처 지역변수를 못 바꾸므로 1칸 배열을 가변 closure cell 로 사용 | | `record OutboundHttpSettings` + 중첩 `record Retry/CircuitBreaker`(박싱 `Integer/Double/Float`) | `OutboundHttpSettings` | 불변 값 + `null` = "기본값 적용" 신호 | | `Optional`/`Optional` | `OutboundHttpResilience` | "데코레이션 없음"(기능 off)을 호출자가 강제로 다루게 | **Spring / Resilience4j / Micrometer 메커니즘** - `@ConfigurationProperties(prefix="app.outbound.http")` + `@ConstructorBinding` → env/yaml → record 바인딩, compact 생성자 검증 실패 시 **startup 실패**. - `SmartLifecycle`(`OutboundHttpShutdownGuard`): `getPhase()=Integer.MAX_VALUE` → 컨텍스트 종료 시 phase **내림차순** stop → 이 빈의 `stop()` 이 가장 먼저 호출(다른 아웃바운드 빈보다 먼저 플래그 set). `ContextClosedEvent` 는 너무 늦고 순서 미보장이라 부적합. - `BeanPostProcessor`(`OutboundHttpTimeoutEnforcer`, **static @Bean**): raw `RestClient`/`RestClient.Builder` 빈 발견 시 `BeanCreationException` → timeout 미설정 클라이언트 등록을 startup 차단. static 이라 다른 빈보다 일찍 생성돼 가로챔. - Resilience4j: `decorateSupplier` 합성, `RetryRegistry`/`CircuitBreakerRegistry` 가 dependency 이름별 인스턴스 캐시(= per-dependency 지표), `IntervalFunction.ofExponentialRandomBackoff`(지수 + jitter). - Micrometer `MeterFilter`(`OutboundHttpResilienceConfig`): 저카디널리티 정규화 — `kind`→`outcome` 태그 리네임, state 값 대문자화, 그 외 `resilience4j.*` 미터 전부 `DENY`. **활성화 가드(D3):** retry/CB 중 하나라도 켜졌는데 `MeterRegistry` 없으면 `IllegalStateException`. - `RestClient` 2개: connect timeout = `HttpClient.connectTimeout`, read timeout = `JdkClientHttpRequestFactory.setReadTimeout`. buffered(trace→size 인터셉터) / streaming(trace 만). **설정 (`app.outbound.http.*`)** | 키 | 기본값 | 효과 | |---|---|---| | `connect-timeout` / `read-timeout` / `global-call-timeout` | 없음(필수) | TCP 연결 / 소켓 읽기 / 리트라이 포함 전체 deadline 예산. 누락 시 startup 실패 | | `retry-enabled` / `circuit-breaker-enabled` | `false` / `false` | R4j retry / CB 활성. 하나라도 켜면 `MeterRegistry` 필수 | | `response-size-limit` | `10MB` | buffered 본문 in-memory 상한(초과 시 `OutboundResponseSizeExceededException`) | | `retry.max-attempts` / `initial-backoff` / `backoff-multiplier` | `3` / `100ms` / `2.0` | 시도 횟수 / 첫 백오프 / 지수 배수 | | `circuit-breaker.failure-rate-threshold` | `50`(%) | open 임계 | | `…sliding-window-size` / `…minimum-number-of-calls` | `100` / `100` | COUNT_BASED 윈도우 / rate 계산 최소 호출 | | `…wait-duration-in-open-state` / `…permitted-calls-in-half-open` | `60s` / `10` | open→half-open 대기 / half-open 시험 호출 수 | **클래스 연관 (빈 배선)** - `OutboundHttpClientConfig` 가 공유 빈(`ShutdownGuard`/`TimeoutEnforcer`/`ErrorMapper`/`OutboundHttpDependencyLogger`/`RetryPolicy`)을 `@Bean @ConditionalOnMissingBean` 등록하되 **`OutboundHttpClient` 빈은 일부러 안 만든다**(의존성마다 named 인스턴스). - `OutboundHttpResilienceConfig` 가 `OutboundHttpResilience` 빈 생산 + MeterFilter 설치. - ⚠️ `retryPolicy` 인스턴스는 `resilience`(`shouldRetry` predicate)와 `OutboundHttpClient`(`beginCall`)가 **같은 것을 공유**해야 한다 — 다르면 `shouldRetry` 가 ctx=null 로 영영 재시도하지 않음. - 인터셉터: `TraceContextPropagationInterceptor`(MDC→`traceparent`/`baggage` 헤더, 샘플 플래그 `00` 하드코딩, allowlist=`tenant_id`·`request_id`), `ResponseSizeBoundingInterceptor`(Content-Length 또는 `BoundedInputStream` 누적이 limit 초과 시 throw, buffered 전용). ## 로컬/dev 검증 (`locally-verified`) **Persistence / (data-layer) Cache: 없음** (재확인 안 함). **Outbound HTTP Client: `locally-verified`** — `src/adapter-outbound/src/test/.../httpclient/` 의 단위 테스트로 다음이 검증됨(prod 배포·측정은 없음): - `OutboundHttpClientTest` — retry-on 500 GET 정확히 3회 / POST 정확히 1회(I4 비멱등 차단) · CB OPEN 시 0회 short-circuit + `DEPENDENCY_CIRCUIT_OPEN` · 셧다운 시 0회 + `REJECTED` · 업스트림 secret body 미유출 · buffered size 초과 시 `OutboundResponseSizeExceededException`(`stream()` 은 성공) · `outcome` 태그 존재/`kind` 태그 부재. - `OutboundHttpErrorMapperTest` — 위 예외 매핑 테이블 전 행 + 408/429 Open Risk + body 미유출. - `OutboundHttpResilienceTest` / `OutboundHttpResilienceConfigTest` — decorate 순서 · 기본값(3/100ms/2.0, 50%/100/100/60s/10) · MeterRegistry 가드. - `OutboundRetryPolicyTest` — 4-조건 게이트(셧다운/멱등/retryable/deadline). - `OutboundHttpShutdownGuardTest` — phase=`Integer.MAX_VALUE`, start/stop 플래그. - `OutboundHttpSettingsTest` — config 바인딩 + 잘못된 값 startup `IllegalArgumentException`. - `TraceContextPropagationInterceptorTest` / `OutboundHttpDependencyLoggerTest` — 헤더 주입 · 로그 레벨/필드. ## 운영 검증 (`prod-verified`) 없음. 운영(prod) 환경에 배포된 적이 없다. 따라서 릴리즈 노트 / 운영 로그 / 모니터링 대시보드 / 인시던트 보고서 어느 것도 존재하지 않는다. - 운영(prod) 환경에 배포된 적이 없다. 따라서 릴리즈 노트 / 운영 로그 / 모니터링 대시보드 / 인시던트 보고서 어느 것도 존재하지 않는다. ## 문서/계획만 존재 (`documented-only` / `planned`) 다음은 모두 **문서/설계 단계**의 결정이며, 코드로 강제되어 있지 않다. 면접에서 "구현했다"고 말하면 안 되는 부분이다. ### Persistence (`partially-implemented`) - SQLState 9-row classifier matrix(`08*` connection, `40001` serialization, `40P01` deadlock, `23xxx` integrity, `57014` query canceled 등) → Spring `DataAccessException` hierarchy 위에 `TRANSIENT_DEPENDENCY / CONFLICT / DATA_INTEGRITY` 카테고리 매핑 (`Category.java` 10-enum 정합 — 이전 `PERSISTENCE` 표기는 stale, 부모 §6 2026-06-01 정합 + `error-codes.yaml` authoritative). - Hibernate **OSIV off**를 baseline으로 결정 (Vlad Mihalcea anti-pattern 평가 + Spring Boot startup WARN 근거). - HikariCP pool wait p99 / pool exhaustion을 1차 alert 지표로 지정. - Read replica lag threshold를 SLO에 포함. - 근거: canonical [[raw/project-notes/ca-skeleton-operational-contract]] §6 + [[raw/branch-notes/feature-persistence-failure-baseline]]. - 구현됨: idempotency/outbox RDBMS adapter, PostgreSQL migration, OSIV-off startup guard, Hikari inter-knob startup guard. - 남음: SQLState 9-row classifier 전체, read replica lag metric/alert, 운영 pool tuning 측정. ### Cache (`partially-implemented`) - cache-aside default + Caffeine local lock(`@Cacheable(sync = true)` / `AsyncLoadingCache`) + Redisson `RLock` distributed mutex(multi-instance HPA 가정). - after-commit invalidation 강제 (Spring `TransactionSynchronizationManager.registerSynchronization`의 `afterCommit()` hook). - Eventual consistency window 5초로 명시. - Strict consistency use case(잔액, 인증, idempotency 검증)는 cache bypass. - Negative cache(존재하지 않는 row, TTL 60s)는 invalidation 채널 적용 대상에서 제외. - 근거: canonical §11 + [[raw/branch-notes/feature-cache-consistency-contract]]. - 구현됨: `CacheStore`, `FailOpenCacheStore`, `CacheStoreRouter`, `RedisCacheStore`, `CacheBindingSettings`, 관련 단위 테스트. - 남음: 원래 문서의 cache-aside+Caffeine local lock+Redisson distributed mutex+after-commit invalidation 전체 contract와 운영 consistency window 측정. ### Outbound HTTP (결정 — **현재 구현됨**, 위 "Outbound HTTP Client" 절 참조) 다음은 baseline 결정 *사실*이며, 결정 자체는 그대로 유효하다. **2026-06-15 기준 코드로 구현되어 있다**(시간/리트라이 *기본값*은 결정 당시 수치와 일부 다르게 구현됨 — 아래 표시): - Spring RestClient(6.1+)를 baseline으로 결정. RestTemplate은 maintenance-only로 신규 채택 제외, WebClient는 MVC servlet baseline의 blocking risk로 extension 분리, OpenFeign은 Spring Cloud 의존으로 baseline에서 제외. → **구현: `RestClient` 2종(buffered/streaming).** - Resilience4j로 retry / circuit breaker 일원화. Hystrix는 maintenance mode로 배제. → **구현: `OutboundHttpResilience` + `OutboundHttpResilienceConfig`.** (TimeLimiter 대신 동기 클라이언트라 deadline 예산 + `OutboundRetryPolicy` 게이트로 대체.) - Timeout 계층: connect / read / global **3축 모두 필수 강제**(하나라도 누락 시 startup 실패). → **구현됨. 단 결정 당시 예시값 `2s/5s/10s` 는 *기본값이 아니라 필수 입력*으로 구현**(`@ConfigurationProperties`, 기본값 없음). - Retry **default disabled**(`retry-enabled=false`) → **구현됨.** idempotency-key 미보장 일반 API 보수적 결정. 켜도 비멱등(POST/PATCH)은 `OutboundRetryPolicy` 가 차단. - 근거: canonical §11, §29 Group G-C + [[raw/branch-notes/feature-outbound-http-client-baseline]] + 코드 `src/adapter-outbound/.../httpclient/`. ### 대안 검토 범위 (요약 — 상세는 concept 참조) 각 sub-topic마다 5종 이상 대안을 비교했고 baseline을 선정했다. 비교의 출처/세부는 [[wiki/concepts/data-layer-persistence-cache-outbound]]에 있다. - Persistence: SQLState classifier vs vendor-specific code, JPA blocking vs R2DBC reactive, OSIV on vs off, Hikari sizing 공식. - Cache: cache-aside vs write-through vs write-behind vs read-through, Caffeine vs Hazelcast(local), Redisson RLock vs SETNX vs Redlock. - Outbound HTTP: RestClient vs RestTemplate vs WebClient vs OpenFeign vs `@HttpExchange`, Resilience4j vs Hystrix vs Spring Retry. ## 면접에서 말할 수 있는 범위 ### 자신 있게 답할 수 있는 질문 - ca-tmpl의 SQLState 9-row classifier matrix를 왜 만들었고, Spring `DataAccessException` hierarchy 위에서 어떤 카테고리(`TRANSIENT_DEPENDENCY / CONFLICT / DATA_INTEGRITY`, `Category.java` 10-enum)로 매핑하기로 했는가. - Hibernate OSIV를 anti-pattern으로 보는 근거(Vlad Mihalcea + Spring Boot WARN)와 OSIV off를 ca-tmpl baseline으로 둔 이유. - cache-aside의 eventual consistency window 5초가 의미하는 바, 그리고 strict consistency가 필요한 use case(잔액, 인증, idempotency 검증)를 cache bypass로 분리한 의도. - Resilience4j를 Hystrix 대신 선택한 이유(Hystrix maintenance mode + Resilience4j functional decorator 모델). - Outbound HTTP timeout을 connect 2s / read 5s / global 10s로 분리한 의도와, 셋 중 어떤 게 빠지면 어떤 위험이 생기는지. ### 적당히 답할 수 있는 질문 - Caffeine vs Hazelcast 같은 local cache 후보 비교 (개념 수준은 가능, 실측 비교 없음). - RestClient vs WebClient (concept-level trade-off는 답할 수 있으나 실제 throughput 측정 없음). - after-commit invalidation을 강제하는 이유 (개념 + Spring API 위치는 설명 가능, 실 hook 코드 없음). ### 답하면 안 되는 질문 (모른다고 해야 함) - HikariCP pool size 튜닝 경험, pool wait p99 실측치, pool exhaustion 대응 경험 — **측정/운영 경험 없음**. - cache hit ratio 측정 / TTL 튜닝 / negative cache stale 사례 — **계측 없음**. - Resilience4j circuit breaker open 운영 경험, half-open probe 동작 관찰, 실제 retry budget 튜닝 — **운영 경험 없음**. - read replica lag 운영 경험, replica failover 대응 — **운영 경험 없음**. - 본 baseline을 적용한 서비스의 SLO 달성 여부 — **prod 배포 없음**. ## 과장 금지 지점 이 프로젝트를 외부에 설명할 때 **사실보다 부풀려지기 쉬운 표현**. - "ca-tmpl에 SQLState classifier 전체를 구현했다" → **❌**. persistence failure classifier 전체는 별도 확인 필요. - "OSIV off startup guard와 Hikari inter-knob guard를 로컬 검증했다" → 가능. - "cache-aside + Redisson RLock으로 분산 환경에서 안전한 캐시를 구현했다" → **❌**. lower-layer cache SPI/router와 원래 cache-aside+distributed mutex contract를 혼동하지 않는다. - "Resilience4j로 circuit breaker/retry baseline을 구현했다" → 가능. 단 운영 장애 대응 경험은 없음. - "RestClient + timeout 2s/5s/10s로 outbound baseline을 구현하고 로컬 테스트로 검증했다" → 가능. 단 운영 SLO 보장은 아님. - "성능 측정 후 baseline을 튜닝했다" → **❌**. 측정·튜닝 모두 미수행. - "운영에서 검증된 baseline이다" → **❌**. prod 배포 없음. 면접·블로그·이력서에서는 항상 "**구현된 slice와 planned slice를 분리**"해야 한다. outbound/cache SPI/idempotency/outbox persistence는 구현·로컬 검증, cache-aside distributed consistency와 운영 tuning은 planned로 둔다. ### Blog-topic ingest: cache/webhook/outbound 묶음 (2026-07-02) 아래 raw seed들은 data-layer/cache/outbound canonical에 연결했다. 이 문서는 2026-07-02 기준 코드와 `./gradlew check`로 검증되어 blogify 가능하다. 단 각 글에서는 구현된 slice와 planned slice를 분리한다. - [[raw/blog-topics/cache-backend-router-fail-open-decorator-2026-07-02]]: cache 장애를 backend 내부 `try/catch`가 아니라 router/decorator 조립 계약으로 중앙화하는 글감. **말할 수 있는 범위**는 ca-tmpl cache role과 검증된 backend 범위다. "모든 cache 실패를 삼켜도 된다"는 식으로 쓰지 않는다. - [[raw/blog-topics/cache-consistency-after-commit-stampede-contract-2026-07-02]]: after-commit invalidation, stampede guard, negative TTL, consistency window를 분리하는 글감. **주의**: planned test와 unsupported decision을 implemented처럼 쓰지 않는다. - [[raw/blog-topics/webhook-full-jitter-dlq-observability-2026-07-02]]: webhook retry를 Full Jitter, DLQ, metric contract로 묶는 글감. **주의**: retry/metric/DLQ 중 planned 항목은 구현 완료로 쓰지 않는다. - [[raw/blog-topics/webhook-signature-replay-contract-2026-07-02]]: raw bytes, timestamp, message id, replay window를 webhook signature 계약으로 묶는 글감. provider 문서는 universal standard가 아니라 사례/source-backed claim으로만 사용한다. - [[raw/blog-topics/webhook-ssrf-egress-proxy-redirect-block-2026-07-02]]: webhook endpoint 등록을 URL 저장이 아니라 egress proxy, redirect block, private range 차단 계약으로 다루는 글감. OWASP/source-backed SSRF 방어와 ca-tmpl planned policy를 분리한다. - [[raw/blog-topics/jdk-httpclient-dns-connectexception-classification-2026-07-02]]: JDK `HttpClient`에서 DNS 실패를 connection failure로 분류할 때 exception wrapping과 retry category를 어떻게 다루는지 정리하는 글감. 운영 장애 사례가 아니라 local/test evidence 중심으로 제한한다. - [[raw/blog-topics/micrometer-meterfilter-resilience4j-functioncounter-2026-07-02]]: outbound HTTP observability에서 Micrometer `MeterFilter`와 Resilience4j `FunctionCounter` registration/tag policy가 충돌할 수 있는 지점을 정리하는 글감. Micrometer/Resilience4j 자체의 일반 결함처럼 쓰지 않는다. - [[raw/blog-topics/repository-capability-archunit-fitness-function-2026-07-02]]: repository 접근 권한을 annotation + registry + ArchUnit fitness function으로 강제하는 글감. 모든 repository misuse를 자동 검출한다고 쓰지 않고, 정적 분석 rule이 볼 수 있는 구조로 제한한다. - [[raw/blog-topics/persistence-audit-metadata-clean-architecture-2026-07-02]]: audit column을 domain model에 섞지 않고 persistence adapter에서 채우는 boundary choice 글감. JPA Auditing이 나쁘다고 쓰지 않고 ca-tmpl skeleton의 선택으로 제한한다. - [[raw/blog-topics/distributed-lock-transaction-commit-boundary-2026-07-02]]: `lock.close()`와 DB commit 순서가 맞물릴 때 lost update 경계가 생기는 이유를 다루는 글감. local/JDBC lock 검증을 운영 분산 환경 보장으로 표현하지 않는다. - [[raw/blog-topics/hikaricp-inter-knob-constraints-startup-guard-2026-06-09]]: HikariCP knob 간 제약을 Spring Boot startup guard로 fail-fast 검증하는 글감. 기존 canonical은 persistence/cache 영역이 stale일 수 있으므로 실제 validator/test 존재 여부를 재확인하기 전까지 구현 등급을 올리지 않는다. ## 관련 개념 - [[wiki/concepts/data-layer-persistence-cache-outbound]] ## Sources - [[raw/project-notes/ca-skeleton-operational-contract]] — §6 Operational Error Category, §11 Adapter Failure Contract, §29 Group G-C 외부 근거 인덱스 - [[raw/branch-notes/feature-persistence-failure-baseline]] — SQLState 9-row matrix, Hikari alert threshold, OSIV off 결정 기록 - [[raw/branch-notes/feature-cache-consistency-contract]] — cache-aside default, Caffeine + Redisson, after-commit invalidation, 5s window 결정 기록 - [[raw/branch-notes/feature-outbound-http-client-baseline]] — RestClient baseline, Resilience4j, timeout 2s/5s/10s, shutdown retry suppression 결정 기록 - [[raw/blog-topics/jdk-httpclient-dns-connectexception-classification-2026-07-02]] — JDK HttpClient DNS/ConnectException classification 블로그 글감 raw seed - [[raw/blog-topics/micrometer-meterfilter-resilience4j-functioncounter-2026-07-02]] — Micrometer/Resilience4j metric registration 블로그 글감 raw seed - [[raw/branch-notes/feature-repository-access-permission-contract]] — repository capability/access permission parent branch - [[raw/blog-topics/repository-capability-archunit-fitness-function-2026-07-02]] — repository capability ArchUnit fitness function 블로그 글감 raw seed - [[raw/branch-notes/feature-persistence-auditing-contract]] — persistence audit metadata parent branch - [[raw/blog-topics/persistence-audit-metadata-clean-architecture-2026-07-02]] — persistence audit metadata 블로그 글감 raw seed - [[raw/branch-notes/feature-distributed-lock-contract]] — distributed lock lifecycle parent branch - [[raw/blog-topics/distributed-lock-transaction-commit-boundary-2026-07-02]] — distributed lock transaction commit boundary 블로그 글감 raw seed - [[raw/blog-topics/hikaricp-inter-knob-constraints-startup-guard-2026-06-09]] — HikariCP startup guard 블로그 글감 raw seed - [[raw/branch-notes/feature-cachestore-multi-backend-router]] — cache backend router/decorator/fail-open 글감의 parent branch - [[raw/branch-notes/feature-webhook-outbound-contract]] — webhook retry/signature/SSRF outbound 글감의 parent branch - [[raw/blog-topics/cache-backend-router-fail-open-decorator-2026-07-02]] — cache router/decorator 블로그 글감 raw seed - [[raw/blog-topics/cache-consistency-after-commit-stampede-contract-2026-07-02]] — cache after-commit/stampede 블로그 글감 raw seed - [[raw/blog-topics/webhook-full-jitter-dlq-observability-2026-07-02]] — webhook retry/DLQ/observability 블로그 글감 raw seed - [[raw/blog-topics/webhook-signature-replay-contract-2026-07-02]] — webhook signature/replay 블로그 글감 raw seed - [[raw/blog-topics/webhook-ssrf-egress-proxy-redirect-block-2026-07-02]] — webhook SSRF/egress 블로그 글감 raw seed ## Cluster / 묶음 - [[wiki/blog/ca-tmpl-data-layer-persistence-cache-outbound-2026-07-02]]