106 lines
16 KiB
Markdown
106 lines
16 KiB
Markdown
---
|
|
title: official-doc / Spring for Apache Kafka — Message Listener Containers (AckMode & Concurrency)
|
|
source_type: official-doc
|
|
url: https://docs.spring.io/spring-kafka/reference/kafka/receiving-messages/message-listener-container.html
|
|
archive_url:
|
|
related_branches: [feature-kafka-consumer-inbox-contract]
|
|
related_projects: [ca-skeleton]
|
|
tags: [official-doc, ca-skeleton, messaging, kafka]
|
|
created: 2026-07-28
|
|
---
|
|
|
|
# official-doc / Spring for Apache Kafka — Message Listener Containers (AckMode & Concurrency)
|
|
|
|
> Layer: `raw/` — 외부 자료(공식 문서)의 **원문 발췌·출처 기록**.
|
|
> 검증된 요약은 `/ingest` 후 `wiki/concepts/`에 별도 작성. 원본은 raw에 영구 보관.
|
|
|
|
## Parent / 활용 branch
|
|
|
|
| Branch | 이 자료가 정당화하는 결정 |
|
|
|---|---|
|
|
| [[raw/branch-notes/feature-kafka-consumer-inbox-contract]] | manual acknowledgement 메커니즘(D3 — "application use case 성공 + DB 트랜잭션 커밋 이후에만 ack")을 `ContainerProperties.AckMode` 층에서 무엇으로 표현하는지의 근거. `AckMode.MANUAL`/`MANUAL_IMMEDIATE` 정의·`Acknowledgment` 호출 제약(`nack()` 스레드 제약, `acknowledge(index)` 리스너 스레드 제약)·`ConcurrentMessageListenerContainer` 의 `concurrency` 대 파티션 수 하향 조정 규칙을 확인 |
|
|
|
|
## 출처
|
|
|
|
- 원본 URL: https://docs.spring.io/spring-kafka/reference/kafka/receiving-messages/message-listener-container.html
|
|
- 아카이브 URL: (미제공)
|
|
- 저자 / 조직: Spring team (Broadcom / VMware Tanzu — Spring for Apache Kafka reference)
|
|
- 문서 버전: Spring for Apache Kafka reference **4.1.0** (페이지 상단 breadcrumb "Spring for Apache Kafka 4.1.0" 확인). 페이지 자체의 별도 발행일 표기는 없고 각 기능 옆에 도입 버전만 표기됨(예: AckMode 관련 `enable.auto.commit` 강제 false 는 2.3부터, `acknowledge(index)` 부분 배치 커밋은 3.0.10부터).
|
|
- 발행일: 명시 없음 (버전 이력만 본문에 표기)
|
|
- 마지막 확인일: 2026-07-28
|
|
|
|
## 왜 저장했는지
|
|
|
|
ca-skeleton kafka consumer inbox 브랜치(`feature-kafka-consumer-inbox-contract`)의 D3(수동 ack, DB 커밋 이후에만 offset 커밋) 결정을 Spring Kafka 컨테이너 API 레벨의 `AckMode` 값과 `Acknowledgment` 인터페이스 계약으로 정당화하기 위함. 아울러 `ConcurrentMessageListenerContainer` 의 `concurrency` 가 파티션 수를 넘길 때 어떻게 처리되는지(D4/D6 의 "파티션당 컨슈머 1개" 전제와 인접)도 이 페이지에서 확인.
|
|
|
|
**요청받은 5개 확인 대상 중 이 URL 페이지에 실제로 있는 것은 2개(AckMode 열거·MANUAL/MANUAL_IMMEDIATE 정의, concurrency 하향 조정)뿐이다.** 나머지 3개(① `Acknowledgment` 를 어느 스레드에서 호출해야 하는지의 "calling consumer thread ... otherwise queued" 계열 문장, ② ack 순서 제약 "must be acknowledged in order ... does not maintain state for each record" 계열 문장, ③ `asyncAcks`/out-of-order ack 의 pause·중복 전달 trade-off 문장)은 **self-grep 결과 이 페이지에 없음을 확인**했다(아래 ## 메모 참조). 1 dispatch = 1 URL 원칙에 따라 다른 페이지 내용을 끌어와 대신 인용하지 않았다.
|
|
|
|
## 핵심 인용
|
|
|
|
> 원문 그대로. 이 페이지는 "Message Listener Containers" 단일 페이지이며 그 안에 "Committing Offsets" 소제목이 있다. 하위 번호 섹션이 없어 위치는 fetched text 의 line 번호로 표기한다 (self-grep 참조).
|
|
|
|
> [Committing Offsets] "MANUAL: The message listener is responsible to acknowledge() the Acknowledgment." (line 447)
|
|
|
|
> [Committing Offsets] "MANUAL_IMMEDIATE: Commit the offset immediately when the Acknowledgment.acknowledge() method is called by the listener." (line 450)
|
|
|
|
> [Committing Offsets] "MANUAL and MANUAL_IMMEDIATE require the listener to be an AcknowledgingMessageListener or a BatchAcknowledgingMessageListener." (line 454)
|
|
|
|
> [Committing Offsets] "The default AckMode is BATCH." (line 429)
|
|
|
|
> [Committing Offsets] "nack() can only be called on the consumer thread that invokes your listener." (line 480)
|
|
|
|
> [Committing Offsets — partial batch commit] "The method must be called on the listener thread" (line 507, `acknowledge(index)` 제약 목록 중 하나)
|
|
|
|
> [Using ConcurrentMessageListenerContainer] "If the concurrency is greater than the number of TopicPartitions, the concurrency is adjusted down such that each container gets one partition." (line 412)
|
|
|
|
## Claims Extracted
|
|
|
|
| Claim ID | Claim | Evidence quote | Strength | Applies to | Does not prove |
|
|
|---|---|---|---|---|---|
|
|
| SPRK-ACKMODE-C1 | `AckMode.MANUAL` 은 리스너가 `Acknowledgment.acknowledge()` 를 호출할 책임을 지며, 그 이후엔 `BATCH` 와 동일한 커밋 시맨틱(poll() 이 반환한 레코드 전체 처리 후 커밋)이 적용된다 | "MANUAL: The message listener is responsible to acknowledge() the Acknowledgment." + (같은 항목의 다음 문장, self-grep 대상은 아니나 문맥) "After that, the same semantics as BATCH are applied." | `official-vendor-doc` | ca-skeleton D3 의 "use case 성공 이후에만 ack" 를 표현할 `AckMode` 값이 `MANUAL` 임을 확인 | acknowledge() 를 **언제** 호출해야 하는지(DB 커밋 성공 이후)는 애플리케이션 책임이지 프레임워크가 강제하지 않음 — 순서를 지키는 코드는 리뷰/계약 테스트로 별도 보장해야 함(branch note 의 §검증해야 할 주장 이미 인지) |
|
|
| SPRK-ACKMODE-C2 | `AckMode.MANUAL_IMMEDIATE` 는 `Acknowledgment.acknowledge()` 호출 즉시 offset 을 커밋한다 — `MANUAL` 의 배치형 커밋과 달리 호출마다 즉시 커밋 | "MANUAL_IMMEDIATE: Commit the offset immediately when the Acknowledgment.acknowledge() method is called by the listener." | `official-vendor-doc` | `MANUAL` vs `MANUAL_IMMEDIATE` 선택 — ca-skeleton D3 는 "커밋 시점"만 결정했고 이 자료는 두 모드가 커밋 빈도(배치 대 즉시)에서 갈린다는 것을 보여줄 뿐, 어느 쪽이 D3 의 의도에 더 맞는지는 이 자료만으로 결정 불가 | 두 모드의 처리량/지연 trade-off (벤치마크 없음). `acknowledge(index)` 부분 배치 커밋(SPRK-ACKMODE-C6)은 `MANUAL_IMMEDIATE` 를 요구한다는 점에서 이 claim 과 연결되나 별도 claim으로 분리 |
|
|
| SPRK-ACKMODE-C3 | `MANUAL`/`MANUAL_IMMEDIATE` 를 쓰려면 리스너가 `AcknowledgingMessageListener` 또는 `BatchAcknowledgingMessageListener` 를 구현해야 한다 | "MANUAL and MANUAL_IMMEDIATE require the listener to be an AcknowledgingMessageListener or a BatchAcknowledgingMessageListener." | `official-vendor-doc` | ca-skeleton consumer 구현 시 리스너 인터페이스 선택 제약 — `Acknowledgment` 를 받으려면 이 인터페이스 중 하나를 구현해야 함(구체 클래스/메서드 명명 자체는 이 자료 범위 밖, §구현 가이드 1 의 `UNSUPPORTED_IMPL_DECISION` 대상) | 어느 인터페이스(단건 vs 배치)를 골라야 하는지는 D4 의 처리 단위 결정에 종속 — 이 자료는 존재 요건만 말함 |
|
|
| SPRK-ACKMODE-C4 | AckMode 를 명시하지 않으면 기본값은 `BATCH` 이지 `MANUAL` 이 아니다 | "The default AckMode is BATCH." | `official-vendor-doc` | ca-skeleton 이 D3 의 수동 ack 계약을 실제로 적용하려면 `AckMode` 를 **명시적으로 `MANUAL` (또는 `MANUAL_IMMEDIATE`) 로 설정해야 함** — 설정을 빠뜨리면 조용히 `BATCH`(자동 그룹 커밋)로 동작 | `BATCH` 자체가 언제 위험한지(자동 커밋과 유사한 문제)는 이 자료가 직접 논하지 않음 — 이미 수집한 `kafka-consumer-offset-commit-semantics-apache-javadoc` 의 자동 커밋 위험 논의가 별도 근거 |
|
|
| SPRK-ACKMODE-C5 | `nack()`(2.3 부터 추가된 `Acknowledgment` 메서드)은 **리스너를 호출한 그 consumer 스레드에서만** 호출할 수 있다 | "nack() can only be called on the consumer thread that invokes your listener." | `official-vendor-doc` | `Acknowledgment` 인터페이스의 적어도 한 메서드(`nack`)가 리스너/consumer 스레드에 강하게 묶여 있다는 것을 확인 — "별도 워커 스레드에서 ack 관련 호출을 해도 되는가"라는 branch 의 질문에 **`nack()` 에 한해서는 "아니오"** 라고 명시적으로 답함 | **`acknowledge()` 자체(단건 리스너, `MANUAL` 모드)에 동일한 스레드 제약이 있다고 이 문장만으로 단정할 수 없다** — 이 페이지가 `acknowledge()` 에 대해 그런 문장을 쓰는 곳은 SPRK-ACKMODE-C6(부분 배치 커밋 변형)뿐이며, 그건 다른 API(`acknowledge(index)`, 배치 리스너, `MANUAL_IMMEDIATE` 한정)다. 사용자가 원한 "calling consumer thread ... otherwise queued" 문장은 이 페이지에 없음(미발견, 아래 Usage Boundaries) |
|
|
| SPRK-ACKMODE-C6 | 3.0.10 부터 추가된 배치 리스너의 부분 배치 커밋(`acknowledge(index)`)은 `AckMode.MANUAL_IMMEDIATE` 를 요구하고, **"리스너 스레드에서 호출되어야 한다"** 는 제약을 명시적으로 건다(그 외에도 List 소비 요건, index 범위·단조 증가 요건이 있으며 위반 시 `IllegalArgumentException`/`IllegalStateException`) | "The method must be called on the listener thread" (같은 목록의 다른 제약: "AckMode.MANUAL_IMMEDIATE is required") | `official-vendor-doc` | `acknowledge(index)` 부분 배치 커밋을 쓸 경우의 스레드 제약 — 이 자료가 제공하는 **가장 근접한** "Acknowledgment 관련 호출은 리스너/consumer 스레드에서" 근거 | 단건 레코드 리스너의 일반 `acknowledge()` 호출(`MANUAL` 모드, 배치 아님)에도 동일 제약이 명시돼 있는지는 **이 페이지에서 확인 불가** — 그 문장은 별도 페이지("Manually Committing Offsets")에 있을 가능성이 있으나 본 dispatch 범위 밖 |
|
|
| SPRK-ACKMODE-C7 | `ConcurrentMessageListenerContainer` 의 `concurrency` 가 배정 가능한 `TopicPartition` 수보다 크면, 각 delegate 컨테이너가 파티션 하나씩만 갖도록 concurrency 가 **하향 조정**된다 | "If the concurrency is greater than the number of TopicPartitions, the concurrency is adjusted down such that each container gets one partition." | `official-vendor-doc` | ca-skeleton 의 concurrency 설정 상한 근거 — 파티션 수를 넘는 concurrency 값은 무의미(초과분 컨테이너가 유휴 상태로 남지 않고 애초에 만들어지지 않음). D4/D6 이 전제하는 "파티션 하나는 컨슈머 그룹 안에서 한 컨테이너만 소비"라는 가정과 **정합**되는 Spring 프레임워크 레벨 동작 | 이 문장은 **Spring 컨테이너의 파티션 분배 동작**을 말할 뿐, Kafka 프로토콜 레벨에서 "파티션 하나는 그룹 내 정확히 한 consumer 가 소비한다"는 것 자체의 공식 Kafka 문서 근거(branch `SOURCE_GAP-1`, `needs-confirmation`)를 대신하지 않는다 — Spring 이 이렇게 동작하는 것은 그 Kafka 레벨 보장을 **전제로 구현**했을 뿐, 이 문서가 Kafka 프로토콜 자체를 규정하는 문서는 아님 |
|
|
|
|
### Strength 참고
|
|
|
|
모든 claim 은 `official-vendor-doc` (Spring 공식 reference — RFC/IETF 표준은 아니므로 `official-standard` 로 격상하지 않음).
|
|
|
|
## Usage Boundaries
|
|
|
|
- 이 자료가 직접 증명하는 것:
|
|
- `SPRK-ACKMODE-C1`~`C4`: `AckMode` 값의 열거와 커밋 시점 정의, 기본값이 `BATCH` 라는 것, `MANUAL`/`MANUAL_IMMEDIATE` 사용 시 리스너 인터페이스 요건
|
|
- `SPRK-ACKMODE-C5`: `nack()` 은 consumer/리스너 스레드에서만 호출 가능
|
|
- `SPRK-ACKMODE-C6`: `acknowledge(index)`(부분 배치 커밋, `MANUAL_IMMEDIATE` 한정)는 리스너 스레드에서 호출돼야 한다는 명시 제약
|
|
- `SPRK-ACKMODE-C7`: `concurrency` > 파티션 수일 때 하향 조정
|
|
- 이 자료가 증명하지 않는 것 (branch 가 요청했으나 이 URL 페이지에서 **미발견** — self-grep 결과 0건):
|
|
- `Acknowledgment.acknowledge()`(단건 레코드 리스너, `MANUAL` 모드) 를 **어느 스레드에서 호출해야 하는지**에 대한 일반 규칙 — 사용자가 지정한 "commit will be performed immediately if the Acknowledgment is acknowledged on the calling consumer thread; otherwise, the acks will be queued" 계열 문장은 `grep -nF`로 "calling consumer thread"/"queued" 검색 시 이 페이지에서 0건. `nack()`(C5)과 `acknowledge(index)`(C6)에 대해서만 스레드 제약이 명시돼 있고, 일반 `acknowledge()` 에 대한 동일 문장은 이 페이지 범위 밖(다른 페이지 "Manually Committing Offsets" 가능성 — 본 dispatch 는 여기서 멈춤, 1 dispatch = 1 URL)
|
|
- ack **순서** 제약 — "acknowledgments must be acknowledged in order, because Kafka does not maintain state for each record, only a committed offset for each group/partition" 계열 문장 — `grep -nF`로 "does not maintain state"/"in order" 검색 시 미발견(단, "Out of Order Commits" 라는 기능명 자체는 이 페이지에 1회 등장 — line 482, 483: `nack()` 은 Out of Order Commits 사용 시 허용 안 됨. 이 기능의 정의 문장 자체는 이 페이지에 없음)
|
|
- `asyncAcks`/out-of-order ack 채택 시 consumer 가 pause 되고 중복 전달 가능성이 커진다는 trade-off 문장 — `grep -ni "asyncAck"` 결과 0건
|
|
- 파티션 단위 pause/resume API — 이미 별도 raw 문서([[raw/official-docs/spring-kafka-listener-container-pause-resume-backpressure]] `SPRK-PAUSE-C5`)에서 동일하게 미발견 확인됨
|
|
- `commitSync`/`commitAsync` 중 어느 것을 쓸지의 권고 (이 페이지는 `syncCommits` 컨테이너 프로퍼티가 그 둘을 스위칭한다는 것만 언급하고 권고는 안 함 — 이미 branch D3 의 Open Risk 로 기록됨)
|
|
- 내 프로젝트에 적용하려면 추가 확인이 필요한 것:
|
|
- `Acknowledgment.acknowledge()` 의 일반적 스레드 제약(있다면)을 확인하려면 "Manually Committing Offsets" 페이지(`Message Listeners` 섹션의 sibling 페이지, 이 문서 최하단 nav 에서 확인됨)를 별도 `wiki-source-summarizer` dispatch 로 조사해야 한다
|
|
- ack 순서 제약과 `asyncAcks` trade-off 도 마찬가지로 별도 페이지 조사 필요 — 이 페이지에 없다고 해서 Spring Kafka 문서 전체에 없다는 뜻은 아님
|
|
|
|
## 메모
|
|
|
|
> 나중에 wiki로 옮길 때 참고할 짧은 메모. 검증되지 않은 내 추론은 여기에 두지 말 것.
|
|
|
|
- self-grep 실행 결과 (fetched text `/tmp/.../scratchpad/source-fetch-20260728-193000.txt`, 658줄):
|
|
- `calling consumer thread` → 0건
|
|
- `queued` → 0건
|
|
- `does not maintain state` → 0건
|
|
- `asyncAck` (대소문자 무관) → 0건
|
|
- `Out of Order Commits` → 1건 (line 482, 기능명만 등장 — 정의 문장 없음)
|
|
- 이 페이지 자체가 "Committing Offsets" 절 안에서 `AckMode` 7종(RECORD/BATCH/TIME/COUNT/COUNT_TIME/MANUAL/MANUAL_IMMEDIATE)을 모두 정의하지만, 이 raw 문서에는 branch 의 관심사(MANUAL/MANUAL_IMMEDIATE 대비, 스레드, concurrency)에 직접 관련된 것만 발췌했다. RECORD/BATCH/TIME/COUNT/COUNT_TIME 의 정의 자체도 필요해지면 이 문서에 claim 추가만으로 확장 가능(재-fetch 불필요, 같은 텍스트 파일에 이미 있음).
|
|
- 추가로 봐야 할 동일 출처 페이지: 사이트 nav 에서 확인된 "Manually Committing Offsets" (이 페이지 바로 다음 sibling, `Message Listeners` 섹션 하위) — `Acknowledgment.acknowledge()` 스레드 규칙과 ack 순서 제약이 있을 가능성이 높음. 별도 URL, 별도 dispatch 필요.
|
|
|
|
## 관련
|
|
|
|
- 같은 주제 다른 official-doc: `[[raw/official-docs/spring-kafka-listener-container-pause-resume-backpressure]]` (같은 "Message Listener Containers" 상위 카테고리, pause/resume 근거), `[[raw/official-docs/kafka-consumer-offset-commit-semantics-apache-javadoc]]` (Kafka 레벨 offset commit 시맨틱), `[[raw/official-docs/kafka-incremental-cooperative-rebalance-kip429]]` (concurrency/rebalance 인접 주제)
|
|
- 이 자료를 인용한 wiki 요약: `[[wiki/concepts/...]]` (생성 시)
|