Files
document-haness/docs/clean-architecture-backend-template/analysis/messaging/messaging-schema-json.md
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

29 KiB

messaging-schema-json 완전 해부

상태: COMPLETE 기준 revision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 분석 범위: src/messaging/messaging-schema-json SSOT owner: messaging-schema-json integration/family document: analysis/19-messaging-platform.md (secondary, INTEGRATION_ONLY)


0. SSOT identity / 커버리지와 숫자 지도

  • registered leaf id: messaging-schema-json
  • canonical state analysisFile: analysis/messaging/messaging-schema-json.md
  • source path: src/messaging/messaging-schema-json
  • registry allowed_dependencies: ["messaging-core-api", "messaging-schema-api"]
  • registry runtime_memberships: ["app-bootstrap"]

숫자

항목
production Java 파일 1
production LOC 226
패키지 1 (dev.caskeleton.messaging.schema.json)
test 파일 3
test 메서드(실행 확인) 18
외부 의존성 1 (tools.jackson.core:jackson-databind, implementation)

이 leaf는 클래스 하나다: JacksonMessageCodec. 그리고 messaging 플랫폼에서 production 소비자를 가진 유일한 codec이다(§12.1).

Coverage ledger

scope/file group count disposition reason
.../json/JacksonMessageCodec.java 1 FULL_READ 226줄 전문
src/test/java/** 3 FULL_READ 전문
build.gradle 1 FULL_READ 8줄
gradle.lockfile 1 STRUCTURAL_ONLY 잠금 파일
build/** EXCLUDED 빌드 산출물

UNCLASSIFIED 0.


1. 모듈의 정체와 경계

Stable JSON codec 하나. MessageCodec(schema-api)을 구현하고 Jackson 3(tools.jackson.* 네임스페이스)을 쓴다.

javadoc이 "기본 codec으로 노출해도 안전한 이유" 셋을 명시한다.

// JacksonMessageCodec.java:29-37
 * <p>Three things make this safe to expose as the default. The message-type registry is closed, so
 * a payload class only becomes reachable when someone registered it. The parser is constrained on
 * depth, document length, and duplicate keys, so a hostile document cannot exhaust the consumer
 * before the handler ever runs. And the encoded size is checked against the destination limit here
 * rather than at the broker, so an oversized payload fails locally with {@code NOT_TRANSMITTED}
 * evidence instead of ambiguously mid-flight.
 *
 * <p>Polymorphic default typing is never enabled. It is the mechanism behind most JSON
 * deserialization gadget chains, and no legitimate message contract needs it.

세 번째가 messaging-core-api의 3상태 발행 결과와 직접 연결된다 — 크기 초과를 브로커가 아니라 여기서 잡으면 NOT_TRANSMITTED 증거가 붙은 REJECTED가 되고, 브로커에서 잡히면 AMBIGUOUS가 된다. 전자는 버려도 안전하고 후자는 아니다.

Jackson 의존성은 implementation이다 — public 시그니처에 Jackson 타입이 없기 때문이다. 형제 leaf(schema-avro, schema-protobuf, cloudevents)는 vendor 타입이 public 시그니처에 나오므로 api로 선언했고 build.gradle에 그 이유를 주석으로 적었다. src/messaging/CLAUDE.md:40-43의 게이트가 이 구분을 강제한다.


2. 의존성과 런타임 배선

들어오는 것: messaging-core-api(api), messaging-schema-api(api), jackson-databind(implementation).

나가는 것: messaging-spring-boot-starter(registry allowed_dependencies에 포함).

실제 배선 지점이 하나 있다 — 이 플랫폼에서 유일하게 조립되는 codec이다.

// messaging-spring-boot-starter/.../MessagingCoreAutoConfiguration.java:360-366
@ConditionalOnMissingBean(dev.caskeleton.messaging.schema.MessageCodecRegistry.class)
public dev.caskeleton.messaging.runtime.RegisteredMessageCodecs messagingCodecs(
    ObjectProvider<MessageContracts> contracts) {
  return dev.caskeleton.messaging.runtime.RegisteredMessageCodecs.of(
      dev.caskeleton.messaging.schema.json.JacksonMessageCodec.of(
          contracts.getIfAvailable(MessageContracts::none).byKey()));
}

RegisteredMessageCodecs.of(defaultCodec, codecs...)의 varargs 자리가 비어 있다. 즉 출하 구성의 codec registry에는 JSON 하나만 들어간다. Avro·Protobuf·raw bytes는 등록되지 않는다.

두 번째 배선 지점은 상수 참조다.

// 같은 파일 :410-413
new dev.caskeleton.messaging.policy.PayloadPolicy(
    JacksonMessageCodec.DEFAULT_MAX_BYTES,
    JacksonMessageCodec.DEFAULT_MAX_BYTES / 2),

payload 정책의 상한이 JSON codec의 상수에서 파생된다. 포맷 중립이어야 할 admission 정책이 한 포맷의 클래스 상수를 참조한다 — §17에서 다룬다.

contracts.getIfAvailable(MessageContracts::none)이 기본값이므로, 애플리케이션이 MessageContracts bean을 내놓지 않으면 빈 registry로 codec이 만들어진다. 그 codec은 모든 encode/decodeUNKNOWN_MESSAGE_TYPE으로 거절한다.


3. 패키지/컴포넌트 지도

클래스 하나, 공개 표면 6개.

멤버 종류 용도
DEFAULT_MAX_BYTES = 1,048,576 public 상수 starter의 payload 정책이 참조
MAX_NESTING_DEPTH = 100 public 상수 파서 깊이 상한
of(Map) factory 기본 1 MiB
of(Map, int) factory 명시 상한
testingDefault(MessageType, Class) factory 단일 계약, v1
testingDefault(MessageType, SchemaVersion, Class) factory 단일 계약, 명시 버전

private 상수 둘: MAX_STRING_CHARACTERS = 5,000,000, MAX_NUMBER_DIGITS = 1,000.


4. 계약·불변식·상태 모델

4.1 파서 강화 — strictMapper

// JacksonMessageCodec.java:207-225
JsonFactory factory =
    JsonFactory.builder()
        .streamReadConstraints(
            StreamReadConstraints.builder()
                .maxNestingDepth(MAX_NESTING_DEPTH)        // 100
                .maxDocumentLength(maxBytes)               // = codec 상한
                .maxNumberLength(MAX_NUMBER_DIGITS)        // 1,000
                .maxStringLength(MAX_STRING_CHARACTERS)    // 5,000,000
                .maxNameLength(MAX_STRING_CHARACTERS)
                .build())
        .enable(StreamReadFeature.STRICT_DUPLICATE_DETECTION)
        .build();
return JsonMapper.builder(factory)
    .enable(DeserializationFeature.FAIL_ON_READING_DUP_TREE_KEY)
    .enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS)
    .enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
    .build();

여섯 가지 방어가 한 곳에 있다.

설정 막는 것
maxNestingDepth(100) 중첩 폭탄으로 파서 스택 소진
maxDocumentLength(maxBytes) 문서 길이 — codec 상한과 동일
maxNumberLength(1000) 초대형 BigDecimal 파싱 비용
maxStringLength/maxNameLength 단일 토큰 메모리
STRICT_DUPLICATE_DETECTION + FAIL_ON_READING_DUP_TREE_KEY 중복 키 — 파서마다 "먼저/나중 승리"가 달라 파싱 차이 공격이 됨
FAIL_ON_TRAILING_TOKENS 문서 뒤 추가 JSON — 두 번째 문서를 조용히 무시하는 것
FAIL_ON_UNKNOWN_PROPERTIES 미등록 필드

그리고 polymorphic default typing을 켜지 않는다. javadoc이 그것이 대부분의 JSON gadget chain의 기반이라고 적는다.

maxDocumentLengthmaxBytes와 같다는 점이 중요하다 — 인코딩 상한과 디코딩 파서 상한이 하나의 값에서 나온다. 따로 두면 둘이 갈라진다.

4.2 인코딩 — 스트리밍 경계

BoundedByteSink sink = BoundedByteSink.of(maxBytes, "PAYLOAD_TOO_LARGE");
try {
  mapper.writeValue(sink, payload);
} catch (JacksonException exception) {
  throw unwrapTooLarge(exception);
}

주석이 이유를 적는다 — "Jackson writes incrementally, so a payload whose serialized form is far larger than the limit stops at the limit instead of after the whole graph has been rendered into a buffer nobody bounded."

unwrapTooLarge가 필요한 이유도 명시돼 있다.

// :190-196
 * <p>Jackson wraps stream failures, so the size refusal would otherwise reach the caller as
 * {@code JSON_ENCODE_FAILED}  indistinguishable from a payload the mapper genuinely could not
 * render, and the two need different operator responses.

for (Throwable cause = exception; cause != null; cause = cause.getCause()) — 원인 사슬을 끝까지 훑어 MessageTooLargeException을 찾는다. 못 찾으면 MessageSerializationException("JSON_ENCODE_FAILED").

4.3 registry 조회 — 세 갈래 결과

private Class<?> requireRegistered(MessageType type, SchemaVersion version) {
  MessageContractKey key = new MessageContractKey(type, version);
  Class<?> registered = registry.get(key);
  if (registered != null) return registered;
  boolean typeIsKnown = registry.keySet().stream().anyMatch(known -> known.type().equals(type));
  if (typeIsKnown) {
    // Deliberately not falling back to another version's class: decoding v999 bytes with the v1
    // class is exactly the silent type confusion the version-keyed registry exists to stop.
    throw new MessageValidationException("SCHEMA_VERSION_NOT_REGISTERED", ...);
  }
  throw new MessageValidationException("UNKNOWN_MESSAGE_TYPE", ...);
}

SCHEMA_VERSION_NOT_REGISTERED 메시지에는 registeredVersions(type)가 정렬되어 포함된다. 테스트가 그 내용을 직접 단언한다 — hasMessageContaining("order.created v999").hasMessageContaining("[1, 2]")(JsonContractRegistryTest.java:58-61). 운영자가 "1과 2는 있고 999는 없다"를 에러 메시지만으로 알 수 있다.

4.4 인코딩·디코딩의 타입 검사 비대칭

방향 검사
encode registered.isInstance(payload)하위 타입 허용
decode registered.equals(payloadType)정확 일치 요구

비대칭이 합리적이다. 인코딩에서 OrderCreated의 하위 타입을 넘기면 Jackson이 등록된 형태로 직렬화한다. 디코딩에서 하위 타입을 허용하면 등록된 계약과 다른 클래스로 역직렬화되므로 정확 일치여야 한다. 다만 이 비대칭은 주석으로 설명되지 않았다 — §15의 추론 항목이다.

4.5 디코딩의 이중 상한

if (encoded.length > maxBytes) { throw new MessageTooLargeException("PAYLOAD_TOO_LARGE", ...); }
...
return mapper.readValue(encoded, payloadType);

명시 검사 하나(encoded.length)와 파서 내부 검사 하나(maxDocumentLength)가 겹친다. 중복이지만 둘의 실패 형태가 다르다 — 전자는 MessageTooLargeException, 후자는 JacksonExceptionMessageSerializationException. 명시 검사가 있어야 크기 초과가 크기 초과로 보고된다.

4.6 EncodedMessage에 붙는 schema reference

return new EncodedMessage(
    sink.toByteArray(), ContentType.JSON, Optional.of(SchemaReference.of(type.value(), version)));

subject가 message type 값이고 URI는 없다. 즉 이 codec은 외부 schema registry를 쓰지 않고 "타입 이름 + 버전"을 스키마 신원으로 삼는다. 테스트가 확인한다(JacksonMessageCodecTest.encodedMessageCarriesTheSchemaReference).


5. 주요 실행 경로

encode: requireRegistered(type, version) → payload가 등록 타입의 인스턴스인지 → BoundedByteSink 생성 → mapper.writeValue(sink, payload) → 실패 시 unwrapTooLargeEncodedMessage(bytes, JSON, SchemaReference)

decode: requireRegistered(type, version) → 요청 클래스가 등록 클래스와 정확히 같은지 → encoded.length 상한 → mapper.readValueJacksonException이면 JSON_DECODE_FAILED


6. 실패 경로와 복구/번역

코드 예외 조건 retryable
UNKNOWN_MESSAGE_TYPE MessageValidationException 타입 자체 미등록 false
SCHEMA_VERSION_NOT_REGISTERED MessageValidationException 타입은 알고 버전 미등록 false
PAYLOAD_TYPE_MISMATCH MessageValidationException encode: 인스턴스 아님 / decode: 클래스 불일치 false
PAYLOAD_TOO_LARGE MessageTooLargeException 인코딩 중 한도 초과 또는 디코딩 입력 초과 false
JSON_ENCODE_FAILED MessageSerializationException 그 외 Jackson 인코딩 실패 false
JSON_DECODE_FAILED MessageSerializationException 파싱 실패(깊이·중복키·trailing·미지 필드 포함) false

전부 retryable = false다 — PERMANENT_BUSINESSDESERIALIZATION 카테고리다. 같은 바이트를 다시 디코딩해도 같은 결과이므로 일관적이다.

진단 손실 하나. 파서 강화가 잡는 여섯 가지(깊이, 중복 키, trailing token, 미지 필드, 문서 길이, 토큰 길이)가 전부 하나의 코드 JSON_DECODE_FAILED로 접힌다. 운영자는 "JSON 디코딩 실패"만 보고 원인 여섯 갈래를 구분할 수 없다. 원인 예외가 cause로 붙지만 FailureDescriptorexceptionTypeOptional.empty()로 둔다(MessageSerializationException의 3인자 생성자 경로). §17 참조.


7. 트랜잭션·동시성·수명주기

트랜잭션 없음.

동시성: JacksonMessageCodec은 불변이다 — registryMap.copyOf, maxBytes는 int, mapper는 빌드 후 재구성되지 않는 Jackson ObjectMapper(스레드 안전). BoundedByteSink는 매 encode마다 새로 만들어지므로 공유되지 않는다.

PlatformOverheadPerformanceTest.aRoundTripDoesNotAllocateAGrowingRetainedSet이 codec이 메시지별 상태를 보유하지 않음을 간접 확인한다(메시지당 유지 메모리 64바이트 미만).


8. 설정·기능 플래그·환경 차이

설정 파일 없음. 상수:

상수 가시성
DEFAULT_MAX_BYTES 1,048,576 public — starter가 참조
MAX_NESTING_DEPTH 100 public
MAX_STRING_CHARACTERS 5,000,000 private
MAX_NUMBER_DIGITS 1,000 private

maxBytes는 생성자 인자로 재정의 가능하고 파서의 maxDocumentLength가 그 값을 따라간다.


9. 퍼시스턴스/외부 시스템 세부

없다.


10. 테스트 레인과 실제 증명 범위

레인: ./gradlew :messaging:messaging-schema-json:test. BUILD SUCCESSFUL, 18 tests, 0 skipped, 0 failures.

클래스 실제로 증명하는 것 증명하지 않는 것
JacksonMessageCodecTest 9 round trip, 1 MiB 초과 거절, 미등록 타입, payload 타입 불일치, trailing token, 미지 필드, 중복 키, 깊이 200 거절, schema reference 등록 registry가 실제 배포에서 채워지는지
JsonContractRegistryTest 6 버전별 클래스 분리, v999 거절 + 등록 버전 목록 노출, 클래스/버전 짝 검사, 타입 미등록과 버전 미등록 구분, 20 MiB payload가 1,024 상한에서 멈춤, 정확히 상한인 payload 허용
PlatformOverheadPerformanceTest 3 봉투 생성 < 20µs/건, JSON 인코딩 < 50µs/건, round trip 유지 메모리 < 64 B/건 실제 처리량. 의도적으로 브로커 없음

성능 테스트의 자기 규정이 명확하다.

// PlatformOverheadPerformanceTest.java:22-30
 * <p>This measures what the platform adds  identity, validation, encoding  and nothing else.
 * There is no broker in the loop, deliberately: broker throughput is a property of the deployment
 * and varies by an order of magnitude between a laptop and a cluster, so asserting on it produces a
 * test that fails for reasons nobody can act on.
 *
 * <p>The budgets are generous on purpose. The regression worth catching here is structural  an
 * accidental per-message reflection call, a defensive copy that became a deep copy, a validator
 * that started compiling a regex per invocation  and those cost orders of magnitude, not
 * percentages. A tight budget would instead catch a busy CI agent.

이것은 성능 테스트가 무엇을 잡으려는지 명시한 드문 예다 — 퍼센트가 아니라 자릿수 회귀. 다만 aRoundTripDoesNotAllocateAGrowingRetainedSetSystem.gc()totalMemory() - freeMemory()에 의존하므로 JVM이 GC 힌트를 무시하면 잡음이 낀다. 64 B/건이라는 여유가 그것을 흡수한다.

JsonContractRegistryTest의 클래스 javadoc이 이 codec에서 만난 두 결함을 기록한다 — 타입만으로 키를 잡았던 것과, 완성된 배열에 크기 제한을 적용했던 것.


11. 빌드/ArchUnit/CI 강제 지점

게이트 이 leaf에 대해
verifyCleanArchitectureDependencies ["messaging-core-api","messaging-schema-api"]
verifyRuntimeModuleMembership ["app-bootstrap"]
vendor api 규칙(src/messaging/CLAUDE.md:40-43) Jackson이 public 시그니처에 없으므로 implementation이 맞음 — 형제 leaf와 반대 판정
ArchUnit 전용 규칙 없음

12. 실제 사용 여부와 negative-space probes

원시 증거: evidence/raw/272-schema-family-reachability.txt.

12.1 Public surface reachability

JacksonMessageCodec의 leaf 밖 참조는 1개 파일이다 — messaging-spring-boot-starter/.../MessagingCoreAutoConfiguration.java.

이 하나가 messaging codec 전체에서 유일한 production 소비다. 형제 비교:

codec 소비자 registry membership
JacksonMessageCodec MessagingCoreAutoConfiguration ["app-bootstrap"]
AvroMessageCodec 없음 []
ProtobufMessageCodec 없음 []
RawBytesMessageCodec 없음 (schema-api 소속, ["app-bootstrap"])
DefaultCloudEventMapper 없음 ["app-bootstrap"]

Avro·Protobuf는 소비자 없음과 membership 없음이 일치한다 — 정합적인 incubating 상태다. RawBytesMessageCodec과 CloudEvents는 어긋난다(각 leaf 문서 참조).

12.2 Conditional sibling comparison

이 leaf에는 bean이 없다. 그러나 이 leaf가 조립되는 지점의 조건은 확인했다.

@ConditionalOnMissingBean(dev.caskeleton.messaging.schema.MessageCodecRegistry.class)

즉 애플리케이션이 자기 MessageCodecRegistry를 내놓으면 JSON codec 조립이 통째로 대체된다. 그 경우 PayloadPolicy가 참조하는 JacksonMessageCodec.DEFAULT_MAX_BYTES그대로 남는다 — 정책 상한만 JSON codec의 값을 유지한다. §17 참조.

12.3 Duplicate mechanism sweep

JSON 인코딩/디코딩을 하는 다른 지점이 저장소에 여럿 있다(web adapter의 응답 직렬화, redis codec, fileserver 저널, mongo cursor 등). 그러나 그들은 다른 책임(HTTP 응답, 캐시 봉투, 로컬 저널)이고 messaging 계약을 구현하지 않는다. runtime eligibility가 겹치지 않으므로 중복 경쟁으로 분류하지 않는다.

같은 messaging family 안에서 MessageCodec을 구현하는 것은 넷이고(JSON·Avro·Protobuf·raw) content type이 서로 달라 RegisteredMessageCodecs.register가 충돌을 거절한다. 책임 분리가 명확하다.

한 가지 실질 중복이 있다. 1 MiB payload 상한이 messaging family의 production 코드 다섯 곳에서 독립적으로 선언된다.

위치 가시성
messaging-policy/PayloadPolicy.DEFAULT_MAX_BYTES:17 public 1,048,576
messaging-schema-api/RawBytesMessageCodec.DEFAULT_MAX_BYTES:21 public 1,048,576
messaging-schema-json/JacksonMessageCodec.DEFAULT_MAX_BYTES:42 public 1,048,576
messaging-schema-avro/AvroMessageCodec.DEFAULT_MAX_BYTES:46 private 1,048,576
messaging-schema-protobuf/ProtobufMessageCodec.DEFAULT_MAX_BYTES:35 private 1,048,576

테스트에도 네 곳(ClaimCheckRetentionValidatorTest:47, DestinationProfileValidatorTest:225, RabbitContractHarness:40, InMemoryMessagingHarness:31)이 같은 리터럴을 갖는다.

schema-apiRawBytesMessageCodec javadoc은 이 값을 "The default encoded byte limit shared with the Stable codecs"라고 부르는데, 실제로는 공유되지 않고 복사돼 있다. 그리고 정책 쪽에 이미 주인이 있다messaging-policyPayloadPolicy.DEFAULT_MAX_BYTES가 public 상수로 존재한다. 그런데 starter는 그것을 쓰지 않고 JacksonMessageCodec.DEFAULT_MAX_BYTES를 참조한다(§2). 같은 값의 후보가 둘 있고 배선이 덜 적절한 쪽을 골랐다.

12.4 Documentation / measured-count drift

문서 주장 재측정 결과
RawBytesMessageCodec javadoc: 1 MiB가 "Stable codec들과 공유되는" 기본 상한 네 codec에 각자 리터럴 존재, 공유 상수 없음 표현 drift — 값은 일치, "shared"는 사실이 아님
JacksonMessageCodec javadoc: polymorphic default typing 미사용 strictMapperactivateDefaultTyping 호출 없음 일치
docs/messaging/support-matrix.md의 JSON Stable 등급 이 leaf가 유일하게 조립되는 codec인 것과 정합 일치

13. Git/설계 문서에서 확인한 변화와 실패 기록

JsonContractRegistryTest 클래스 javadoc이 이 codec에서 만난 두 결함을 남겼다.

// JsonContractRegistryTest.java:20-24
 * <p>Two defects met in this codec. The registry was keyed on message type alone, so a message
 * labelled v999 was decoded with the v1 class and kept its v999 label  the compatibility gate and
 * the audit record then both described a contract that was never registered. And the size limit was
 * applied to the finished byte array, which reports an oversized payload rather than preventing
 * one.

두 결함 다 messaging-schema-api가 소유하는 타입(MessageContractKey, BoundedByteSink)으로 고쳐졌다. 즉 이 leaf에서 발견된 문제가 상위 leaf의 타입을 만들어냈다.

MessagingCoreAutoConfiguration:420-427의 주석은 이 codec이 아니라 publisher 조립 결함(MSG-INT-003)을 기록하는데, 같은 configuration 안에 있으므로 조립 이력의 맥락으로 참조할 가치가 있다 — "no configuration produced one … the starter did not depend on that leaf."


14. 런타임·터미널 Evidence

id 종류 파일 무엇을 보여주는가 한계
EVD-272 command evidence/raw/272-schema-family-reachability.txt §D, §E codec별 소비자와 registry membership 정적 검색
EVD-275 command ./gradlew :messaging:messaging-schema-json:test --rerun-tasks BUILD SUCCESSFUL, 18 / 0 / 0 브로커 없음

15. 명시적 설계 이유와 추론을 구분한 정리

명시적

  • 기본 codec으로 안전한 이유 셋 — 클래스 javadoc
  • polymorphic default typing 금지 — 클래스 javadoc
  • 크기 초과를 로컬에서 잡아야 NOT_TRANSMITTED가 된다 — 클래스 javadoc
  • unwrapTooLarge가 필요한 이유 — 메서드 javadoc
  • 다른 버전 클래스로 폴백하지 않는 이유 — requireRegistered 주석
  • 성능 예산이 느슨한 이유 — PlatformOverheadPerformanceTest javadoc
  • 이 codec에서 만난 두 결함 — JsonContractRegistryTest javadoc

추론

  • encode는 isInstance, decode는 equals로 비대칭인 이유 → 추론. 방향별 안전성으로 설명되지만 주석이 없다.
  • 파서 실패 여섯 갈래가 한 코드로 접힌 것이 의도인지 → 미상.

16. 확인한 것 / 확인하지 못한 것

확인한 것

  • 226줄 전문의 계약과 파서 강화 설정 전수
  • 18개 테스트가 통과하고 무엇을 단언하는지
  • 이 codec이 유일하게 조립되는 codec이라는 것과 그 조립 코드의 정확한 형태
  • payload 정책 상한이 이 codec의 public 상수에서 파생된다는 것
  • 1 MiB 상한이 네 codec에 복사돼 있다는 것

확인하지 못한 것

  • 실제 배포에서 MessageContracts bean이 채워지는지. 채워지지 않으면 codec은 모든 메시지를 UNKNOWN_MESSAGE_TYPE으로 거절한다. 이 저장소에 MessageContracts production 구현이 있는지는 starter leaf가 소유한다.
  • Jackson 3의 StreamReadConstraints가 이 값들에서 실제로 어떻게 실패하는지 — 테스트는 깊이 200과 중복 키만 확인했고 maxNumberLength·maxStringLength는 검증하지 않았다.
  • 성능 예산이 실제 CI 하드웨어에서 얼마나 여유 있는지 — 이번 실행은 통과했으나 측정값을 남기지 않았다.

17. 손볼 것

P2 — 포맷 중립 payload 정책이, 자기 상수를 두고 JSON codec의 상수를 참조한다

  • 사실. MessagingCoreAutoConfiguration:410-413new PayloadPolicy(JacksonMessageCodec.DEFAULT_MAX_BYTES, JacksonMessageCodec.DEFAULT_MAX_BYTES / 2)를 만든다. 그런데 PayloadPolicy 자신이 같은 값의 public 상수 PayloadPolicy.DEFAULT_MAX_BYTES(messaging-policy/PayloadPolicy.java:17)를 갖고 있다.
  • 근거. 두 라인, 그리고 git grep -n '1_048_576' -- 'src/messaging/**/*.java'의 production 5건.
  • 왜 문제인가. MessagingAdmissionController는 목적지의 codec이 무엇이든 지나는 관문이다. 그 상한이 한 포맷 클래스의 상수에서 나오면 두 가지가 깨진다. (1) @ConditionalOnMissingBean이 허용하는 대로 애플리케이션이 자기 MessageCodecRegistry를 내놓아 JSON codec을 대체해도, 정책은 여전히 JSON codec의 값을 읽는다. (2) 다섯 곳의 리터럴 중 하나만 바뀌면 조용히 갈라지고, RawBytesMessageCodec javadoc이 이미 "shared with the Stable codecs"라고 사실과 다르게 부르고 있다. 정책 소유자가 이미 존재하는데 배선이 그것을 지나쳤다.
  • 확인 방법. git grep -n '1_048_576' -- 'src/messaging/**/*.java' · grep -n 'DEFAULT_MAX_BYTES' src/messaging/messaging-policy/src/main/java/dev/caskeleton/messaging/policy/PayloadPolicy.java
  • 후보. starter가 PayloadPolicy.DEFAULT_MAX_BYTES를 참조하게 바꾸고, 네 codec의 기본값도 그 상수(또는 설정 프로퍼티)에서 파생시킨다.
  • 다음 단계. CASE 후보. 조립 지점이 한 줄이고 재현이 정적이며, "값은 맞는데 출처가 틀렸다"는 형태가 명확하다.

P3 — 파서 방어 여섯 갈래가 하나의 실패 코드로 접힌다

  • 사실. 깊이 초과·중복 키·trailing token·미지 필드·문서 길이·토큰 길이가 전부 JSON_DECODE_FAILED가 된다.
  • 근거. decodecatch (JacksonException) 단일 분기(JacksonMessageCodec.java:155-158).
  • 왜 문제인가. 여섯 중 셋(중복 키, trailing token, 깊이)은 적대적 입력의 신호이고 나머지는 계약 불일치다. DLQ에 쌓인 메시지를 보는 운영자가 그 둘을 구분할 수 없다. FailureDescriptor.exceptionType도 비어 있다.
  • 확인 방법. JacksonMessageCodecTest의 네 케이스가 전부 같은 예외 타입을 기대하는 것으로 확인 가능.
  • 후보. JacksonException 하위 타입별로 코드를 나누거나, 최소한 exceptionType에 원인 클래스 단순명을 채운다.
  • 다음 단계. REFERENCE 후보(실패 코드는 운영자의 다음 행동이 갈리는 지점마다 나눈다).

P3 — 빈 registry로 조립되면 모든 메시지가 거절된다

  • 사실. contracts.getIfAvailable(MessageContracts::none)이 기본값이므로 MessageContracts bean이 없으면 빈 registry로 codec이 만들어진다.
  • 근거. MessagingCoreAutoConfiguration:362-365.
  • 왜 문제인가. 그 codec은 시작에 성공하고 첫 publish에서 UNKNOWN_MESSAGE_TYPE으로 실패한다. messaging-core-api 계열의 다른 leaf에서 관측된 것과 같은 형태다 — "시작은 하고 첫 쓰기에서 실패한다."
  • 확인 방법. MessageContracts production 구현의 존재 여부를 starter leaf에서 확인해야 한다.
  • 다음 단계. OPEN QUESTION 후보. 판정이 이 leaf 밖(messaging-spring-boot-starter)의 사실에 걸린다. 그 leaf SSOT가 답을 갖는다.

확인된 설계(문제 아님)

  • 파서 상한 여섯 가지와 polymorphic typing 금지
  • maxDocumentLength가 codec 상한과 같은 값에서 나오는 것
  • unwrapTooLarge가 원인 사슬을 훑어 크기 실패를 크기 실패로 보고하는 것
  • 미등록 버전 에러가 등록된 버전 목록을 포함하는 것
  • Jackson을 implementation으로 선언한 것(형제 leaf와 반대이고, 그것이 맞다)

Source anchors

id kind path revision what it proves limitations
MSJ-001 registry src/config/architecture/modules.json 21234e38 deps, memberships ["app-bootstrap"] 선언
MSJ-002 build messaging-schema-json/build.gradle same Jackson이 implementation
MSJ-003 code .../json/JacksonMessageCodec.java 전문 same §4 전체
MSJ-004 test JacksonMessageCodecTest (9) same 파서 방어와 registry 거절 브로커 없음
MSJ-005 test JsonContractRegistryTest (6) same 버전 키 동작, 20 MiB가 1 KiB 상한에서 멈춤
MSJ-006 test PlatformOverheadPerformanceTest (3) same 구조적 회귀 예산 처리량 아님. System.gc() 의존
MSJ-007 assembly messaging-spring-boot-starter/.../MessagingCoreAutoConfiguration.java:358-366, 408-417 same 유일한 codec 조립 지점, varargs 비어 있음, payload 정책의 상수 출처 해당 leaf SSOT가 소유
EVD-272 command evidence/raw/272-schema-family-reachability.txt same codec별 소비자와 membership 정적 검색
EVD-275 command ./gradlew :messaging:messaging-schema-json:test --rerun-tasks same 18 / 0 / 0