시니어 엔지니어의 kafka 디자인 패턴
[!INFO] Medium 글 번역본입니다. 원글
Apache Kafka는 현대 이벤트 기반 아키텍처의 중심 축이 되었다. 하지만 Kafka를 제대로 사용하는 건 전혀 다른 이야기다. 특히 대규모 트래픽을 다룰수록 더 그렇다.
시니어 엔지니어라면, 단순히 메시지를 보내고(consuming/producing) 받는 수준을 넘어야 한다. 복원력(resilience), 처리량(throughput), 순서 보장(ordering), 장애 복구(failure recovery), 장기 유지보수성까지 설계해야 한다.
이 글은 실무에서 실제로 쓰게 될 Kafka 설계 패턴들을 다룬다.
1. 키 당 단일 작성자(Single Writer per Key) 패턴
언제 사용하나: 사용자, 세션, 특정 엔티티 단위로 이벤트 순서를 보존해야 할 때.
어떻게 동작하나: Kafka는 파티션 내부에서만 순서를 보장한다. 따라서 같은 키의 이벤트는 항상 같은 파티션으로 라우팅하고, 해당 토픽에 **오직 하나의 서비스만 쓰기(write)**를 하도록 설계하면, 순서를 지킬 수 있다.
예시: 라이드 헤일링(택시 호출) 앱에서 기사 위치 정보를 계속 전송하는 경우
- Key =
driver_id - Value =
{ "lat": ..., "lng": ..., "timestamp": ... }
팁:
- 순서가 중요한 데이터는
user_id,account_id,driver_id같은 값으로 파티션 키를 잡아라. - 순서가 중요할 때는 랜덤 파티셔닝을 절대 쓰지 말 것.
2. 최신 상태를 위한 로그 컴팩션(Log Compaction)
언제 사용하나: 특정 엔티티의 **“현재 최신 상태”**를 유지하고 싶을 때.
어떻게 동작하나: 로그 컴팩션이 켜진 토픽은 각 키에 대해 “가장 최근 메시지”만 유지한다. 이렇게 하면 Kafka 로그만으로도 서비스가 필요로 하는 상태(예: 캐시)를 다시 재구성할 수 있다.
예시: 사용자 프로필 서비스가 업데이트 이벤트를 발행한다고 하자.
- Key =
"user-123" - Value =
{ "email": "new@mail.com" }
늦게 합류한 컨슈머라도 컴팩션된 로그를 읽기만 하면 해당 사용자의 최신 프로필 정보를 다시 만들어낼 수 있다.
팁:
- 사용자 상태, 설정값(config), feature flag 등 상태성 데이터에 적합하다.
- **스냅샷(snapshot)**과 함께 사용하면 복구 속도를 크게 줄일 수 있다.
3. 여러 Consumer Group을 이용한 Fan-Out
언제 사용하나: 하나의 이벤트를 여러 다운스트림 시스템이 동시에 소비해야 할 때.
어떻게 동작하나: 각 컨슈머 그룹은 Kafka에서 독립적인 읽기 오프셋을 가진다. 즉, 프로듀서는 하나인데 읽는 쪽은 여러 그룹으로 확장 가능하다.
예시: 결제 성공 이벤트 하나를 다음에서 모두 사용해야 할 수 있다.
- 분석(Analytics) 팀
- 이메일 알림 서비스
- 부정거래(Fraud) 탐지 시스템
이들은 각각 다른 consumer group ID를 사용해 읽는다.
팁:
- 하나의 그룹 안에 너무 많은 컨슈머를 넣으면, 파티션을 두고 서로 경쟁하게 된다.
- 하지만 여러 개의 그룹을 만드는 것 자체는 두려워할 필요 없다. Kafka는 애초에 이를 위해 설계됐다.
4. 재시도(Retry) 토픽 & 데드 레터(Dead Letter) 토픽
언제 사용하나:일시적인 오류나 복구 불가능한 처리 실패를 다뤄야 할 때.
어떻게 동작하나: 실패한 메시지를 그대로 막거나(crash) 블로킹하기보다는, 다음과 같이 보낼 수 있다.
- Retry 토픽: 짧은 지연 후 재시도하기 위한 토픽
- Dead-letter 토픽(DLT): 더 이상 복구가 어렵다고 판단되는 메시지를 보내고, 알림/모니터링을 위한 용도
예시 플로우:
main-topic → processing-service → 실패 시 retry-topic ↓ 3번 재시도 후에도 실패 → dead-letter-topic
팁:
- DLT에 보낼 때는 **실패 사유(reason)**를 꼭 함께 담아라.
- DLT는 반드시 모니터링해야 한다. "조용히 쌓이게" 놔두면 언젠가 폭발한다.
5. Kafka Streams를 이용한 Exactly Once Processing(EoS)
언제 사용하나: 이벤트를 정확히 한 번(exactly once) 처리해야 할 때 (최소 한 번(at least once)도, 혹시 한 번 maybe once도 아님).
어떻게 동작하나: Kafka Streams는 다음을 통해 EOS를 지원한다.
- 멱등(idempotent) 프로듀서
- Kafka와 DB 모두에 대해 트랜잭셔널 쓰기를 수행
예시: 은행 계좌 간 이체 스트림을 처리하는 경우, 한쪽 계좌에서 출금(debit)하고 다른 계좌에 입금(credit)할 때 재시도 때문에 중복 기록이 발생하면 절대 안 된다.
builder
.stream("debit-events")
.transform(() -> new TransactionalProcessor())
.to("ledger-entries");
팁:
- 컴팩션 토픽, 멱등성 DB 쓰기와 함께 사용하면 특히 잘 맞는다.
- EoS는 복잡성을 확실히 끌어올린다. 정말 필요한 경우에만 적용해라.
6. Avro + Schema Registry로 스키마 진화(Schema Evolution)
언제 사용하나: 메시지 포맷이 시간이 지나며 계속 바뀌는 경우 (사실 모든 서비스가 그렇다).
어떻게 동작하나: Avro + Confluent Schema Registry를 사용하면 **후방/전방 호환성(backward/forward compatibility)**을 가진 스키마를 정의하고 진화시킬 수 있다.
예시:
record User {
string user_id;
string email;
string phone; // 새로 추가된 optional 필드
}
프로듀서와 컨슈머 모두, 데이터를 보내거나 받을 때 스키마 기반으로 검증한다.
팁:
- 스키마 호환성 규칙은 기본적으로 backward compatibility를 사용하는 것이 안전하다.
- 스키마 변경과 버전 관리는 문서화를 철저히 해라.
7. 이벤트 안무(Choreography) vs 오케스트레이션(Orchestration)
언제 사용하나: 주문 처리 같은 멀티 서비스 워크플로우가 있을 때.
어떻게 동작하나:
- Choreography: 각 서비스가 이벤트를 구독하고, 그에 반응하면서 일련의 흐름이 만들어진다. (느슨한 결합)
- Orchestration: 중앙 오케스트레이터가 각 서비스를 호출하고 지시하면서 플로우를 명시적으로 관리한다. (명시적 제어)
예시:
Choreography 방식
- Order 서비스가
OrderPlaced이벤트 발행 - Inventory 서비스가 이를 구독 →
InventoryReserved이벤트 발행 - Payment 서비스가 이를 구독 →
PaymentCompleted이벤트 발행
Orchestration 방식
- Orchestrator 서비스가 순서대로 Inventory, Payment 등을 호출하고 그 결과를 다시 듣고 전체 플로우를 제어한다.
팁:
- Choreography: 스케일 아웃은 쉽지만, 버그 추적이 어려워질 수 있다.
- Orchestration: 디버깅은 쉬워지지만, 결합도가 높아진다.
자주 나오는 함정(Common Gotchas)
- 파티션 수를 너무 많이 잡으면 컨슈머 시작 시간과 리밸런스가 느려진다.
- 1MB 이상의 큰 메시지는 다운스트림 시스템을 쉽게 망가뜨린다. 큰 페이로드는 Kafka에 직접 실지 말고, **Blob 스토리지(S3 등)**를 사용해라.
- 키가 없는(unkeyed) 메시지는 순서를 잃는다. 중요한 엔티티에는 항상 키를 넣어라.
- 컨슈머에서 처리 시간이 너무 길면 리밸런스를 오래 잡아먹는다. 가능하면 빠르게, 상태 없는(stateless) 처리로 설계해라.
마무리
Kafka는 그저 **"파이프"일 뿐이다. **어떻게 설계해서 쓰느냐가 모든 걸 결정한다.
시니어 엔지니어라면 Kafka는 단순히 클러스터를 세팅하는 도구가 아니다. 다음 같은 것들을 설계하는 일에 가깝다.
- 안전한 재시도 플로우
- 제대로 된 메시지 스키마
- 망가지지 않는 이벤트 기반 패턴
이 패턴들을 활용하면, 트래픽이 몰리는 상황에서도 신뢰성 있고, 관측 가능하며, 유지보수가 가능한 시스템을 만들 수 있다.
그리고 항상 기억하자:
1K TPS에서 잘 돌아간다고 해서, 100K TPS에서도 잘 돌아간다는 보장은 전혀 없다. 설계가 안 되어 있으면, 규모가 커질수록 반드시 터진다.