12 KiB
title, source_type, status, confidence, tags, related_projects, last_reviewed, canonical_sources, audience, target_publish, status_label
| title | source_type | status | confidence | tags | related_projects | last_reviewed | canonical_sources | audience | target_publish | status_label | |||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Persistence와 Cache, Outbound 경계를 한 문서에서 분리하기 | blog | verified | high |
|
|
2026-07-02 |
|
backend-engineer | ready |
Persistence와 Cache, Outbound 경계를 한 문서에서 분리하기
Parent / 부모 (필수)
- 핵심 canonical: wiki/projects/ca-tmpl/data-layer-persistence-cache-outbound
- 관련 개념 문서: wiki/concepts/data-layer-persistence-cache-outbound — data layer 일반 개념. 현재 이 글의 구현 사실 근거는 verified project canonical에 둔다.
타깃 독자 / Target reader
- 독자 profile: data layer baseline을 skeleton 수준에서 정하려는 백엔드 엔지니어.
- 이미 안다고 가정하는 것: JPA, cache-aside, outbound HTTP, connection pool.
- 처음 듣는다고 가정하는 것: persistence/cache/outbound를 한데 묶되 증거 등급을 분리하는 방식.
도입 / Hook
- 문제 / 궁금증: data layer는 persistence, cache, outbound가 섞여 보여도 실패 모드와 검증 범위가 다르다.
- 이 글이 답하는 것: ca-tmpl에서 구현된 항목과 문서/계획만 있는 항목을 구분한다.
- 이 글이 답하지 않는 것: 실제 운영 DB latency와 cache hit ratio 개선 측정.
본문 outline / Body outline
- data layer baseline을 쪼개서 보기 — persistence, cache, outbound.
- 실제 구현된 범위 — idempotency/outbox adapter, migration, OSIV/Hikari guard, lower-layer cache SPI.
- cache와 outbound의 실패 계약 — fail-open/fail-closed를 구분한다.
- Hikari/startup guard와 slow query 논의 — 구현/계획/needs-confirmation을 분리한다.
- 운영 검증 없음 — metric과 incident 경험처럼 말하지 않는다.
본문 / Body
data layer라고 부르면 하나로 보이지만, 실제로는 서로 다른 실패 모드가 섞여 있습니다. persistence는 DB transaction과 constraint, connection pool 문제가 중심입니다. cache는 빠른 조회와 stale data, backend 장애 시 degrade 정책이 중심입니다. outbound HTTP는 외부 dependency의 timeout, retry, circuit breaker, shutdown 처리 문제가 중심입니다. ca-tmpl의 data layer 문서는 이 셋을 한 문서에 두되, 구현된 범위와 계획만 있는 범위를 분리합니다.
이 분리가 중요한 이유는 과장하기 쉽기 때문입니다. “data layer baseline을 구현했다”고 말하면 persistence classifier, cache consistency, outbound resilience가 모두 같은 수준으로 끝난 것처럼 들립니다. 하지만 ca-tmpl 기준으로는 구현된 slice가 서로 다릅니다. idempotency/outbox RDBMS adapter, PostgreSQL migration, OSIV/Hikari startup guard, lower-layer cache SPI/router/fail-open, outbound HTTP client는 구현·로컬 검증됐습니다. 반면 SQLState classifier 전체, read replica lag metric, cache-aside + Redisson distributed mutex + after-commit invalidation 전체 contract, 운영 tuning은 아직 planned 또는 부분 구현입니다.
persistence 쪽에서 구현된 대표 guard는 OSIV off입니다. OSIV(Open Session In View)는 web response 렌더링 시점까지 Hibernate session을 열어두는 방식입니다. 편리하지만 presentation layer에서 lazy association을 만지는 순간 DB query가 나갈 수 있습니다. ca-tmpl은 spring.jpa.open-in-view=true가 명시되면 startup에서 실패시키는 validator를 둡니다. 즉 layer boundary를 runtime configuration에서도 깨지 않게 합니다.
HikariCP 설정도 startup guard로 다룹니다. connection-timeout은 최소 250ms 이상이어야 하고, validation-timeout은 connection-timeout보다 작아야 하며, keepalive-time은 max-lifetime보다 작아야 합니다. leak-detection-threshold도 켤 거면 2000ms 이상이어야 합니다. 이것은 pool sizing을 운영에서 측정했다는 뜻이 아닙니다. 잘못 조합된 knob를 애플리케이션 시작 시점에 빨리 실패시키는 guard입니다.
cache 쪽은 fail-open 경계를 구현했습니다. ca-tmpl의 CacheStoreRouter는 logical cache name을 backend id로 라우팅합니다. binding이 없는 logical cache를 호출하면 조용히 no-op 하지 않고 AdapterDisabledException을 던집니다. 반대로 backend가 구성된 뒤 실제 cache backend 호출이 실패하면 FailOpenCacheStore가 get은 miss로, put은 관찰된 실패로 낮춥니다. cache는 성능 보조 장치이므로 backend 장애가 곧 5xx가 되지 않게 하는 쪽입니다.
이 차이가 outbox와 다릅니다. outbox publish는 fail-open이면 안 됩니다. 메시지 발행 실패를 조용히 삼키면 downstream이 영원히 변경 사실을 모를 수 있습니다. 그래서 outbox는 FAILED/DEAD state machine과 error log를 갖습니다. cache는 장애 시 miss로 degrade할 수 있지만, outbox는 실패를 상태로 남기고 다시 처리해야 합니다. 같은 “adapter failure”라도 업무 의미가 다릅니다.
outbound HTTP는 또 다른 경계입니다. ca-tmpl의 OutboundHttpClient는 dependency name별 baseline client를 만들고, shutdown 중이면 네트워크를 맺기 전에 fail-fast합니다. buffered 호출은 retry/circuit breaker decorator를 거치고, streaming 호출은 retry하지 않습니다. 이미 일부 bytes를 소비한 stream은 안전하게 재시도하기 어렵기 때문입니다. retry policy도 GET/HEAD/PUT/DELETE 같은 idempotent method만 재시도 대상으로 둡니다. POST/PATCH는 기본적으로 제외됩니다.
outbound HTTP에서 중요한 것은 timeout 3축입니다. connect timeout, read timeout, global call timeout을 분리해서 생각합니다. connect timeout은 TCP 연결 단계, read timeout은 socket read 단계, global call timeout은 retry를 포함한 전체 예산입니다. ca-tmpl의 현재 구현은 이 값을 기본값으로 박아두기보다 필수 설정으로 요구하고, 누락 또는 잘못된 raw RestClient 등록을 startup에서 막는 방향입니다.
정리하면 ca-tmpl의 data layer baseline은 “DB, cache, HTTP를 다 구현했다”는 단순한 문장이 아닙니다. 구현된 것은 구현됐다고 말하고, planned인 것은 planned라고 남기는 문서입니다. 이 글에서 가장 중요한 학습 포인트도 여기에 있습니다. data layer의 경계는 기술 이름으로 나뉘는 것이 아니라, 실패했을 때 무엇을 보존해야 하는지로 나뉩니다. DB transaction은 정합성을 보존해야 하고, cache는 miss로 degrade할 수 있으며, outbound HTTP는 retry/timeout 예산 안에서 실패를 분류해야 합니다.
코드 예제 / Code samples (있다면)
// 출처: [[wiki/projects/ca-tmpl/data-layer-persistence-cache-outbound]]
// 실제 파일: app-bootstrap/.../OpenInViewSafetyValidator.java, ca-tmpl @f6fbd4e196b4
public void afterSingletonsInstantiated() {
Boolean openInView = environment.getProperty("spring.jpa.open-in-view", Boolean.class);
if (Boolean.TRUE.equals(openInView)) {
throw new IllegalStateException("APP_DATASOURCE_OPEN_IN_VIEW must be false");
}
}
// 출처: [[wiki/projects/ca-tmpl/data-layer-persistence-cache-outbound]]
// 실제 파일: app-bootstrap/.../HikariPoolConstraintValidator.java
if (validationTimeout != null
&& connectionTimeout != null
&& validationTimeout >= connectionTimeout) {
violations.add("validation-timeout must be < connection-timeout");
}
if (keepaliveTime != null && maxLifetime != null && keepaliveTime >= maxLifetime) {
violations.add("keepalive-time must be < max-lifetime");
}
// 출처: [[wiki/projects/ca-tmpl/data-layer-persistence-cache-outbound]]
// 실제 파일: adapter-outbound/.../CacheStoreRouter.java
public Optional<String> get(String logicalName, String key) {
return resolve(logicalName).get(key);
}
private CacheStore resolve(String logicalName) {
String backendId = bindings.get(logicalName);
if (backendId == null) {
throw new AdapterDisabledException("cache", "no cache backend bound");
}
return backends.get(backendId);
}
// 출처: [[wiki/projects/ca-tmpl/data-layer-persistence-cache-outbound]]
// 실제 파일: adapter-outbound/.../FailOpenCacheStore.java
public Optional<String> get(String key) {
try {
return delegate.get(key);
} catch (Exception ex) {
dependencyLogger.logFailure(delegate.backendId(), "cache", "get", ex);
return Optional.empty();
}
}
// 출처: [[wiki/projects/ca-tmpl/data-layer-persistence-cache-outbound]]
// 실제 파일: adapter-outbound/.../OutboundHttpClient.java
if (shutdownGuard.isShuttingDown()) {
throw observer.rejectShutdown("shutdown in progress — outbound call rejected fail-fast");
}
retryPolicy.beginCall(method, deadline);
try {
Supplier<T> decorated = countingSupplier;
if (retry.isPresent()) decorated = Retry.decorateSupplier(retry.get(), decorated);
if (cb.isPresent()) decorated = CircuitBreaker.decorateSupplier(cb.get(), decorated);
return decorated.get();
} finally {
retryPolicy.endCall();
}
// 출처: [[wiki/projects/ca-tmpl/data-layer-persistence-cache-outbound]]
// 실제 파일: adapter-outbound/.../OutboundRetryPolicy.java
private static final Set<HttpMethod> IDEMPOTENT_METHODS =
Set.of(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.PUT, HttpMethod.DELETE);
if (!IDEMPOTENT_METHODS.contains(ctx.method())) {
return false;
}
Sources / 근거 (canonical 인용 필수, derived layer 의무)
- wiki/projects/ca-tmpl/data-layer-persistence-cache-outbound — 이 글의 1차 canonical. persistence/cache/outbound 각각의 구현·부분 구현·planned 경계를 따른다.
- wiki/concepts/data-layer-persistence-cache-outbound — 관련 개념 문서. 구현 사실 출처로 쓰지 않는다.
사실 vs 의견 / Fact vs opinion 구분
- 사실: ca-tmpl에는 idempotency/outbox RDBMS adapter와 migration, OSIV-off startup guard, Hikari inter-knob startup guard, lower-layer cache SPI/router/fail-open, outbound HTTP baseline이 존재한다. 근거: wiki/projects/ca-tmpl/data-layer-persistence-cache-outbound
- 사실: SQLState classifier 전체, read replica lag metric/alert, cache-aside + Redisson distributed mutex + after-commit invalidation 전체 contract, 운영 tuning은 구현 완료로 말하지 않는다. 근거: wiki/projects/ca-tmpl/data-layer-persistence-cache-outbound
- 사실: 운영 배포, pool wait p99, cache hit ratio, circuit breaker 운영 경험은 없다. 근거: wiki/projects/ca-tmpl/data-layer-persistence-cache-outbound
- 의견: persistence/cache/outbound를 함께 다루더라도 실패 계약은 분리해서 설명해야 한다.
- 알지 못하는 것: production pool wait, slow query, cache hit rate, outbound dependency 장애율.
답할 수 있는 범위 / Answer boundary
- 자신 있게 답할 수 있는 후속 질문:
- fail-open cache와 fail-closed outbox의 차이는 무엇인가?
- OSIV off startup guard가 layer boundary와 어떤 관련이 있는가?
- Hikari knob guard는 운영 tuning과 어떻게 다른가?
- outbound HTTP retry에서 POST/PATCH를 제외한 이유는 무엇인가?
- 다음 글로 넘길 부분:
- 실제 DB/cache 운영 metric.
- SQLState classifier 전체 구현과 운영 alert.
- cache-aside after-commit invalidation의 full contract.
게시 체크리스트 / Publish checklist
- 모든 사실 주장에 canonical 링크 있음
- 사실 vs 의견 분리 명시됨
- 금지 마케팅 표현 없음
- 코드 예제 출처 명시
- 타깃 독자 가정과 톤 일치
/lint통과- 게시 URL 기록 (게시 후):