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>
14 KiB
kind, slug, title, topic, project, status, sourceRevision, evidenceCapturedOn, rootTreeNode, body, assets, evidence, source
| kind | slug | title | topic | project | status | sourceRevision | evidenceCapturedOn | rootTreeNode | body | assets | evidence | source | |||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| CASE | a14-f015-virtualthreadprofile-propertyname | 켜는 속성이라고 적힌 메서드가 어느 게이트도 쓰지 않는 이름을 돌려준다 | web-inbound-and-http-surface | clean-architecture-backend-template | 게시 전 | 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 | 2026-09-03 | case:a14-f015-virtualthreadprofile-propertyname | case-a14-f015-virtualthreadprofile-propertyname.body.md |
|
|
|
켜는 속성이라고 적힌 메서드가 어느 게이트도 쓰지 않는 이름을 돌려준다
가상 스레드 프로파일에 propertyName() 이 있고 자바독이 이것을 켜는 속성이라고 적었다. 그 메서드는 문자열을 그대로 적어 돌려주는데 mvc- 접두사가 빠져 있어서, 실제 바인딩과 게이트가 쓰는 이름과 다르다. 그 이름 자체는 저장소에 이 반환문에만 있다. 2026-08-13 계획 문서가 적은 게이트 접두사에 이름 enabled 를 합치면 나오는 키가 바로 그 이름이다.
관계
- 선언된 Advanced 능력 열둘 중 속성 이름을 읽는 코드가 있는 것은 둘이다 거기서 이 메서드는 속성 이름을 만들기만 하는 둘 중 하나로 세었다. 그 이름이 왜 게이트와 어긋나는지는 여기서 다룬다.
- 호출자 없이 들어온 메서드이고, 도달 불가라던 가지는 켜지면 반대로 답한다 둘 다 부르는 곳이 없어서 지금은 아무 일도 일어나지 않는다. 다만 a14-f002 는 검사 메서드 전체가 그렇고, 여기는 배선된 타입에 붙은 정적 메서드 하나가 그렇다.
- 선언 순서를 단언하는 테스트는 그 순서를 읽는 코드가 있을 때만 게이트다 형제 두 타입에는 이름을 단언하는 시험이 있다. 기대값은 손으로 적은 문자열이거나 상수 개수다. 그 이름을 읽는 코드가 있는지는 어느 쪽도 보지 않는다.
문제
가상 스레드 스위치의 이름이 코드 세 군데에 다른 철자로 나온다고 원문이 적었다. 그 셋을 확인하고, 같은 이름의 메서드를 가진 다른 타입까지 함께 봤다.
결론
같지 않다. VirtualThreadProfile:74-77 만 mvc- 가 없는 이름을 돌려준다.
propertyName() 이라는 메서드를 선언하는 타입이 이 저장소에 셋 있다. 웹 능력 열거형과 웹소켓 능력 열거형은 상수 이름에서 문자열을 만들고, 가상 스레드 프로파일만 문자열을 적어 두었다. 적어 둔 그 하나만 static 이고, 틀린 것도 그 하나다.
발행된 이름 그대로는 저장소에 한 번, 자기 반환문에만 나온다. 접두사까지만 잘라 찾으면 한 줄이 더 나온다. 2026-08-13 확장 계획 문서가 아직 만들지 않은 설정 클래스를 virtual-threads 로 게이트하라고 적은 자리다. 계획대로 만들었다면 그 게이트가 읽었을 속성이 이 메서드가 돌려주는 이름이다. 구현된 게이트는 mvc-virtual-threads 를 쓴다. 원문은 계획 문서를 언급하지 않는다.
코드에서 그 이름을 읽거나 쓰는 자리는 없다. 운영자용 문서 두 편은 맞는 이름을 쓴다. docs/web/advanced-capabilities.md:8 의 지원 표와 docs/web/virtual-thread-profile.md:25 의 yml 예시다.
두 이름을 각각 참으로 두고 웹 컨텍스트를 띄웠다. 게이트된 설정 클래스만 올린 구성과, 루트가 이 리프에 거는 패키지를 그대로 스캔한 구성 둘이다. 두 구성의 표는 본문에 있다.
출하 쪽 구성에서는 설정 빈이 늘 등록되고, 속성 이름이 좌우하는 것은 게이트다. 발행된 이름은 두어도 결과가 같다. 다만 그 구성은 SecuritySettings 가 요구하는 ca-skeleton.security.issuer-uri 를 채워야 기동한다.
이 설정 클래스가 발행하는 설정 메타데이터에도 그 이름은 없다. 고정 소스에 설정 프로세서를 돌려 뽑으면 접두사 backend.web.advanced.mvc-virtual-threads 아래 이름 여섯이 나온다. 리프가 프로세서를 선언하도록 루트 빌드가 리프마다 check 에 걸어 두었으므로, 이 메타데이터는 배포판에 늘 들어간다.
한편 이 타입 자체는 배선되어 있다. VirtualThreadSettings:77 이 레코드를 만들고 VirtualThreadMvcConfiguration:40 이 빈으로 등록하며 VirtualThreadSettings:91 이 기동 검증에서 생성자를 다시 부른다. 부르는 곳이 없는 것은 그 정적 메서드 하나뿐이다.
원문은 이 건에 권고를 적지 않았다. 같은 절의 권고는 앞 항목인 P2 의 것이다. 이 메서드는 지우거나 실제 게이트 이름을 돌려주게 고치면 된다. 열거형이 같은 능력에 대해 맞는 이름을 계산하므로 그것을 부르면 된다.
판정은 P3 이고 원문과 같다. 부르는 곳이 없어 실행 결과가 달라지지 않는다.
검증 환경
OpenJDK : 21.0.12 Spring Boot : 4.0.8 확인 방식 : 세 propertyName() 구현과 호출자·메서드 참조 대조, git 추적 파일 전체에서 발행된 이름 검색, 두 이름을 각각 참으로 둔 웹 컨텍스트 기동 두 구성, 이 설정 클래스의 메타데이터 재생성 소스 수정 : x
재현 조건
원문 근거는 분석 문서의 §35.3 표와 #L1279 절이다.
- propertyName() 을 선언하는 세 자리를 찾아 구현을 나란히 읽는다.
- 셋 각각을 부르는 자리를 센다. 메서드 참조도 함께 센다.
- git 추적 파일 전체에서 발행된 이름을 찾고, 접두사까지만 잘라서도 찾는다. 운영자용 문서가 쓰는 이름도 함께 본다.
- 실제 바인딩 접두사와 게이트 조건을 읽는다.
- 출하 조립 루트가 이 리프의 설정을 어디에서 등록하는지 확인한다.
- 게이트된 설정만 올린 구성과, 루트와 같은 스캔을 이 하위 패키지에 건 구성에서 두 이름을 각각 참으로 두고 컨텍스트를 띄운다.
- 이 설정 클래스 한 파일에만 설정 프로세서를 걸어 어떤 이름이 실리는지 본다.
- 그 타입을 만들고 등록하는 자리를 찾아 타입 자체가 배선되어 있는지 확인한다.
본문
자바독이 용도를 적고, 바로 아래에서 문자열을 그대로 돌려준다.
// VirtualThreadProfile.java:74-77
/** The property that turns this on. */
public static String propertyName() {
return "backend.web.advanced.virtual-threads.enabled";
}
같은 메서드를 가진 형제 둘은 이름을 계산한다
:::evidence key="a14-f015-virtualthreadprofile-propertyname" alt="저장소 루트에서 돌린 정적 검색 출력 78줄. 선언 자리를 검색으로 센 결과 셋과 그 구현이 차례로 보이고, 앞의 둘은 상수 이름에서 문자열을 조립하며 셋째는 문자열을 그대로 적는다. 이어서 셋 각각을 부르는 자리가 메서드 참조까지 포함해 나오고, 발행된 이름 그대로 나오는 한 줄과 접두사까지만 잘라 찾았을 때 나오는 두 줄과 그 계획 문서가 접두사 다음 줄에 적은 name 항목, 운영자용 문서 두 편이 쓰는 이름, 실제 바인딩 접두사와 게이트 조건, 출하 조립 루트의 속성 스캔 애너테이션과 이 리프가 그 목록에 든 줄과 그 목록의 패키지 수, 그 타입을 만들고 빈으로 등록하는 자리, 루트가 거는 그 패키지 아래의 @ConfigurationProperties 타입 수와 그중 값을 요구하는 하나, 이 리프의 설정 프로세서 선언과 그것을 리프마다 check 에 거는 루트 빌드 블록이 보인다." caption="세 propertyName 과 그 이름이 나오는 자리 — 78줄 · exit 0" zoom="true" :::
앞의 둘은 name() 을 소문자로 바꾸고 밑줄을 하이픈으로 바꿔 접두사에 붙인다. 상수를 새로 넣어도 이름이 상수와 어긋나지 않는다. 셋째는 그 조립을 하지 않는다.
호출은 앞의 둘에만 있다. WebAdvancedReleaseTest 의 :48·:52 와 ResumeTokenTest 의 :187·:191 이다. 각 쌍의 앞은 직접 호출이고 뒤는 메서드 참조다. 앞은 문자열을 그대로 적어 비교하고, 뒤는 그 열거형이 선언한 이름이 서로 겹치지 않는지만 센다. 웹 쪽은 열둘, 웹소켓 쪽은 열넷이다.
그 접두사는 코드 밖에 한 번 더 있다
이름 그대로 찾으면 자기 반환문 한 줄이다. 접두사까지만 잘라 찾으면 2026-08-13 확장 계획 문서가 한 줄 더 나온다.
docs/web-superpowers-package/.../2026-08-13-web-advanced-capabilities-expansion-plan.md:195-202
Expected: FAIL because no virtual-thread MVC profile or admission guard exists.
- [ ] **Step 3: Implement the minimum production contract**
(199 의 java 펜스 한 줄 생략)
@Configuration
@ConditionalOnProperty(
prefix = "backend.web.advanced.virtual-threads",
계획은 이 설정 클래스가 아직 없다고 적은 뒤 이 접두사로 게이트하라고 지시한다. 다음 줄이 name = "enabled" 이므로 그 게이트가 읽는 속성은 이 메서드가 돌려주는 이름과 같다. 구현된 게이트는 다른 접두사를 쓴다.
// VirtualThreadSettings.java:17 · VirtualThreadMvcConfiguration.java:32-35
@ConfigurationProperties(prefix = "backend.web.advanced.mvc-virtual-threads")
...
@ConditionalOnProperty(
prefix = "backend.web.advanced.mvc-virtual-threads",
name = "enabled",
havingValue = "true")
두 이름을 각각 두고 띄워 보면
:::evidence key="a14-f015-virtualthreadprofile-propertyname-bind" alt="JVM 프로브 출력 29줄. 메서드가 반환하는 이름과 실제 게이트 이름이 나란히 보인 뒤, 두 가지 구성의 결과가 표로 이어진다. 게이트된 설정만 올린 구성에서는 발행된 이름 쪽에 프로파일 빈도 설정 빈도 없고, 실제 이름 쪽에는 둘 다 있다. 루트가 이 리프에 거는 패키지를 그대로 스캔한 구성에서는 아무것도 채우지 않으면 보안 설정이 필수값을 요구해 기동이 멈추고, 그 한 값을 채운 세 행에서는 설정 빈이 모두 있고 enabled 값만 갈리며, 발행된 이름 행이 그 이름만 빼고 같게 둔 행과 같은 값을 낸다. 끝으로 고정 소스에서 새로 뽑은 설정 메타데이터의 이름 여섯과, 발행된 이름이 그 안에 없다는 확인이 보인다." caption="두 이름을 각각 참으로 둔 두 구성과 재생성한 설정 메타데이터 — 29줄 · exit 0" zoom="true" :::
두 구성 모두 예산 세 값을 늘 함께 준다. 그 값이 없으면 실제 이름을 참으로 둔 행이 바인딩 검증에서 멈춘다.
첫 구성은 게이트된 설정 클래스만 올린다. 발행된 이름 쪽은 프로파일 빈도 설정 빈도 없다. 다만 그 설정 클래스가 이 구성에서 설정 빈을 등록하는 유일한 자리이므로, 설정 빈이 없는 것은 게이트가 닫힌 결과다.
출하 조립은 게이트와 무관하게 이 설정을 등록한다.
// CaSkeletonApplication.java:58-80 중 세 줄
@ConfigurationPropertiesScan(
basePackages = {
...
"dev.caskeleton.adapter.inbound.web",
두 번째 구성은 그 애너테이션을 그 패키지 그대로 건다. 리프 전체를 걸면 SecuritySettings 가 ca-skeleton.security.issuer-uri 를 요구해 기동이 거기서 멈추므로, 그 한 값만 채우고 나머지는 두었다. 이 발견과 무관한 값이고, 리프의 여덟 설정 중 값을 요구하는 것은 그것 하나다. 그러면 설정 빈이 세 행 모두 있고 enabled 값만 갈린다. 발행된 이름을 참으로 둔 행은 그 이름을 빼고 같게 둔 없음 행과 똑같은 값을 낸다. 그 속성을 바인딩하겠다고 선언한 클래스가 없어서 기동을 막지도 않는다.
같은 프로브가 이 설정 클래스에 설정 프로세서를 돌려 메타데이터를 새로 뽑는다. 나오는 여섯은 모두 접두사 backend.web.advanced.mvc-virtual-threads 아래에 있고, 발행된 이름은 없다.
이름을 잘못 발행하는 타입은 살아 있다
// VirtualThreadSettings.java:77-78 · VirtualThreadMvcConfiguration.java:39-41
public VirtualThreadProfile toProfile() {
return new VirtualThreadProfile(enabled, admissionLimit, databasePoolSize, outboundBulkhead);
@Bean
public VirtualThreadProfile virtualThreadProfile(VirtualThreadSettings properties) {
return properties.toProfile();
설정이 이 레코드를 만들고 설정 클래스가 빈으로 등록한다. VirtualThreadSettings:91 은 기동 검증에서 생성자를 다시 불러 예산이 맞는지 본다. 죽은 것은 타입이 아니라 거기 붙은 정적 메서드 하나다.
확인하지 못한 것
발행된 이름을 설정했을 때 기동 로그에 아무 말도 나오지 않는지는 확인하지 못했다. 프로브가 로깅을 끄고 돌았기 때문이다. 확인한 것은 기동이 실패하지 않는다는 것까지다.
두 번째 구성은 이 리프 하나만 스캔한다. 루트가 거는 스무 개 패키지를 모두 올린 것이 아니므로, 다른 리프의 설정이 이 결과에 영향을 주는지는 보지 않았다.
메타데이터를 다시 만들 때 이 설정 클래스 한 파일만 프로세서에 넣었다. 여기 나온 여섯은 이 클래스가 발행하는 이름이지 리프 전체 목록이 아니다.
계획 문서와 구현 중 어느 쪽 커밋이 먼저인지는 이력에서 보지 않았다. 확인한 것은 고정 리비전에 두 파일이 함께 있고 계획 쪽이 게이트 접두사로 이 이름을 적었다는 것까지다.