refactor: 각 어댑터터별 리펙토링 진행
This commit is contained in:
@@ -7,60 +7,76 @@
|
||||
> either of the old prefixes now fails startup with a message naming the key — see
|
||||
> `MessagingPrefixMigrationValidator`.
|
||||
|
||||
> **이 페이지는 실행된다.** 아래 YAML 블록은 `MessagingConfigurationBindingTest`가 이 파일에서 직접
|
||||
> 읽어 컨텍스트에 올린다. 문서가 설명하는 모양이 곧 바인딩되는 모양이라는 뜻이고, 문서를 고치면서
|
||||
> 코드를 고치지 않으면 테스트가 깨진다. 이전 판은 destination·broker·security 세 섹션을 설명했지만
|
||||
> 어떤 binder도 그것을 읽지 않았다 — 문서대로 설정한 배포는 아무것도 바뀌지 않았고 아무 말도 듣지
|
||||
> 못했다 (MSG-008).
|
||||
|
||||
## Destination profile
|
||||
|
||||
```yaml
|
||||
app:
|
||||
messaging:
|
||||
destinations:
|
||||
order-events:
|
||||
broker: kafka-primary
|
||||
kind: EVENT_STREAM # ASYNC_COMMAND | DOMAIN_EVENT | INTEGRATION_EVENT
|
||||
destinations:
|
||||
order-events:
|
||||
broker: kafka-primary
|
||||
kind: EVENT_STREAM # ASYNC_COMMAND | DOMAIN_EVENT | INTEGRATION_EVENT
|
||||
# | WORK_QUEUE | PUBLISH_SUBSCRIBE | EVENT_STREAM | REQUEST_REPLY
|
||||
tier: M1 # M1 | M2 | M3
|
||||
physical:
|
||||
topic: order.events.v1
|
||||
schema:
|
||||
codec: application/json
|
||||
compatibility: BACKWARD_TRANSITIVE
|
||||
message-types: [order.created]
|
||||
guarantees:
|
||||
delivery: AT_LEAST_ONCE # AT_MOST_ONCE | AT_LEAST_ONCE
|
||||
ordering: KEY # NONE | DESTINATION | PARTITION | KEY
|
||||
external-side-effect: INBOX_TRANSACTIONAL
|
||||
producer:
|
||||
confirmation: REPLICATION_OR_PERSISTENCE_ACK
|
||||
timeout: 5s
|
||||
mandatory-routing: true
|
||||
idempotent: true
|
||||
consumer:
|
||||
group: order-projection
|
||||
concurrency: 6
|
||||
max-in-flight-per-ordering-unit: 1
|
||||
prefetch: 16
|
||||
handler-timeout: 30s
|
||||
manual-settlement: false
|
||||
retry:
|
||||
mode: PAUSE_PARTITION # NONE | INLINE | BLOCKING | PAUSE_PARTITION
|
||||
tier: M1 # M1 | M2 | M3
|
||||
physical:
|
||||
topic: order.events.v1
|
||||
schema:
|
||||
codec: application/json
|
||||
compatibility: BACKWARD_TRANSITIVE
|
||||
message-types: [order.created]
|
||||
guarantees:
|
||||
delivery: AT_LEAST_ONCE # AT_MOST_ONCE | AT_LEAST_ONCE
|
||||
ordering: KEY # NONE | DESTINATION | PARTITION | KEY
|
||||
external-side-effect: INBOX_TRANSACTIONAL
|
||||
producer:
|
||||
confirmation: REPLICATION_OR_PERSISTENCE_ACK
|
||||
timeout: 5s
|
||||
mandatory-routing: true
|
||||
idempotent: true
|
||||
consumer:
|
||||
group: order-projection
|
||||
concurrency: 1 # DESTINATION 순서를 요구하면 1이어야 한다
|
||||
max-in-flight-per-ordering-unit: 1
|
||||
prefetch: 16
|
||||
handler-timeout: 30s
|
||||
manual-settlement: false
|
||||
retry:
|
||||
mode: PAUSE_PARTITION # NONE | INLINE | BLOCKING | PAUSE_PARTITION
|
||||
# | RETRY_DESTINATION | BROKER_DELAYED
|
||||
max-attempts: 3
|
||||
initial-delay: 200ms
|
||||
max-delay: 2s
|
||||
multiplier: 2.0
|
||||
jitter: true
|
||||
ordering-impact: PRESERVE # PRESERVE | ALLOW_REORDER
|
||||
dlq:
|
||||
destination: order-events-dlq
|
||||
max-redrive-count: 1
|
||||
payload:
|
||||
max-bytes: 1048576
|
||||
claim-check-threshold-bytes: 1048576
|
||||
key-resolver-configured: true
|
||||
production: true
|
||||
topology-auto-create: false
|
||||
max-attempts: 3
|
||||
initial-delay: 200ms
|
||||
max-delay: 2s
|
||||
multiplier: 2.0
|
||||
jitter: true
|
||||
ordering-impact: PRESERVE # PRESERVE | ALLOW_REORDER
|
||||
dlq:
|
||||
destination: order-events-dlq
|
||||
max-redrive-count: 1
|
||||
payload:
|
||||
max-bytes: 1048576
|
||||
claim-check-threshold-bytes: 1048576
|
||||
key-resolver-configured: true
|
||||
production: false
|
||||
topology-auto-create: false
|
||||
order-events-dlq:
|
||||
broker: kafka-primary
|
||||
kind: WORK_QUEUE
|
||||
physical:
|
||||
topic: order.events.v1.dlt
|
||||
schema:
|
||||
message-types: [order.created]
|
||||
```
|
||||
|
||||
`dlq.destination`이 가리키는 destination도 선언되어야 한다. 선언되지 않은 이름은 부팅 실패이며,
|
||||
메시지가 갈 곳 없는 DLQ 설정이 조용히 통과하지 않는다. `retry.destination`과 `dlq.destination`이
|
||||
섞여 만드는 순환(A의 retry가 B로, B의 dlq가 A로)도 하나의 그래프로 검사되어 경로와 함께 거절된다.
|
||||
|
||||
## 기본값
|
||||
|
||||
| 설정 | 기본값 | 근거 |
|
||||
@@ -81,26 +97,36 @@ app:
|
||||
| Outbox polling | 500ms | |
|
||||
| metric dimension 상한 | 200 | cardinality 폭발 방지 |
|
||||
|
||||
`schema.codec`은 `application/json`, `schema.compatibility`는 `BACKWARD_TRANSITIVE`,
|
||||
`guarantees.delivery`는 `AT_LEAST_ONCE`, `retry.mode`는 `NONE`이 기본값이다. 자동 retry가 기본으로
|
||||
꺼져 있는 이유는 순서를 흐트러뜨리거나 비멱등 side effect를 두 번 실행하는 retry가 눈에 보이는
|
||||
실패보다 나쁘기 때문이다.
|
||||
|
||||
## Broker profile
|
||||
|
||||
브로커는 `app.messaging.brokers` 아래에 한 번만 기술한다. `type`이 어느 계열의 설정이 적용되는지
|
||||
결정하며, 다른 계열의 키(Kafka 항목의 `prefetch` 같은)는 무시되지 않고 부팅 실패로 거절된다 —
|
||||
무시하면 그 줄을 쓴 사람은 무언가가 적용됐다고 믿게 된다.
|
||||
|
||||
### Kafka
|
||||
|
||||
```yaml
|
||||
app:
|
||||
messaging:
|
||||
brokers:
|
||||
kafka-primary:
|
||||
type: kafka
|
||||
stable: true
|
||||
production: true
|
||||
bootstrap-servers: [broker-1:9093, broker-2:9093]
|
||||
enable-idempotence: true # stable에서 필수
|
||||
acks: all # stable에서 필수
|
||||
max-in-flight-requests-per-connection: 5 # 최대 5
|
||||
delivery-timeout: 30s
|
||||
enable-auto-commit: false # 항상 금지
|
||||
tls-enabled: true # production 필수
|
||||
authentication-enabled: true # production 필수
|
||||
brokers:
|
||||
kafka-primary:
|
||||
type: kafka
|
||||
stable: true
|
||||
production: false
|
||||
bootstrap-servers: [broker-1:9093, broker-2:9093]
|
||||
enable-idempotence: true # stable에서 필수
|
||||
acks: all # stable에서 필수
|
||||
max-in-flight-requests-per-connection: 5 # 최대 5
|
||||
delivery-timeout: 30s
|
||||
enable-auto-commit: false # 항상 금지
|
||||
consumer-group: order-projection
|
||||
tls-enabled: false # production이면 필수
|
||||
authentication-enabled: false # production이면 필수
|
||||
```
|
||||
|
||||
### RabbitMQ
|
||||
@@ -108,40 +134,52 @@ app:
|
||||
```yaml
|
||||
app:
|
||||
messaging:
|
||||
brokers:
|
||||
rabbit-primary:
|
||||
type: rabbitmq
|
||||
stable: true
|
||||
production: true
|
||||
addresses: [rabbit-1:5671]
|
||||
publisher-confirms: true # stable에서 필수
|
||||
publisher-returns: true # stable에서 필수
|
||||
mandatory: true # stable에서 필수
|
||||
confirm-timeout: 5s
|
||||
auto-ack: false # 항상 금지
|
||||
prefetch: 16
|
||||
quorum-queues: true # durable work queue 필수
|
||||
tls-enabled: true
|
||||
authentication-enabled: true
|
||||
brokers:
|
||||
rabbit-primary:
|
||||
type: rabbitmq
|
||||
stable: true
|
||||
production: false
|
||||
addresses: [rabbit-1:5671]
|
||||
publisher-confirms: true # stable에서 필수
|
||||
publisher-returns: true # stable에서 필수
|
||||
mandatory: true # stable에서 필수
|
||||
confirm-timeout: 5s
|
||||
auto-ack: false # 항상 금지
|
||||
prefetch: 16
|
||||
quorum-queues: true # durable work queue 필수
|
||||
tls-enabled: false
|
||||
authentication-enabled: false
|
||||
```
|
||||
|
||||
`production: true`인 브로커는 `tls-enabled`와 `authentication-enabled`가 모두 참이어야 하고,
|
||||
그렇지 않으면 `KafkaProfileValidator` / `RabbitProfileValidator`가 부팅을 거절한다. 위 예시가
|
||||
`production: false`인 것은 이 페이지가 그대로 실행되는 fixture이기 때문이며, 실 배포는 셋 다 참이다.
|
||||
|
||||
## 보안
|
||||
|
||||
```yaml
|
||||
app:
|
||||
messaging:
|
||||
security:
|
||||
kafka-primary:
|
||||
producer: { type: SASL_SCRAM, credential-id: kafka-producer }
|
||||
consumer: { type: SASL_SCRAM, credential-id: kafka-consumer }
|
||||
# admin은 application runtime에 설정하지 않는다
|
||||
hostname-verification: true
|
||||
access:
|
||||
publishable: [order-events]
|
||||
consumable: []
|
||||
administrable: []
|
||||
security:
|
||||
kafka-primary:
|
||||
producer: { type: SASL_SCRAM, credential-id: kafka-producer }
|
||||
consumer: { type: SASL_SCRAM, credential-id: kafka-consumer }
|
||||
# admin은 application runtime에 설정하지 않는다
|
||||
hostname-verification: true
|
||||
access:
|
||||
publishable: [order-events]
|
||||
consumable: []
|
||||
administrable: []
|
||||
```
|
||||
|
||||
키는 `app.messaging.brokers`에 선언된 브로커 이름과 같아야 한다. `tls-enabled`와 `production`은
|
||||
브로커 쪽에만 있고 여기에 중복되지 않는다 — 하나의 브로커가 두 곳에서 기술되면 두 값이 어긋나는
|
||||
날이 오고, 어느 쪽이 이기는지는 아무도 모른다.
|
||||
|
||||
`credential-id`는 이름일 뿐이고 자격 증명 자체가 아니다. 실제 재료는 `CredentialProvider`가
|
||||
연결 시점에 해석하므로, 설정 덤프나 힙 덤프에서 나오는 것은 이름뿐이다. producer와 consumer는
|
||||
서로 다른 `credential-id`를 써야 하며, 같으면 부팅에 실패한다.
|
||||
|
||||
## Experimental / Optional
|
||||
|
||||
기본값은 전부 `false`다.
|
||||
@@ -149,12 +187,12 @@ app:
|
||||
```yaml
|
||||
app:
|
||||
messaging:
|
||||
experimental:
|
||||
kafka-share: false
|
||||
pulsar: false
|
||||
nats: false
|
||||
bridge:
|
||||
spring-cloud-stream: false
|
||||
experimental:
|
||||
kafka-share: false
|
||||
pulsar: false
|
||||
nats: false
|
||||
bridge:
|
||||
spring-cloud-stream: false
|
||||
```
|
||||
|
||||
## Backpressure
|
||||
@@ -162,9 +200,24 @@ app:
|
||||
```yaml
|
||||
app:
|
||||
messaging:
|
||||
backpressure:
|
||||
global-limit: 512
|
||||
per-destination-limit: 64 # global-limit 이하여야 한다
|
||||
backpressure:
|
||||
global-limit: 512
|
||||
per-destination-limit: 64 # global-limit 이하여야 한다
|
||||
```
|
||||
|
||||
`per-destination-limit > global-limit`이면 global limit이 limit이 아니게 되므로 부팅에 실패한다.
|
||||
|
||||
## 바인딩되지 않는 키
|
||||
|
||||
섹션은 바인딩되는데 그 안의 키 하나가 오타인 경우는 접두사 오타와 달리 조용하다 — 섹션은 붙고,
|
||||
플랫폼은 뜨고, 바꾸러 온 그 설정만 적용되지 않는다. `MessagingConfigurationKeyValidator`가
|
||||
`app.messaging.destinations|brokers|security` 아래의 모든 키를 settings 레코드에서 파생한 목록과
|
||||
대조하고, 없는 키는 그 키 이름을 담아 부팅을 거절한다.
|
||||
|
||||
허용 키 목록은 이 문서가 아니라 레코드에서 나온다. 문서에 목록을 적으면 필드가 추가된 날 그
|
||||
목록이 틀리고, 오타를 잡으라고 만든 검사가 정상 필드를 거절하게 된다.
|
||||
|
||||
환경변수(`APP_MESSAGING_...`)는 이 검사의 대상이 아니다. `APP_MESSAGING_DESTINATIONS_ORDER_EVENTS_
|
||||
CONSUMER_PREFETCH`에서 entry 이름과 leaf를 가르는 밑줄은 둘 안에 있는 밑줄과 구별되지 않으므로,
|
||||
되돌려 쪼개려면 추측해야 한다. 여기서의 추측은 정상 배포를 거절하는 쪽으로 틀리며, 그것은 배포
|
||||
매니페스트에 손으로 적어야 하는 변수에서 오타 하나를 놓치는 것보다 나쁘다.
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
플랫폼이 **무엇을 보장하는지**와 **무엇을 보장하지 않는지**를 브로커별로 고정한다.
|
||||
여기 없는 조합은 지원되지 않는다.
|
||||
|
||||
> **등급은 증거를 따른다.** `CompatibilityMatrix.Entry.hasLiveBrokerCertification()`은 선언된
|
||||
> boolean이 아니라 `CertifiedEvidence`가 가진 레인 증거에서 파생된다. RabbitMQ가 Stable에서 내려온
|
||||
> 이유가 이것이다 — 어댑터는 공유 contract 7개를 통과하고 `RabbitBrokerIT`가 실 컨테이너에서 정상
|
||||
> 경로를 돌리지만, 이 저장소의 Stable 기준인 **장애 시나리오 증거**가 하나도 없다. 레인이 생겨
|
||||
> 증거를 내면 등급은 코드 수정 없이 따라 올라간다.
|
||||
|
||||
> **인증 근거.** 이 표의 버전은 이 저장소의 컨테이너 레인이 실제로 실행한 이미지다. 이전 판은
|
||||
> Kafka 4.2/4.3을 선언했지만 fixture는 `apache/kafka:4.1.0`, lockfile client는 4.1.1이었다 — 표와
|
||||
> 코드 상수가 서로 일치했을 뿐 어느 쪽도 실행된 적이 없었다. 장애 시나리오 커버리지도 마찬가지로
|
||||
@@ -25,7 +31,7 @@
|
||||
| 브로커 | 등급 | 인증 기준 | Stable 기능 | 제한 |
|
||||
|---|---|---|---|---|
|
||||
| Kafka | Stable | 4.1.x | producer idempotence, consumer group, batch, pause/resume, replay, transaction capability | Share Group은 Experimental |
|
||||
| RabbitMQ | Stable | 4.3.x | exchange/routing, publisher confirm, mandatory return, manual ACK, quorum queue, retry queue, DLQ | stream 및 특수 plugin 미지원 |
|
||||
| RabbitMQ | Experimental | 4.3.x | exchange/routing, publisher confirm, mandatory return, manual ACK, quorum queue, retry queue, DLQ | 장애 시나리오 레인 미실행 — 증거 없음. stream 및 특수 plugin 미지원 |
|
||||
| Pulsar | Experimental | 4.0 LTS + 4.2 | typed publish/consume, Shared, Key_Shared, schema | transaction 미승격, 기본 비활성 |
|
||||
| NATS JetStream | Experimental | 2.14.x | stream, durable consumer, explicit ACK, dedupe, replay | native DLQ 없음(플랫폼이 대행), 기본 비활성 |
|
||||
| Artemis/JMS | Extension | 범위 밖 | adapter SPI만 | 별도 ADR + Contract Suite 통과 필요 |
|
||||
@@ -86,11 +92,15 @@ Kafka와 RabbitMQ가 동일한 7개 테스트를 변경 없이 통과한다. 결
|
||||
|---|---|
|
||||
| `KafkaBrokerIT` | `acks=all`이 실제 replication 증거를 만든다 / 잘못된 토픽은 `REJECTED` / 발행-소비 왕복에서 identity 보존 및 contiguous commit |
|
||||
| `KafkaAmbiguityChaosIT` | 브로커를 `docker pause`로 멈춘 상태의 publish가 **`AMBIGUOUS`** 로 보고된다 (broker acceptance 없음, confirmation level `NONE`, 비-retryable) |
|
||||
| `KafkaBrokerCertificationIT` | 인증 레인. Toxiproxy를 broker 앞에 두고 connection cut / confirm 유실 / 지연 / settlement 유실을 각각 주입하고, 통과한 시나리오마다 `BrokerCertificationEvidence` 한 줄을 manifest에 쓴다 |
|
||||
| `RabbitBrokerIT` | exchange가 confirm했는데 어떤 큐에도 바인딩되지 않은 publish가 **`REJECTED` + `UNROUTABLE`** 로 보고된다 |
|
||||
| `OutboxPostgresIT` | 롤백된 트랜잭션은 발행 가능한 행을 남기지 않는다 / `SKIP LOCKED` lease가 두 relay를 분리한다 / ambiguous 행이 같은 `messageId`로 재클레임된다 |
|
||||
| `InboxPostgresIT` | 재전달이 side effect를 두 번 적용하지 않는다 / 롤백은 예약도 되돌린다 |
|
||||
|
||||
Docker가 없으면 `DockerAvailability` 가드로 skip되며, 이 표의 항목은 그때 **검증되지 않은 것**으로 취급한다.
|
||||
`KafkaBrokerCertificationIT`만 예외다 — 인증 레인은 가드를 달지 않고 Docker가 없으면 실패한다. skip하는
|
||||
레인은 아무도 켜지 않은 브로커에 대해 성공을 보고하기 때문이다. 그래서 이 레인은 `test`에서 태그로
|
||||
제외되고 `messagingCertificationTest`로만 실행된다.
|
||||
|
||||
### 3. 장애 시나리오 커버리지 (`BrokerFailureMatrix`)
|
||||
|
||||
@@ -109,6 +119,18 @@ Docker가 없으면 `DockerAvailability` 가드로 skip되며, 이 표의 항목
|
||||
커버해야 하고, Experimental 어댑터는 `LIVE_BROKER` 커버리지를 주장할 수 없다. 커버리지는 *능력*이 아니라
|
||||
*무엇을 실제로 돌렸는지*의 기록이다.
|
||||
|
||||
**증거의 출처.** `CertifiedEvidence`는 더 이상 손으로 쓴 목록이 아니라
|
||||
`messaging-testkit/src/main/resources/messaging/broker-certification-evidence.jsonl`을 읽는다. 그 파일은
|
||||
`messagingCertificationTest` 레인이 실제 Kafka 컨테이너에 장애를 주입하며 만들어낸 출력이고,
|
||||
`verifyMessagingCertificationEvidence`가 커밋된 manifest와 이번 실행의 출력을 대조해 다르면 빌드를
|
||||
실패시킨다. 즉 **manifest를 손으로 고치면 게이트가 깨지고, 레인을 돌리면 manifest가 다시 쓰인다.**
|
||||
|
||||
오늘 Kafka가 가진 증거는 `connection-cut-after-write` · `confirm-timeout` · `high-latency` ·
|
||||
`settlement-lost` 네 개다. `connection-refused`는 남은 gap이며 그 이유가 있다 — Kafka producer는 연결
|
||||
존재 여부를 알기 전에 레코드를 버퍼에 넣으므로, 연결 거부는 전송에 대해 아무것도 증명하지 못하는
|
||||
delivery timeout으로 나타난다. 이를 `REJECTED`로 보고하는 것은 이 플랫폼이 금지한 추측이므로,
|
||||
시나리오는 `CertifiedEvidence.knownGaps`가 이름으로 들고 있는 미커버 항목으로 남는다.
|
||||
|
||||
### 실 브로커가 실제로 잡아낸 결함
|
||||
|
||||
이 스위트들은 장식이 아니다. 작성 과정에서 결정적 테스트가 통과하는데 실 인프라에서 실패한
|
||||
|
||||
Reference in New Issue
Block a user