Files
document-haness/docs/clean-architecture-backend-template/final/evidence/raw/a14-f010-no-store.txt
T
DongHyeonkaandClaude Opus 5 b2963105a8 docs(keycloak-session-store): import the session-storage lab as a new project
The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.

Follows the import procedure in README.md.

  source/     the originating repository verbatim — 78 documents, 28 SVGs,
              8 manifests, plus .source-revision recording the commit
  final/      the SSOT
    document.md   729 lines written from the 29 experiment documents, not
                  concatenated: what was predicted, what was measured, and
                  where the measurement itself was wrong
    evidence/raw    125 outputs, flattened to <experiment>__<file> because
                    the originals collided (01-baseline.txt appeared three
                    times) and the audit only globs the top level
    evidence/meta   one per raw file; command and exitCode are null and the
                    README says why rather than inventing them
    evidence/browser  22 captures
    assets/       three diagrams through techviz
    .techviz/     their VizSpecs

A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.

Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.

verify-pipeline.py passes. audit-records.py reports no issues.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 22:51:59 +09:00

159 lines
8.7 KiB
Plaintext

# 필터가 무엇을 붙이고, 그것이 되돌려질 수 있다고 무엇이 말하는가
/**
* Applies the skeleton's default HTTP cache policy: every response gets {@code Cache-Control:
* no-store} and a {@code Vary: Accept, Accept-Encoding, Authorization} header. Defaults are set
* before the chain so a cacheable endpoint can opt in by returning a {@code ResponseEntity} whose
* {@code Cache-Control} header overwrites the default. See README for the design rationale.
*/
@Component
@Order(Ordered.HIGHEST_PRECEDENCE + 20)
public class CacheControlFilter extends OncePerRequestFilter {
static final String DEFAULT_CACHE_CONTROL = "no-store";
static final String DEFAULT_VARY = "Accept, Accept-Encoding, Authorization";
@Override
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
response.setHeader(ApiHeaders.CACHE_CONTROL, DEFAULT_CACHE_CONTROL);
response.setHeader(ApiHeaders.VARY, DEFAULT_VARY);
chain.doFilter(request, response);
}
# 그 정책을 이 필터가 혼자 갖도록 보안 설정이 프레임워크 기본값을 끈다
http.cors(c -> c.configurationSource(corsConfigurationSource()))
// Disable Spring Security's default Cache-Control writer; CacheControlFilter
// owns the cache header policy. See README for the design rationale.
.headers(headers -> headers.cacheControl(cache -> cache.disable()))
다만 픽스처 모듈은 그 설정을 스캔에서 뺀다 : 38: SecurityConfig.class,
39: MethodSecurityConfig.class
그 모듈이 적어 둔 제외 이유 :
// The standalone sample is a public demo app: URL access should not require a
// configured IdP or method-security principal. Production modules keep these guards.
그 모듈이 대신 두는 체인. 캐시 기록기를 끄지 않는다 :
http.securityMatcher("/**")
.csrf(csrf -> csrf.disable())
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth.anyRequest().permitAll());
return http.build();
}
# 조건부 읽기를 구현한 자리 전부와 그것을 켜는 조건
main 에서 응답에 개체 태그를 쓰는 파일 (아웃바운드 S3 클라이언트는 뺐다) :
WorkLogController.java
WebSuccessResponse.java
WebResponseContract.java
MvcDownloadResponseWriter.java
ReactiveDownloadResponseWriter.java
이 파일들과 304 판정 클래스를, 시험을 빼고 부르는 자리가 있는지 :
WebSuccessResponse 를 부르는 자리 : WebResponseContract.java (끝)
WebResponseContract 를 부르는 자리 : WebSuccessResponse.java (끝)
ConditionalReadEvaluator 를 부르는 자리 : (끝)
같은 패키지의 ETags 를 실제로 호출하는 자리 : WorkLogController.java (끝)
레지스트리가 선언한 런타임 컴포지션과 이 리프의 소속 :
"runtime_compositions": [
"app-bootstrap",
"sample-portfolio"
],
"modules": [
"id": "adapter-inbound-web",
"runtime_memberships": [
"app-bootstrap",
"sample-portfolio"
]
304 판정을 구현해 둔 클래스와 그 자바독 첫 줄 :
/**
* Decides whether a {@code GET} or {@code HEAD} may be answered with a 304.
내려받기 컨트롤러를 켜는 조건과 그 스위치의 출하값 :
24:import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
41:@ConditionalOnProperty(prefix = "app.fileserver-platform", name = "enabled", havingValue = "true")
854: enabled: ${APP_FILESERVER_PLATFORM_ENABLED:false}
.env:161:APP_FILESERVER_PLATFORM_ENABLED=false
.env.example:240:APP_FILESERVER_PLATFORM_ENABLED=false
# 내려받기 경로가 스스로 쓰는 태그와 지시자
37: response.setHeader(HttpHeaders.ETAG, descriptor.representation().strongEtag());
43: response.setHeader(HttpHeaders.CACHE_CONTROL, descriptor.cacheControl());
같은 문자열을 자바 밖에서 다시 쓰는 자리 :
.env:181:APP_FILESERVER_PLATFORM_DOWNLOAD_CACHE_CONTROL=private, no-store
app-bootstrap/src/main/resources/application.yml:880: cache-control: ${APP_FILESERVER_PLATFORM_DOWNLOAD_CACHE_CONTROL:private, no-store}
.env.example:255:APP_FILESERVER_PLATFORM_DOWNLOAD_CACHE_CONTROL=private, no-store
DownloadPolicy.java:23: /** Design default: single Range, {@code private, no-store}, attachment only. */
DownloadPolicy.java:25: return new DownloadPolicy(RangeBudget.unbounded(), "private, no-store", false);
FileserverPlatformSettings.java:127: @DefaultValue("private, no-store") String cacheControl,
# 픽스처 모듈 쪽 조건부 읽기 경로
/** Emits an {@code ETag}; honours {@code If-None-Match} with a 304 (no body). */
@GetMapping("/worklogs/{id}")
public ResponseEntity<WorkLogResponse> getOne(
@PathVariable String id,
@RequestHeader(value = ApiHeaders.IF_NONE_MATCH, required = false) String ifNoneMatch) {
WorkLog workLog = get.handle(new GetWorkLogQuery(toId(id)));
String etag = etagOf(workLog);
if (ETags.matches(ifNoneMatch, etag)) {
return ResponseEntity.status(HttpStatus.NOT_MODIFIED).eTag(etag).build();
}
return ResponseEntity.ok().eTag(etag).body(WorkLogWebMapper.toResponse(workLog));
}
이 모듈 main 에서 Cache-Control 을 스스로 정하는 자리 : (0)
# 둘을 조정하라고 만들어 둔 패키지
28줄 WebCacheHeaders.java
72줄 WebCachePolicy.java
120줄 WebCachePolicyCatalog.java
90줄 WebVaryPolicy.java
그 타입들을 이름으로 부르는 파일과 횟수 :
2 main · WebCacheHeaders.java
5 main · WebCachePolicy.java
23 main · WebCachePolicyCatalog.java
6 main · WebVaryPolicy.java
22 test · WebCachePolicyTest.java
cache 패키지 밖에서 부르는 파일 : (0)
# 그 패키지를 그렇게 둔 근거
/**
* The published cache profiles, their directives and the {@code Vary} rule.
*
* <p>CORE: a caching decision is a set of directives, not a framework call. Keeping it free of
* Spring is what lets the same profile be applied by the servlet writer, the reactive writer and
* the OpenAPI document without three renderings of it.
*/
CACHE("cache", "cache", WebModulePurity.CORE),
# 그 근거가 말한 적용자들이 실제로 무엇을 쓰는가
main 에서 Cache-Control 값을 정하는 자리 (헤더 이름 상수 두 파일은 뺐다) :
MvcDownloadResponseWriter.java:43 response.setHeader(HttpHeaders.CACHE_CONTROL, descriptor.cacheControl());
ReactiveDownloadResponseWriter.java:36 headers.set(HttpHeaders.CACHE_CONTROL, descriptor.cacheControl());
TusController.java:138 .header(HttpHeaders.CACHE_CONTROL, "no-store");
CacheControlFilter.java:24 static final String DEFAULT_CACHE_CONTROL = "no-store";
CacheControlFilter.java:31 response.setHeader(ApiHeaders.CACHE_CONTROL, DEFAULT_CACHE_CONTROL);
뺀 두 파일 :
ApiHeaders.java
WebHeaderName.java
연산 프로파일이 이름으로 들고 있는 캐시 정책과 그것을 읽는 자리 :
36: CachePolicyName cachePolicy,
95: CachePolicyName.standard(),
110: CachePolicyName.standard(),
cachePolicy() 를 읽는 자리 : (0)
지시자 문자열을 직접 들고 있는 main 자리 :
DownloadPolicy.java:25 return new DownloadPolicy(RangeBudget.unbounded(), "private, no-store", false);
FileserverPlatformSettings.java:127 @DefaultValue("private, no-store") String cacheControl,
TusController.java:138 .header(HttpHeaders.CACHE_CONTROL, "no-store");
CacheControlFilter.java:24 static final String DEFAULT_CACHE_CONTROL = "no-store";
WebCachePolicy.java:41 if (cacheControl.contains("no-store") && freshness.isPresent()) {
WebCachePolicy.java:43 "no-store and a freshness lifetime contradict each other: one says never keep this, the"
WebCachePolicyCatalog.java:36 "sensitive", "private, no-store", WebVaryPolicy.none(), false, Optional.empty());
WebCachePolicyCatalog.java:49 "public, max-age=31536000, immutable",
WebCachePolicyCatalog.java:65 "public, max-age=0, must-revalidate",
WebCachePolicyCatalog.java:80 "private, max-age=30",
CachePolicyName.java:28 return new CachePolicyName("no-store");
GraphQlHttpGetCachePolicy.java:13 NO_STORE("no-store"),
GraphQlHttpGetCachePolicy.java:16 PRIVATE("private, max-age=0"),
GraphQlHttpGetCachePolicy.java:21 SHARED("public, max-age=60");
목록이 발행하는 프로파일 이름과 그 지시자 :
36: "sensitive", "private, no-store", WebVaryPolicy.none(), false, Optional.empty());
48: "immutable-asset",
64: "revalidated",
79: "browser-private",