Engineering Note
Kafka가 잘하는 대용량과 보내면 안 되는 대용량
Kafka는 대용량 이벤트 스트림에 강하지만 대용량 파일 전송 채널로 쓰면 시스템 전체의 storage amplification이 커지는 이유를 정리합니다.
Kafka는 대용량 처리를 잘합니다. 그래서 가끔 이런 질문이 나옵니다.
Kafka가 대용량을 잘 처리한다면, 대용량 파일도 Kafka로 보내면 안 되나?
가능하게 만들 수는 있습니다. producer의 max.request.size, topic의 max.message.bytes, consumer의 fetch.max.bytes와 max.partition.fetch.bytes 같은 설정을 조정하면 기본 제한보다 큰 record batch를 다룰 수 있습니다. Apache Kafka 문서에서도 producer request size, topic-level max message size, consumer fetch size가 각각 별도로 존재한다는 점을 확인할 수 있습니다.
하지만 이 질문은 설정값의 문제가 아닙니다. 더 중요한 차이는 payload의 성격입니다.
Kafka가 잘하는 대용량은 보통 많은 이벤트가 계속 흘러오는 대용량입니다. 반대로 대용량 파일을 Kafka message에 그대로 넣는 것은 이미 파일인 데이터를 다시 파일처럼 옮기는 일에 가깝습니다. 둘 다 byte는 크지만, 시스템 전체에서 만들어내는 storage cost model은 다릅니다.
이 글의 결론은 단순합니다.
Kafka에는 파일을 보내기보다 파일이 생겼다는 이벤트를 보내는 편이 낫다.
파일 본문은 object storage에 두고, Kafka에는 참조와 metadata를 흘려보내라.
Kafka가 말하는 대용량은 이벤트 스트림에 가깝다
Kafka는 real-time data feed, high-throughput event stream, partitioned processing을 위해 설계된 시스템입니다. 많은 producer가 이벤트를 만들고, broker는 partition log에 기록하고, 여러 consumer group이 같은 log를 독립적으로 읽습니다.
운영에서 흔히 보는 실시간 이벤트 흐름은 대략 이렇습니다.
- producer가 이벤트를 생성합니다.
- producer는 이벤트를 Kafka에 publish합니다.
- producer는 자기 로컬에 그 이벤트 payload를 오래 보관하지 않습니다.
- consumer는 이벤트를 읽고 처리합니다.
- consumer는 필요한 결과나 상태만 자기 시스템에 남깁니다.
이 모델에서 Kafka의 storage 부담은 비교적 계산하기 쉽습니다.
Kafka storage ~= incoming bytes/sec
x retention window
x replication factor
x compression/index/segment overhead
물론 실제 운영에서는 compaction, tiered storage, partition 분포, broker별 hot spot 같은 변수가 더 붙습니다. 그래도 기본 구조는 명확합니다. Kafka가 payload를 일정 시간 log로 보관하고, retention이 지나면 제거합니다.
중요한 점은 producer와 consumer가 같은 payload를 “파일 원본”으로 계속 저장하는 흐름이 아니라는 것입니다. 이벤트는 발생 사실, 상태 변화, 명령, 측정값, 로그 조각처럼 그 자체로 처리 단위가 됩니다. consumer가 이벤트를 읽은 뒤 다른 시스템에 결과를 남길 수는 있지만, 반드시 같은 payload를 파일로 다시 저장해야 하는 것은 아닙니다.
파일 payload는 흐름이 다르다
대용량 파일은 조금 다르게 움직입니다.
producer가 파일을 Kafka에 넣는다는 것은 보통 다음 중 하나입니다.
- 이미 object storage, NAS, local disk, database에 있는 파일을 읽어서 Kafka message로 만듭니다.
- 애플리케이션이 생성한 큰 binary result를 Kafka message로 직접 밀어 넣습니다.
- 업로드 요청으로 받은 파일 본문을 Kafka로 넘기고, 뒤쪽 consumer가 저장하도록 합니다.
이때 producer는 “이벤트를 만들고 버리는” 쪽이 아니라 “파일 본문을 읽어서 다른 경로로 복사하는” 쪽에 가까워집니다.
그리고 consumer도 마찬가지입니다. consumer가 수십 MB, 수백 MB짜리 payload를 받았다면 대부분은 그 payload를 다시 어딘가에 씁니다. object storage에 저장할 수도 있고, filesystem에 쓸 수도 있고, 변환 후 다른 storage에 넣을 수도 있습니다. 즉, consumer는 이벤트를 처리하고 버리는 것이 아니라 파일을 다시 파일처럼 취급합니다.
이 순간 Kafka는 파일 전송 경로의 중간 storage가 됩니다.
문제는 broker만 힘든 것이 아니다
대용량 파일을 Kafka에 넣으면 broker가 힘들어지는 것은 맞습니다.
- producer request가 커집니다.
- broker network I/O가 커집니다.
- partition log segment가 빠르게 커집니다.
- replication traffic이 커집니다.
- page cache와 disk write path에 큰 압력이 생깁니다.
- consumer fetch request와 processing latency가 흔들릴 수 있습니다.
하지만 이것만 보면 문제를 Kafka cluster 내부로만 좁혀 보게 됩니다. 실제로 더 흥미로운 지점은 Kafka 바깥입니다.
consumer group이 여러 개라고 해서 Kafka topic의 저장량이 group 수만큼 늘어나는 것은 아닙니다. Kafka는 같은 log를 여러 consumer group이 독립적으로 읽게 해줍니다. 이 구조는 Kafka의 강점입니다.
그런데 payload가 파일이면 이야기가 달라집니다. Kafka 내부에서는 같은 log를 공유하더라도, Kafka 바깥의 consumer들은 각자 그 파일 payload를 다시 저장할 가능성이 높습니다.
예를 들어 하나의 uploaded-video topic을 생각해봅시다.
| consumer group | 하는 일 | downstream storage |
|---|---|---|
thumbnail-generator | 썸네일 생성 | thumbnail bucket |
transcoder | 해상도별 변환 | video rendition bucket |
moderation-scanner | 검사 결과 저장 | moderation evidence store |
archive-writer | 장기 보관 | archive bucket |
Kafka 내부에서는 같은 message를 읽고 있을 뿐입니다. 하지만 message payload가 원본 파일이라면 각 consumer group은 자기 목적에 맞게 파일을 다시 씁니다. 결과적으로 원본 payload는 broker replication으로 한 번 커지고, downstream storage에서 consumer group별 저장본으로 다시 커집니다.
이것을 단순하게 표현하면 이렇습니다.
파일 payload 총 부담
= producer 쪽 원본 또는 임시 파일
+ Kafka log x replication factor x retention
+ consumer group별 downstream 저장본
+ retry, 재처리, 실패 보관, 임시 파일
여기서 핵심은 “Kafka가 consumer group 수만큼 저장한다”가 아닙니다. Kafka는 같은 log를 공유합니다. 문제는 파일 payload가 Kafka를 지나간 뒤에도 파일로 계속 살아남는다는 점입니다. 그래서 consumer group이 늘어날수록 시스템 전체의 storage amplification이 커질 수 있습니다.
설정을 키우는 것은 해결책이 아니다
Kafka에는 큰 message를 제어하는 설정이 있습니다.
- producer의
max.request.size - topic의
max.message.bytes - broker default인
message.max.bytes - consumer의
fetch.max.bytes - consumer의
max.partition.fetch.bytes
이 값들을 맞추지 않으면 producer는 보내지 못하거나, broker는 받지 않거나, consumer는 효율적으로 가져오지 못합니다. 그래서 “대용량 파일을 Kafka로 보내려면 어떤 설정을 바꿔야 하나요?”라는 질문은 자연스럽습니다.
하지만 이 설정들은 “큰 파일 전송을 좋은 설계로 바꿔주는 스위치”가 아닙니다. 단지 큰 record batch를 통과시킬 수 있게 만드는 제한값입니다.
제한값을 올리면 다음 질문이 바로 따라옵니다.
- producer memory와 buffering은 충분한가?
- broker network thread와 request queue는 견딜 수 있는가?
- replication이 같은 속도로 따라오는가?
- consumer가 큰 payload를 읽는 동안 poll loop와 processing timeout은 괜찮은가?
- retry가 발생하면 같은 큰 payload가 얼마나 반복 전송되는가?
- DLQ나 실패 보관 topic에도 같은 파일 payload를 넣을 것인가?
- observability에서 byte rate, request size, disk write, consumer lag를 함께 보고 있는가?
큰 message를 허용하는 것은 시작일 뿐입니다. 운영 비용 모델은 그대로 남습니다.
더 나은 패턴: 파일은 storage에, Kafka에는 참조를
대부분의 경우 더 나은 구조는 파일 본문과 이벤트를 분리하는 것입니다.
- producer 또는 upload service가 파일을 object storage에 저장합니다.
- 저장이 끝나면 Kafka에는 파일 metadata와 참조만 publish합니다.
- consumer는 Kafka event를 읽고 필요한 경우 storage에서 파일을 가져옵니다.
- consumer는 자기 목적에 맞는 결과만 저장합니다.
Kafka message는 이런 모양이 됩니다.
{
"eventType": "FileUploaded",
"fileId": "file_01JZ9M0E4A8R6Z7E3N2Q",
"bucket": "raw-upload",
"objectKey": "tenant-a/2026/06/19/file_01JZ9M0E4A8R6Z7E3N2Q.bin",
"sizeBytes": 73400320,
"contentType": "video/mp4",
"sha256": "8d8f4c7e...",
"createdAt": "2026-06-19T08:20:00Z"
}
이 방식은 Kafka가 못해서 피하는 우회가 아닙니다. 역할을 나누는 설계입니다.
| 데이터 | 어울리는 위치 | 이유 |
|---|---|---|
| 파일 본문 | object storage | 큰 binary, lifecycle, versioning, range read, access control에 적합 |
| 파일 발생 사실 | Kafka | 여러 consumer group에 빠르게 알리고 replay하기 적합 |
| 처리 결과 | consumer별 storage | thumbnail, scan result, index, transformed file처럼 목적이 다름 |
| 운영 상태 | metric/log/trace | 처리량, 실패율, latency, storage cost를 추적 |
이렇게 나누면 Kafka message는 작고 재처리하기 쉬워집니다. consumer group이 여러 개 있어도 같은 큰 payload를 Kafka에서 반복적으로 끌고 다니지 않습니다. 필요한 consumer만 파일을 읽고, 필요 없는 consumer는 metadata만 보고 건너뛸 수 있습니다.
참조 패턴에도 함정은 있다
물론 object storage 참조를 보내는 방식도 공짜는 아닙니다. 설계할 때 몇 가지를 정해야 합니다.
- Kafka publish 전에 파일 업로드가 성공했는가?
- Kafka publish는 성공했지만 이후 producer가 실패하면 정리해야 할 orphan object가 생기는가?
- consumer가 event를 읽었을 때 object가 아직 보이지 않는 eventual consistency 문제가 있는가?
- presigned URL을 보낼 것인가, bucket/key와 권한 기반 접근을 쓸 것인가?
- URL이나 token 만료 시간이 consumer retry보다 짧지 않은가?
- object lifecycle과 Kafka retention이 서로 맞는가?
- 같은
fileId를 여러 번 처리해도 idempotent한가? - checksum으로 다운로드 결과를 검증하는가?
이 질문들은 귀찮지만, 그래도 큰 파일을 Kafka payload로 밀어 넣는 것보다 다루기 쉽습니다. 파일의 생명주기는 storage 계층에서 관리하고, Kafka는 그 파일을 둘러싼 사건의 순서와 배포를 담당하게 되기 때문입니다.
언제 예외가 있을까
작은 binary payload나 제한된 내부 payload까지 모두 금지할 필요는 없습니다. 몇 KB, 몇십 KB 수준의 compressed document, 작은 이미지 thumbnail, protocol buffer payload, 암호화된 작은 blob은 Kafka message로 충분히 다룰 수 있습니다.
판단 기준은 “크다/작다” 하나가 아닙니다.
- 같은 payload를 여러 consumer group이 파일처럼 다시 저장하는가?
- retry와 DLQ에서도 payload가 반복 복사되는가?
- retention 기간 동안 broker storage가 급격히 커지는가?
- consumer가 payload를 읽기 위해 별도의 큰 memory buffer를 요구하는가?
- consumer 중 일부는 metadata만 필요한가?
- 파일 lifecycle이 Kafka retention과 다르게 움직이는가?
여기에 많이 해당한다면 Kafka에는 참조만 보내는 쪽이 낫습니다.
운영 관점에서 봐야 할 것
이 주제는 architecture 결정이면서 동시에 운영 관찰 문제입니다. 큰 payload가 Kafka에 들어오기 시작하면 message count는 평범해 보여도 byte rate, request size, broker disk write, replication traffic, consumer fetch latency는 전혀 평범하지 않을 수 있습니다.
그래서 다음 지표를 같이 봐야 합니다.
| 영역 | 볼 것 |
|---|---|
| producer | request size, record size, compression ratio, retry rate |
| broker | bytes in/out, request latency, disk write, replication lag, rejected bytes |
| topic | retention bytes, segment roll, max message size override |
| consumer | fetch size, processing time, lag, retry, downstream write latency |
| storage | object put/get latency, storage growth, lifecycle cleanup, failed object count |
이런 운영 관점은 Konduo에서도 중요하게 다룹니다. Konduo는 Kafka 같은 운영 대상을 플러그인으로 연결하고, 리소스 상태와 메트릭 근거, 알림 대응을 하나의 흐름에서 볼 수 있도록 설계된 통합 관리 운영 플랫폼입니다. Kafka 지표만이 아니라 consumer와 storage까지 함께 봐야 하는 상황에서 이런 연결된 관점이 특히 중요해집니다.
결론
Kafka가 대용량을 잘 처리한다는 말은 “아무 큰 payload나 넣어도 된다”는 뜻이 아닙니다.
Kafka가 잘하는 대용량은 많은 이벤트를 순서 있는 log로 받아 여러 consumer에게 배포하는 것입니다. 반면 대용량 파일은 이미 별도의 저장 수명을 가진 데이터 덩어리입니다. 그것을 Kafka message로 넣으면 Kafka replication, retention, consumer 재저장, retry, 실패 보관이 겹치면서 시스템 전체의 storage amplification이 커질 수 있습니다.
좋은 기준은 이 문장으로 정리할 수 있습니다.
Kafka에는 파일을 보내지 말고, 파일이 생겼다는 사건을 보내라.
파일 본문은 object storage가 맡고, Kafka는 파일을 둘러싼 이벤트와 metadata를 맡게 하는 편이 더 단순하고 오래 갑니다.
함께 읽기 좋은 글
이 글의 관점과 이어지는 주제를 더 보려면 아래 글도 함께 읽어볼 수 있습니다.
- Kafka는 메모리가 남아 있는데 왜 OOM이 났을까 - heap 밖 direct buffer까지 포함해 Kafka broker 메모리를 다시 봅니다.
- Kafka의 디스크는 어디로 가고 있나: Object Storage로 옮겨지는 Streaming Storage - Kafka storage가 object storage와 분리형 구조로 이동하는 흐름을 봅니다.
- Consumer Lag은 건강 점수가 아니다: Kafka Consuming Pressure라는 관점 - consumer lag을 처리 압력 관점으로 다시 해석합니다.