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>
292 lines
19 KiB
Markdown
292 lines
19 KiB
Markdown
# messaging-nats-experimental 완전 해부
|
|
|
|
> 상태: COMPLETE
|
|
> 재오픈 게이트: cycle 2 — `src/main` production 7파일 755줄 축자 통독 완료. test 2파일 460줄 축자 통독 완료. `STRUCTURAL_ONLY` 잔여 없음.
|
|
> 기준 revision: `21234e38cdb9a926cbc92bb97a2aee2e4a7d2916`
|
|
> 분석 범위: `src/messaging/messaging-nats-experimental`
|
|
> SSOT owner: `messaging-nats-experimental`
|
|
> integration/family document: `analysis/19-messaging-platform.md` (secondary, INTEGRATION_ONLY)
|
|
|
|
---
|
|
|
|
## 0. SSOT identity / 커버리지
|
|
|
|
- 선언 의존: messaging 계열 project 7 + vendor `jnats:2.26.2`
|
|
- `runtime_memberships`: **`[]`** — build-only · 등급 EXPERIMENTAL
|
|
|
|
| 파일 | LOC |
|
|
|---|---:|
|
|
| `NatsJetStreamTransport` | 295 |
|
|
| `NatsJetStreamProfile` | 103 |
|
|
| `NatsMaxDeliverParkingWorkflow` | 85 |
|
|
| `NatsJetStreamProfileValidator` · `NatsStreamPosition` | 75 · 75 |
|
|
| `NatsPreSendRejection` | 65 |
|
|
| `NatsAckMode` | 57 |
|
|
| **main 합계** | **755** |
|
|
| `NatsAdapterContractTest` · `NatsMaxDeliverParkingTest` | 337 · 123 |
|
|
|
|
### Coverage ledger
|
|
|
|
| scope | count | disposition | reason |
|
|
|---|---:|---|---|
|
|
| `main/java/**` | 7 | `FULL_READ` | 755줄 전 본문 |
|
|
| `test/java/**` | 2 | `FULL_READ` | 460줄 전 본문 · 테스트 28개 |
|
|
| `build.gradle` | 1 | `FULL_READ` | 전문 |
|
|
| `gradle.lockfile` | 1 | `STRUCTURAL_ONLY` | 잠금 파일 |
|
|
|
|
`UNCLASSIFIED` 0.
|
|
|
|
---
|
|
|
|
## 1. 이 어댑터의 판단 셋
|
|
|
|
**JetStream 만 쓴다.**
|
|
|
|
> "A core publish returns as soon as the bytes are written to the socket, with no persistence and no
|
|
> acknowledgement, so an adapter using it would report success for messages that were never stored —
|
|
> the failure is total and silent."
|
|
|
|
거부 코드는 `NatsJetStreamProfileValidator.validate` 에 있다 — 최소 한 번 배달 목적지에 코어 NATS 는 안 된다. 다만 그 검증기를 호출하는 곳이 저장소에 하나도 없다(§17.3). 이 절이 서술하는 것은 판단이 코드로 적혀 있다는 사실이지, 그 판단이 실행 경로에 걸려 있다는 사실이 아니다.
|
|
|
|
**확인은 지속 증거다.** 발행 승인이 메시지가 안착한 스트림과 순번을 이름 짓는다. 소켓에 바이트를 쓴 영수증이 아니다.
|
|
|
|
**기본 실패는 모호다.** 사전 거절 타입만 확실히 전송되지 않음으로 다루고 나머지는 전부 모호다.
|
|
|
|
> "a caller that reads `REJECTED` may republish under a new identity and duplicate a message the
|
|
> server already stored."
|
|
|
|
## 2. 죽은 편지가 없는 브로커에서 죽은 편지를 만든다
|
|
|
|
`NatsMaxDeliverParkingWorkflow` javadoc:
|
|
|
|
> "JetStream has no dead-letter queue. When a message hits `maxDeliver` the server terminates it: no
|
|
> redelivery, no routing, no record beyond an advisory. Every other broker in this platform parks a
|
|
> poison message somewhere an operator can find it, and this workflow is what makes NATS behave the
|
|
> same way."
|
|
|
|
핵심은 시점이다.
|
|
|
|
> "The parking therefore happens on the delivery **before** the limit, not on the limit itself.
|
|
> Acting at `maxDeliver` would mean acting on the delivery JetStream is about to discard, so any
|
|
> failure in the dead-letter publish would lose the message outright."
|
|
|
|
그래서 프로파일이 `maxDeliver < 2` 를 거부한다 — 플랫폼이 주차할 여유 배달이 최소 하나 있어야 한다.
|
|
|
|
그리고 정착은 죽은 편지 발행이 확인된 뒤에만 허용된다.
|
|
|
|
> "Terminating first would discard the message on a broker that cannot redeliver it, which is the
|
|
> one irreversible mistake available here."
|
|
|
|
세 번째 결과 `ALREADY_TERMINATED` 는 살아 있는 소비자 아래에서 프로파일이 바뀐 경우에만 도달한다. 회복할 것이 없고, 재배달로 오인되지 않도록 결과로 남긴다.
|
|
|
|
## 3. 능력 선언
|
|
|
|
```java
|
|
CAPABILITIES = (true, true, true, true, true, false, true, false, false, true, false, true);
|
|
```
|
|
|
|
`nativeDeadLetter=false` 의 근거가 클래스 javadoc 과 검증기 javadoc 양쪽에 있다 — 없는 큐를 찾아 나서게 만들지 않는다.
|
|
|
|
`keyedOrdering=false` 도 검증기가 강제한다 — 키 순서를 요구하는 목적지를 거부한다.
|
|
|
|
`deduplicatedPublish=true` 는 §17.1 이 다룬다.
|
|
|
|
## 4. 프로파일이 스스로 거부하는 것
|
|
|
|
`NatsJetStreamProfile` 은 record 이고, 압축 생성자가 이 어댑터의 불변식을 전부 들고 있다. 검증기가 호출되지 않는 지금, **실제로 실행되는 유일한 게이트가 여기다.**
|
|
|
|
```java
|
|
if (!ackMode.supportsAtLeastOnce()) throw …; // NONE · ALL 거부
|
|
if (ackWait.isNegative() || ackWait.isZero()) throw …;
|
|
if (maxDeliver < 2) throw …; // "headroom"
|
|
if (deduplicationWindow.isPresent() && …isZero()) throw …; // 설정했으면 양수
|
|
```
|
|
|
|
`ackMode` 거부 사유는 `NatsAckMode` 자신이 문장으로 들고 있고(`rejectionReason()`), 프로파일이 그 문장을 예외 메시지에 그대로 싣는다. `NONE` 은 "forgotten", `ALL` 은 "still in flight" — 테스트가 그 두 낱말로 각각 걸어 잠근다.
|
|
|
|
`PARKING_HEADROOM = 1` 상수와 `parkAtDelivery() = maxDeliver - PARKING_HEADROOM` 가 §2 의 시점 선택을 숫자로 못 박는다. `NatsMaxDeliverParkingWorkflow.parkingThreshold()` 는 이 값을 그대로 위임한다 — 임계값의 정의가 한 곳에만 있다.
|
|
|
|
**주의할 비대칭.** 편의 팩토리 `durable(subject, stream, durableName)` 는 중복 제거 창을 `Optional.of(2분)` 으로 채운다. 즉 팩토리를 거친 프로파일은 §17.1 의 구멍에 빠지지 않는다. 그러나 팩토리에도 호출자가 없고(저장소 전역 grep 0건), 정규 생성자는 빈 창을 정상값으로 받는다. 기본 경로가 안전하다는 사실이 그 구멍을 닫아 주지 않는다.
|
|
|
|
## 10. 테스트 레인
|
|
|
|
두 테스트 460줄 · 28개.
|
|
|
|
`NatsAdapterContractTest` 17개 — 지속 증거(`REPLICATION_OR_PERSISTENCE_ACK`), 위치 반환, 시간 초과의 모호 판정, 사전 거절만이 `NOT_TRANSMITTED` 라는 것, 감싸인 미지 실패의 모호 판정, 호출자 마감의 유효성, 중복 제거 식별자 유무, 초과 페이로드 거절, 능력 두 개, 닫힌 전송, 재배달 인식, 순번 하한, 실패 범주.
|
|
|
|
전송은 `(subject, deduplicationId, request) -> CompletionStage<NatsStreamPosition>` 람다로 주입된다. 실제 JetStream 클라이언트는 이 리프에 없고, 테스트가 성공·실패·영영 안 끝남을 직접 만든다.
|
|
|
|
두 테스트가 회귀를 이름으로 기록한다 — `aFailureNamedLikeAKnownOneIsStillAmbiguous` 는 "예외 클래스 이름이 분류자였던" 과거를, `aPublishThatNeverCompletesIsBoundedByTheCallersTimeout` 은 "호출자 마감이 아예 무시되던" 과거를 주석에 남긴다. 셋째 회귀 기록은 어셈블이 비어 있다(§17.4).
|
|
|
|
`NatsMaxDeliverParkingTest` 11개 — 한계 직전 주차, 한계 자체도 주차, 한계 초과의 `ALREADY_TERMINATED`, 확인 뒤 정착, `maxDeliver=1` 거부, 배달 수 하한, 임계값, `ackMode` 세 값.
|
|
|
|
`NatsJetStreamProfileValidator` 를 세우는 테스트는 없다.
|
|
|
|
## 12. negative-space probes
|
|
|
|
**12.1 도달성.** `dev.caskeleton.messaging.nats` 를 import 하는 코드가 리프 밖에 없다. 리프 밖에서 이 모듈이 등장하는 곳은 세 군데인데 전부 **이름 문자열**이다 — `config/architecture/modules.json` 의 등록, `messaging-testkit/CompatibilityMatrix` 의 `("messaging-nats-experimental", List.of("2.14"), Tier.EXPERIMENTAL, false, false)` 항목, 그리고 그 표를 문서와 대조하는 `MessagingDocumentationContractTest`. 즉 등급표가 이 어댑터를 알고 있을 뿐, 어떤 실행 경로도 이 클래스들에 닿지 않는다. build-only · experimental 표기 그대로다.
|
|
|
|
**12.2 대조군 — 자매 실험 어댑터.** `messaging-pulsar-experimental` 과 구조가 같다 — 주입되는 전송 연산, 타입 있는 사전 거절, 기본 모호, 실험 등급 게이트. 차이는 능력 선언의 출처다. Pulsar 는 전송과 검증기가 서로 다른 값을 답하고(그쪽 §17.1), NATS 는 두 곳이 같은 값을 답한다.
|
|
|
|
다만 그 일치는 공유가 아니라 **복사**다. `NatsJetStreamTransport.CAPABILITIES` 상수와 `NatsJetStreamProfileValidator.capabilities()` 가 열두 개 불리언 리터럴을 각자 손으로 적어 두었고, 둘을 묶는 것은 아무것도 없다. 오늘 같은 값인 것이 내일도 같으리라는 보장은 코드에 없다 — Pulsar 가 이미 그 갈라짐의 실물이다.
|
|
|
|
이쪽의 문제는 따로 있다. 그 값이 프로파일에서 파생되지 않는다는 것이다(§17.1).
|
|
|
|
**12.4 드리프트.** 실험 등급 표기와 코드가 일치한다.
|
|
|
|
## 16. 확인하지 못한 것
|
|
|
|
- 실제 JetStream 서버를 띄우지 않았다. 클라이언트 브리지를 싣지 않는 리프다.
|
|
- 중복 제거 창이 없는 프로파일로 모호 재발행을 재현하지 않았다. 능력 상수와 `deduplicationId` 구현으로 판정했다.
|
|
- 검증기를 부르는 조립 지점이 다른 형태(설정 클래스 · 스타터)로 어딘가에 있을 가능성은 클래스 이름 · 패키지 이름 두 가지 grep 으로만 배제했다. 리플렉션이나 문자열 기반 조립이라면 잡히지 않는다.
|
|
- 테스트를 실행하지 않았다. §17.4 의 "항상 통과"는 어셈블 의미론으로 판정한 것이다.
|
|
|
|
## 17. 손볼 것
|
|
|
|
### 17.1 P2 — `deduplicatedPublish` 를 무조건 참으로 선언하는데 실제 중복 제거는 프로파일에 창이 있을 때만 일어난다
|
|
|
|
능력은 상수다.
|
|
|
|
```java
|
|
private static final MessagingCapabilities CAPABILITIES =
|
|
new MessagingCapabilities(true, true, true, true, true, false, true, false, false, true, false, true);
|
|
// ^^^^ deduplicatedPublish
|
|
```
|
|
|
|
검증기의 `capabilities()` 도 같은 값을 돌려준다.
|
|
|
|
그런데 중복 제거 식별자는 프로파일에 창이 있을 때만 만들어진다.
|
|
|
|
```java
|
|
private Optional<String> deduplicationId(TransportPublishRequest request) {
|
|
return profile.deduplicationWindow().map(window -> request.envelope().messageId().value().toString());
|
|
}
|
|
```
|
|
|
|
`NatsJetStreamProfile.deduplicationWindow` 는 `Optional<Duration>` 이고, 비어 있는 것이 정상 상태다 — 프로파일 생성자도 검증기도 창을 요구하지 않는다. 창이 없으면 `Nats-Msg-Id` 가 실리지 않고 서버는 중복을 제거하지 않는다.
|
|
|
|
즉 능력 선언이 프로파일과 무관하게 참이다.
|
|
|
|
**왜 이 플래그인가.** 이 저장소에서 능력 열두 개 중 부재가 예외를 만드는 유일한 것이 `deduplicatedPublish` 다(`DefaultMessagePublisher:250`). 나머지는 읽히지 않거나 분기에 쓰인다. 그러므로 이 플래그의 과대 선언은 다른 어느 플래그의 과대 선언보다 직접적이다 — 창 없는 목적지가 그 가드를 통과한다.
|
|
|
|
**그리고 어댑터 자신이 그 조건을 알고 있다.** 클래스 javadoc:
|
|
|
|
> "A publish that times out is `AMBIGUOUS`: JetStream may have stored it and lost only the
|
|
> acknowledgement, and **the deduplication window is what makes retrying it safe when the profile
|
|
> enables one.**"
|
|
|
|
"when the profile enables one" 이 정확히 능력이 담지 않은 조건이다. 창이 없는 목적지에서 모호를 재시도하면 스트림에 같은 메시지가 두 번 들어간다.
|
|
|
|
`MessagingCapabilities` 의 클래스 javadoc 이 이 상황을 미리 서술한다 — "a silently weakened guarantee is indistinguishable from a working one until the incident."
|
|
|
|
**테스트가 두 쪽을 동시에 못 박는다.** `NatsAdapterContractTest` 안에서, 같은 빈 창 프로파일(`confirming(Optional.empty())`)에 대해:
|
|
|
|
```java
|
|
void theAdapterAdvertisesDeduplicatedPublish() {
|
|
assertThat(confirming(Optional.empty()).capabilities(…).capabilities()
|
|
.deduplicatedPublish()).isTrue(); // 능력은 참이라고 한다
|
|
}
|
|
|
|
void noDeduplicationWindowSendsNoDeduplicationId() {
|
|
confirming(Optional.empty()).publish(request(64))…;
|
|
assertThat(capturedDeduplicationIds).singleElement()
|
|
.satisfies(id -> assertThat(id).isEmpty()); // 선에는 아무것도 안 실린다
|
|
}
|
|
```
|
|
|
|
둘 다 통과한다. 모순이 우연히 남은 것이 아니라 **테스트로 고정되어** 있다는 뜻이고, 수정할 때 함께 고쳐야 할 지점이 어디인지도 이 두 개가 알려 준다.
|
|
|
|
**팩토리는 이 구멍을 메우지 않는다.** `NatsJetStreamProfile.durable(...)` 는 창을 2분으로 채워 주지만 호출자가 없고, 정규 생성자는 빈 창을 정상값으로 받는다(§4).
|
|
|
|
**수정.** 능력을 프로파일에서 파생시킨다.
|
|
|
|
```java
|
|
new MessagingCapabilities(…, profile.deduplicationWindow().isPresent(), …)
|
|
```
|
|
|
|
또는 검증기가 최소 한 번 배달 목적지에 중복 제거 창을 요구한다. 후자는 코어 NATS 거부와 같은 형태의 시작 시점 거부다.
|
|
|
|
### 17.2 P3 — 닫힌 전송의 거절이 영구 업무 실패로 분류된다
|
|
|
|
`rejectedLocally` 가 `FailureCategory.PERMANENT_BUSINESS` 를 고정으로 쓰고, 두 호출자 중 하나가 `NATS_TRANSPORT_CLOSED` 다.
|
|
|
|
자매 어댑터(Pulsar)와 같은 형태이고 같은 판단이다 — 종료 중이라는 것은 이 세대의 사정이지 업무의 영구 실패가 아니다. 같은 파일의 `classify` 는 범주를 신중히 나눈다.
|
|
|
|
두 리프가 같은 형태를 공유하므로 수정도 함께 하는 편이 낫다.
|
|
|
|
### 17.3 P2 — `NatsJetStreamProfileValidator` 를 호출하는 곳이 저장소에 없다. javadoc 링크 하나가 유일한 흔적이다
|
|
|
|
75줄짜리 검증기가 이 어댑터의 시작 시점 판단 넷을 들고 있다 — 실험 스위치가 꺼져 있으면 거부, 최소 한 번 배달에 코어 NATS 거부, 순서 있는 소비자와 경쟁 작업자 동시 사용 거부, 키 순서 목적지 거부.
|
|
|
|
저장소 전역에서 이 클래스 이름이 나오는 곳은 두 줄뿐이다.
|
|
|
|
```
|
|
NatsJetStreamTransport.java:35: * is why {@link NatsJetStreamProfileValidator} refuses the combination at startup.
|
|
NatsJetStreamProfileValidator.java:21: public final class NatsJetStreamProfileValidator {
|
|
```
|
|
|
|
하나는 선언이고 하나는 **javadoc 링크**다. 코드 호출자 0, 테스트 0.
|
|
|
|
`validate` 는 `jetStreamEnabled` · `orderedConsumer` · `competingWorkers` · `enabled` 를 전부 인자로 받는다. 즉 스스로 아무것도 관찰하지 않고, 호출자가 이미 알고 있는 사실을 넘겨 줘야만 판단한다. 그런 호출자가 없으니 이 판단들은 한 번도 실행된 적이 없다.
|
|
|
|
**왜 P2 인가.** 전송의 클래스 javadoc 이 "그래서 검증기가 시작 시 그 조합을 거부한다"고 단언한다. 읽는 사람에게 이 어댑터는 코어 NATS 오설정으로부터 보호되는 것처럼 보이는데, 실제로는 아무 게이트도 걸려 있지 않다. 실험 등급이라 지금 당장 사고가 나지는 않지만, 이 어댑터를 실전에 붙이는 사람이 가장 먼저 신뢰할 문장이 지금 사실이 아니다.
|
|
|
|
같은 형태를 이 저장소에서 여러 번 봤다 — 채점기는 있는데 그 채점기에 값을 넣어 주는 생산자가 없는 구조(`GrpcRawApiImportRule` · `GrpcApplicationBoundaryRules` · `GrpcNettyParityContract` 등). 이쪽이 더 나쁜 쪽인 이유는 그 리프들에서는 최소한 테스트가 리터럴을 먹여 판단 자체는 실행해 보는데, 여기서는 그것조차 없다는 점이다.
|
|
|
|
**수정.** 어댑터 조립 지점에서 `validate` 를 부르거나, 그럴 지점이 아직 없다면 최소한 프로파일 생성 시점에 걸리도록 옮긴다(§4 의 압축 생성자가 이미 실행되는 유일한 게이트다). 어느 쪽도 못 하겠다면 전송 javadoc 의 "refuses ... at startup" 을 사실에 맞게 고친다.
|
|
|
|
### 17.4 P3 — 경과 시간 회귀를 막으려는 어셈블이 항상 참이다
|
|
|
|
```java
|
|
@Test
|
|
void theReportedElapsedTimeIsMeasuredRatherThanZero() {
|
|
PublishResult result = await(failingWith(new TimeoutException("no ack")).publish(request(64)));
|
|
|
|
assertThat(result.elapsed())
|
|
.as("every outcome reported Duration.ZERO, so latency evidence was fiction")
|
|
.isGreaterThanOrEqualTo(Duration.ZERO);
|
|
}
|
|
```
|
|
|
|
`as(...)` 가 막으려는 회귀는 "모든 결과가 `Duration.ZERO` 를 보고하던 것"이다. 그런데 어셈블은 `>= Duration.ZERO` 다. `Duration.ZERO` 는 이 조건을 통과한다. 경과 시간은 시작 시점에서 잰 값이라 음수가 될 수 없으므로, 이 어셈블은 **구현이 무엇을 하든 통과한다.**
|
|
|
|
이름과 `as` 메시지가 정확히 짚은 회귀를, 어셈블만 못 잡는다. 그래서 이 테스트는 회귀 방지가 아니라 회귀 방지의 표시다.
|
|
|
|
**수정.** `isGreaterThan(Duration.ZERO)` 로 바꾼다. 시간 분해능이 불안하면 전송 람다에 관측 가능한 지연을 넣고 그 하한과 비교한다 — 같은 클래스의 `aPublishThatNeverCompletesIsBoundedByTheCallersTimeout` 가 이미 50밀리초 마감으로 그 방식을 쓴다.
|
|
|
|
### 확인된 설계(문제 아님)
|
|
|
|
- **코어 NATS 를 최소 한 번 배달에 쓰지 못하게 시작 시 거부한 것과 그 근거.**
|
|
- **확인을 지속 증거로 기록한 것** — 스트림과 순번을 이름 짓는 승인이다.
|
|
- **알 수 없는 실패의 기본값을 모호로 둔 것.**
|
|
- **한계 직전 배달에서 주차하는 것과 그 시점 선택의 근거.**
|
|
- **`maxDeliver < 2` 를 거부해 주차 여유를 강제한 것.**
|
|
- **죽은 편지 발행이 확인된 뒤에만 원본을 정착시키는 것.**
|
|
- **`ALREADY_TERMINATED` 를 별도 결과로 남겨 재배달과 구분한 것.**
|
|
- **`nativeDeadLetter=false` 를 선언하고 그 이유를 두 곳에 적은 것.**
|
|
- **순서 있는 소비자와 경쟁 작업자의 배타성을 검증기가 강제한 것.**
|
|
- **중복 제거 식별자로 논리 메시지 식별자를 쓰는 것** — 시도마다 새 식별자를 만들면 창이 필요한 상황에서 쓸모가 없어진다.
|
|
- **예외 클래스 이름으로 실패를 분류하던 것을 걷어내고 타입으로 옮긴 것** — 테스트가 그 회귀를 주석으로 남겨 두었다.
|
|
- **주차 임계값의 정의를 프로파일 한 곳에만 둔 것** — 워크플로는 `parkAtDelivery()` 를 위임만 한다.
|
|
- **`NatsStreamPosition` 이 스트림 순번과 소비자 순번을 따로 들고 있는 것** — 재배달 인식이 둘의 차이에서 나오고, 재생은 스트림 순번으로만 되돌아간다.
|
|
|
|
---
|
|
|
|
## Source anchors
|
|
|
|
```
|
|
src/messaging/messaging-nats-experimental/build.gradle
|
|
main/java/…/nats/NatsJetStreamTransport.java:1-295
|
|
main/java/…/nats/NatsJetStreamProfile.java:1-103
|
|
main/java/…/nats/NatsMaxDeliverParkingWorkflow.java:1-85
|
|
main/java/…/nats/NatsJetStreamProfileValidator.java:1-75
|
|
main/java/…/nats/NatsStreamPosition.java:1-75
|
|
main/java/…/nats/NatsPreSendRejection.java:1-65
|
|
main/java/…/nats/NatsAckMode.java:1-57
|
|
test/java/…/nats/NatsAdapterContractTest.java:1-337
|
|
test/java/…/nats/NatsMaxDeliverParkingTest.java:1-123
|
|
src/messaging/messaging-core-api/…/destination/MessagingCapabilities.java (성분 의미)
|
|
src/messaging/messaging-testkit/…/CompatibilityMatrix.java:107-109 (등급표의 이름 항목)
|
|
src/config/architecture/modules.json (등록)
|
|
```
|