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>
141 lines
11 KiB
Plaintext
141 lines
11 KiB
Plaintext
# 가족 문서가 이 항목을 어떻게 들고 있는가
|
|
messaging/CLAUDE.md:70:## application이 이 family에 도달하는 경로 (MSG-015, 미해결)
|
|
messaging/CLAUDE.md:82:이것이 리뷰의 **MSG-015 (P0)** 이며 닫히지 않았다. 지금 안전한 이유는 설계가 아니라 기본값이다 —
|
|
|
|
# 두 스택의 설정 클래스와 생산자 빈
|
|
bootstrap/messaging/KafkaSenderConfig.java:37 @Configuration(proxyBeanMethods = false)
|
|
bootstrap/messaging/KafkaSenderConfig.java:38 @ConditionalOnProperty(name = "app.messaging.broker", havingValue = "kafka")
|
|
bootstrap/messaging/KafkaSenderConfig.java:39 public class KafkaSenderConfig {
|
|
bootstrap/messaging/KafkaSenderConfig.java:61 @Bean(name = "kafkaSeamProducer", destroyMethod = "close")
|
|
bootstrap/messaging/KafkaSenderConfig.java:62 @ConditionalOnMissingBean(name = "kafkaSeamProducer")
|
|
bootstrap/messaging/KafkaSenderConfig.java:63 public Producer<String, String> kafkaSeamProducer(KafkaAdapterSettings settings) {
|
|
autoconfigure/KafkaMessagingAutoConfiguration.java:126 * @return the producer, closed with the context
|
|
autoconfigure/KafkaMessagingAutoConfiguration.java:127 */
|
|
autoconfigure/KafkaMessagingAutoConfiguration.java:128 @Bean(destroyMethod = "close")
|
|
autoconfigure/KafkaMessagingAutoConfiguration.java:129 @ConditionalOnMissingBean
|
|
autoconfigure/KafkaMessagingAutoConfiguration.java:130 public org.apache.kafka.clients.producer.Producer<byte[], byte[]> messagingKafkaProducer(
|
|
autoconfigure/KafkaMessagingAutoConfiguration.java:131 org.springframework.core.env.Environment environment) {
|
|
|
|
# 플랫폼 쪽이 켜지는 사슬
|
|
main · messaging/autoconfigure/MessagingPlatformRootAutoConfiguration.java:28 @ConditionalOnProperty(prefix = MessagingSettings.PREFIX, name = "enabled", havingValue = "true")
|
|
main · messaging/autoconfigure/MessagingProviderSelection.java:48 "kafka", KafkaMessagingAutoConfiguration.class,
|
|
|
|
# broker 만 준 시험이 통과하는 이유 — 게이트 뒤 설정을 손으로 등록한다
|
|
test · adapter/outbound/messaging/MessagingConfigTest.java:41 .withUserConfiguration(KafkaAdapterConfig.class)
|
|
test · OptionalAdapterBeanGatingTest.java:59 private final ApplicationContextRunner runner =
|
|
test · OptionalAdapterBeanGatingTest.java:60 new ApplicationContextRunner()
|
|
test · OptionalAdapterBeanGatingTest.java:61 // Registered as a real auto-configuration entry, which is how the composition root
|
|
test · OptionalAdapterBeanGatingTest.java:62 // reaches it: the HTTP Client platform's off state is a property of that entry, not of a
|
|
test · OptionalAdapterBeanGatingTest.java:63 // hand-assembled subset of its children.
|
|
test · OptionalAdapterBeanGatingTest.java:64 .withConfiguration(AutoConfigurations.of(HttpClientPlatformAutoConfiguration.class))
|
|
test · OptionalAdapterBeanGatingTest.java:65 .withUserConfiguration(
|
|
test · OptionalAdapterBeanGatingTest.java:66 OutboundSupportConfig.class,
|
|
test · OptionalAdapterBeanGatingTest.java:67 MessagingConfig.class,
|
|
test · OptionalAdapterBeanGatingTest.java:68 KafkaAdapterConfig.class,
|
|
test · OptionalAdapterBeanGatingTest.java:69 NotificationConfig.class,
|
|
test · OptionalAdapterBeanGatingTest.java:70 SlackNotificationAdapterConfig.class,
|
|
두 시험이 컴포지션 루트를 쓰는가 :
|
|
MessagingConfigTest.java CaSkeletonApplication 언급 0 건
|
|
OptionalAdapterBeanGatingTest.java CaSkeletonApplication 언급 0 건
|
|
KafkaSenderConfig 가 그 슬라이스에 등록되는가 :
|
|
MessagingConfigTest.java KafkaSenderConfig 언급 0 건
|
|
OptionalAdapterBeanGatingTest.java KafkaSenderConfig 언급 0 건
|
|
|
|
# 이 조합을 거르는 검증기가 있는가
|
|
CapabilityDependencyValidator.java:64 }
|
|
CapabilityDependencyValidator.java:65 if (relay && !outbox) {
|
|
CapabilityDependencyValidator.java:66 violations.add(
|
|
CapabilityDependencyValidator.java:67 "ca-skeleton.outbox.relay-enabled=true only starts the scheduler for a capability that is "
|
|
CapabilityDependencyValidator.java:68 + "off; set ca-skeleton.outbox.enabled=true or turn the relay off.");
|
|
CapabilityDependencyValidator.java:69 }
|
|
CapabilityDependencyValidator.java:70 if (messaging && broker.isEmpty()) {
|
|
CapabilityDependencyValidator.java:71 violations.add(
|
|
CapabilityDependencyValidator.java:72 "app.messaging.enabled=true needs a transport; set app.messaging.broker to a registered "
|
|
CapabilityDependencyValidator.java:73 + "broker id.");
|
|
CapabilityDependencyValidator.java:74 }
|
|
CapabilityDependencyValidator.java:75 if ("jdbc"
|
|
CapabilityDependencyValidator.java:76 .equalsIgnoreCase(
|
|
CapabilityDependencyValidator.java:77 environment.getProperty(
|
|
CapabilityDependencyValidator.java:78 "ca-skeleton.capabilities.idempotency.provider", "disabled"))
|
|
CapabilityDependencyValidator.java:79 && !jpa) {
|
|
CapabilityDependencyValidator.java:80 violations.add(
|
|
|
|
# 운영 문서가 이 키를 무엇이라고 설명하는가
|
|
docs/runbooks/outbox-publish-failed.md:26 ### Step 1 — 확인
|
|
docs/runbooks/outbox-publish-failed.md:27 1. ERROR log에서 `OUTBOX_PUBLISH_FAILED` 라인 확인: `event_type`, `event_id`, `correlation_id`, `attempt_count` 추출
|
|
docs/runbooks/outbox-publish-failed.md:28 2. broker 상태 확인: `APP_MESSAGING_BROKER` 값과 broker endpoint 가용성. 이 키는 활성화 스위치가
|
|
docs/runbooks/outbox-publish-failed.md:29 아니라 **선택자**다 — messaging을 끄는 것은 `APP_MESSAGING_ENABLED=false`이고, 이 값을 비운다고
|
|
docs/runbooks/outbox-publish-failed.md:30 messaging이 꺼지지는 않는다.
|
|
docs/runbooks/outbox-publish-failed.md:31 - `APP_MESSAGING_BROKER`가 공백인 채로 relay가 켜져 있으면 **애플리케이션이 기동하지 않는다**
|
|
docs/runbooks/outbox-publish-failed.md:32 (`OutboxRelayBrokerRequirementValidator`, MSG-024). 이 조합에서는 publish가 전부
|
|
|
|
# 출하 설정이 두 키를 어떻게 주는가
|
|
compose-profile-contracts.json:170: "APP_MESSAGING_ENABLED": "true",
|
|
compose-profile-contracts.json:171: "APP_MESSAGING_BROKER": "kafka",
|
|
compose-profile-contracts.json:202: "APP_MESSAGING_ENABLED": "true",
|
|
compose-profile-contracts.json:203: "APP_MESSAGING_BROKER": "kafka",
|
|
compose-profile-contracts.json:604: "APP_MESSAGING_ENABLED": "true",
|
|
compose-profile-contracts.json:605: "APP_MESSAGING_BROKER": "kafka",
|
|
.env:38:APP_MESSAGING_BROKER=
|
|
.env.example:229:APP_MESSAGING_BROKER=
|
|
.env.example:299:APP_MESSAGING_ENABLED=false
|
|
.env.local.example:20:APP_MESSAGING_ENABLED=false
|
|
.env.local.example:21:APP_MESSAGING_BROKER=
|
|
|
|
# KafkaSenderConfig 가 조건에서 물러나는가
|
|
KafkaSenderConfig.java:60 */
|
|
KafkaSenderConfig.java:61 @Bean(name = "kafkaSeamProducer", destroyMethod = "close")
|
|
KafkaSenderConfig.java:62 @ConditionalOnMissingBean(name = "kafkaSeamProducer")
|
|
KafkaSenderConfig.java:63 public Producer<String, String> kafkaSeamProducer(KafkaAdapterSettings settings) {
|
|
KafkaSenderConfig.java:64 Map<String, Object> config = new HashMap<>();
|
|
KafkaSenderConfig.java:65 config.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, String.join(",", settings.brokers()));
|
|
|
|
# 이음매 설정 클래스 자신은 스캔 대상인가
|
|
KafkaSenderConfig.java:1 package dev.caskeleton.bootstrap.messaging;
|
|
AUTO_CONFIGURED_PACKAGES 가 bootstrap.messaging 을 제외하는가 : 0 건
|
|
|
|
# 이음매 쪽 생산자가 요구하는 설정 빈은 어디서 오는가
|
|
main · adapter/outbound/messaging/kafka/KafkaAdapterConfig.java:19 @EnableConfigurationProperties(KafkaAdapterSettings.class)
|
|
main · adapter/outbound/messaging/autoconfigure/MessagingBridgeRootAutoConfiguration.java:21 @ConditionalOnProperty(prefix = "app.messaging", name = "enabled", havingValue = "true")
|
|
main · adapter/outbound/messaging/autoconfigure/MessagingBridgeRootAutoConfiguration.java:23 @Import({MessagingConfig.class, KafkaAdapterConfig.class})
|
|
|
|
# 그 설정 클래스가 컴포넌트 스캔으로 들어올 수 있는가
|
|
CaSkeletonApplication.java:41 @ComponentScan(
|
|
CaSkeletonApplication.java:42 basePackages = {
|
|
CaSkeletonApplication.java:43 "dev.caskeleton.bootstrap",
|
|
CaSkeletonApplication.java:44 "dev.caskeleton.adapter",
|
|
CaSkeletonApplication.java:45 "dev.caskeleton.application",
|
|
CaSkeletonApplication.java:46 "dev.caskeleton.domain",
|
|
CaSkeletonApplication.java:47 "dev.caskeleton.shared"
|
|
CaSkeletonApplication.java:48 },
|
|
CaSkeletonApplication.java:49 excludeFilters = {
|
|
CaSkeletonApplication.java:50 @ComponentScan.Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
|
|
CaSkeletonApplication.java:51 @ComponentScan.Filter(
|
|
CaSkeletonApplication.java:52 type = FilterType.CUSTOM,
|
|
CaSkeletonApplication.java:53 classes = AutoConfigurationExcludeFilter.class),
|
|
CaSkeletonApplication.java:54 @ComponentScan.Filter(
|
|
CaSkeletonApplication.java:55 type = FilterType.REGEX,
|
|
CaSkeletonApplication.java:56 pattern = CaSkeletonApplication.AUTO_CONFIGURED_PACKAGES)
|
|
CaSkeletonApplication.java:57 })
|
|
CaSkeletonApplication.java:100 static final String AUTO_CONFIGURED_PACKAGES =
|
|
CaSkeletonApplication.java:101 "dev\\.caskeleton\\.bootstrap\\.autoconfigure\\..*"
|
|
CaSkeletonApplication.java:105 + "|dev\\.caskeleton\\.adapter\\.outbound\\.messaging\\..*"
|
|
ConfigurationPropertiesScan 목록에 adapter.outbound.messaging 이 있는가 :
|
|
0 건
|
|
|
|
# broker=kafka 를 쓰는 시험마다 같은 파일이 enabled 를 주는가
|
|
MessagingConfigTest.java broker=kafka 3 줄 · enabled= 0 줄
|
|
OptionalAdapterBeanGatingTest.java broker=kafka 1 줄 · enabled= 0 줄
|
|
MessagingLiveRoundTripQualificationTest.java broker=kafka 1 줄 · enabled= 1 줄
|
|
MessagingStarterOffContractTest.java broker=kafka 3 줄 · enabled= 7 줄
|
|
enabled 를 한 줄도 주지 않는 파일의 시험 이름 :
|
|
MessagingConfigTest.java:25 void blankBrokerBindsDisabledSentinelsCharacterization() {
|
|
MessagingConfigTest.java:39 void selectedKafkaBrokerWithoutProjectSenderFailsStartupCharacterization() {
|
|
MessagingConfigTest.java:54 void selectedBrokerIdMismatchFailsStartupCharacterization() {
|
|
MessagingConfigTest.java:76 public void send(
|
|
OptionalAdapterBeanGatingTest.java:77 void allOptionalAdaptersAreDisabledByDefaultAndTheAppStillStarts() {
|
|
OptionalAdapterBeanGatingTest.java:114 void kafkaSelectedRegistersTheBrokerAndBindsTheRealPublishers() {
|
|
OptionalAdapterBeanGatingTest.java:135 void slackWebhookEnabledContributesTheProviderAndRoutesBoundNotifications() {
|
|
OptionalAdapterBeanGatingTest.java:153 void googleEmailEnabledContributesTheProviderAndRoutesBoundNotifications() {
|
|
OptionalAdapterBeanGatingTest.java:170 void aRouteBindingToADisabledProviderFailsStartup() {
|