콘텐츠로 이동

이벤트에서 Kafka까지

Kafka의 구성 요소를 먼저 외우지 않고, 왜 사건을 기록해 전달해야 했는지부터 생산자·파티션·소비자의 흐름으로 이어 갑니다.

예상 읽기 시간 8분 · 핵심 질문: 직접 호출 대신 사건을 남기면 무엇을 얻고 무엇을 새로 운영해야 하는가

주문 서버가 주문을 완료한 뒤 알림, 검색 색인과 매출 집계를 모두 직접 호출한다고 생각해 보자. 세 시스템 가운데 하나가 느려지면 주문 응답도 느려진다. 하나가 잠시 꺼지면 주문 서버가 어디까지 재시도해야 하는지 판단해야 한다. 후속 작업이 늘어날수록 주문 서버는 자신과 직접 관계없는 시스템을 더 많이 알아야 한다.

이때 주문 서버가 주문이 생성되었다는 사실을 남기고 먼저 끝낼 수 있다면, 알림과 분석 시스템은 각자의 속도로 그 사실을 처리할 수 있다. Kafka는 이런 사건 기록을 순서가 있는 로그에 오래 보관하고, 여러 프로그램이 서로 다른 속도로 다시 조회할 수 있게 하는 이벤트 스트리밍 플랫폼이다.

Kafka는 느린 후속 시스템을 주문 요청에서 분리하지만 복잡성을 없애지는 않는다. 이제는 사건의 형식, 파티션 안의 순서, 소비 지연, 중복 처리와 재처리 절차를 운영해야 한다. 이 글은 그 비용이 어디서 생기는지를 한 사건의 이동 순서로 설명한다.

이벤트는 이미 일어난 사실이다

이벤트(Event)는 시스템에서 이미 일어난 사실을 표현한 기록이다. 주문을 생성하라는 앞으로 할 일을 지시하는 명령이고, 주문이 생성되었다는 과거에 일어난 결과를 알리는 이벤트다. 이름을 과거형으로 붙이는 이유는 생산자가 소비자에게 특정 행동을 강요하지 않기 위해서다.

이벤트에는 소비자가 판단할 수 있는 최소한의 맥락이 있어야 한다. 어떤 주문인지, 사건 종류와 발생 시점은 무엇인지, 같은 사건을 구분할 식별자는 무엇인지가 필요하다. 반대로 특정 이메일 서비스의 내부 필드까지 넣으면 생산자와 소비자가 다시 강하게 묶인다.

이벤트 기반 아키텍처(Event-driven Architecture)는 이런 사건을 중심으로 구성 요소가 반응하는 넓은 설계 방식이다. 한 프로세스 안에서 버튼 클릭을 처리하는 것도 이벤트 기반일 수 있고, 여러 서버가 Kafka의 사건을 소비하는 것도 이벤트 기반일 수 있다. 이벤트 기반이라고 해서 반드시 Kafka가 필요한 것은 아니다.

메시징과 이벤트 스트리밍은 무엇이 다를까

메시징(Messaging)은 프로그램 사이에 처리할 내용을 비동기로 전달하는 전체 방식을 뜻한다. 전통적인 작업 대기열에서는 하나의 작업을 소비자 하나가 가져가 처리하고, 완료된 메시지는 대개 대기열에서 사라진다. 목적은 작업을 안전하게 넘기는 데 가깝다.

이벤트 스트리밍(Event Streaming)에서는 사건을 일정 기간 로그에 남긴다. 소비자는 메시지를 없애는 대신 자신이 어디까지 처리했는지를 따로 기억한다. 알림 소비자와 분석 소비자는 같은 주문 사건을 각자 조회할 수 있고, 새 소비자를 추가한 뒤 과거 사건부터 다시 처리할 수도 있다.

단순히 이메일 발송 작업을 작업자 하나에게 넘기고 끝내려면 전통적인 대기열이 더 자연스러울 수 있다. 같은 사건을 여러 목적에서 사용하고, 과거를 다시 처리하거나 시간 흐름을 분석해야 한다면 스트리밍 로그의 가치가 커진다.

Kafka는 사건을 어디에 저장할까

Kafka에 사건을 기록하는 프로그램을 생산자(Producer), 저장된 사건을 처리하는 프로그램을 소비자(Consumer)라고 한다. 같은 종류의 사건을 묶는 논리적 이름이 토픽(Topic)이고, 토픽의 데이터를 실제로 나누어 보관하는 순서 로그가 파티션(Partition)이다. Kafka 서버는 브로커(Broker)라고 부른다.

생산자는 사건을 토픽에 보낸다. Kafka는 사건의 키를 이용해 어느 파티션에 넣을지 정하고, 그 파티션의 끝에 레코드를 추가한다. 파티션 안에서 레코드가 놓인 위치는 계속 증가하는 오프셋(Offset)으로 표시한다. 소비자는 “다음에 어느 오프셋부터 조회할지”를 저장한다.

이 관계는 다음 한 줄로 기억하면 된다.

생산자가 사건을 기록하면 브로커가 파티션 로그에 보관하고, 소비자는 자기 오프셋부터 사건을 조회한다.

주문 생산자가 Kafka 토픽의 파티션에 사건을 저장하고 알림과 분석 소비자 그룹이 각자의 처리 위치에서 조회하는 구조
한 사건은 파티션 로그에 남고, 알림과 분석 소비자 그룹은 서로 영향을 주지 않고 각자의 처리 위치를 저장합니다.

순서는 파티션 안에서만 보장된다

Kafka는 한 파티션에 추가된 레코드의 순서를 유지한다. 주문 식별자를 사건의 키로 사용하면 같은 주문의 생성 → 결제 → 배송 사건을 같은 파티션에 모을 수 있다. 하지만 서로 다른 파티션 사이에는 하나의 전역 순서가 없다.

파티션 수를 늘리면 여러 소비자가 동시에 처리할 여지가 커진다. 대신 어떤 키를 같은 파티션에 모을지 결정해야 하고, 특정 키에 사건이 몰리면 한 파티션만 바빠지는 핫 파티션이 생긴다. 모든 사건의 순서를 얻으려고 파티션을 하나만 사용하면 한 파티션의 처리량이 전체 상한이 된다. 순서 범위를 좁히는 대신 병렬성을 얻는 구조라고 이해하면 된다.

각 파티션은 보통 하나의 리더 복제본이 기록을 받고 다른 복제본이 따라간다. 생산자가 몇 개 복제본의 확인을 기다리는지와 동기화된 복제본을 얼마나 요구하는지가 기록 지연과 장애 시 보존 범위를 바꾼다. 복제 수를 설정했다는 사실만으로 모든 장애에서 사건이 보존되는 것은 아니다.

소비자 그룹은 파티션을 나누어 맡는다

같은 목적의 소비자 여러 개를 소비자 그룹(Consumer Group)으로 묶으면 파티션을 나누어 처리한다. 파티션이 여섯 개이고 소비자가 세 개라면 각 소비자가 대략 두 개씩 맡을 수 있다. 소비자가 파티션보다 많으면 일부는 맡을 일이 없다.

소비자가 추가되거나 사라지면 파티션 담당을 다시 나누는 재조정(Rebalance)이 일어난다. 담당이 바뀌는 동안 처리가 잠시 멈출 수 있고, 완료 결과와 오프셋 저장의 순서에 따라 같은 사건을 다시 받을 수도 있다. 소비자 수를 늘리는 일은 단순한 수평 확장이 아니라 재조정 빈도와 한 사건의 처리 시간을 함께 보는 문제다.

알림 그룹과 분석 그룹은 서로 다른 오프셋을 가진다. 분석이 한 시간 밀려도 알림의 처리 위치는 영향을 받지 않는다. 이것이 하나의 대기열을 여러 작업자가 경쟁해서 가져가는 모델과 다른 핵심이다.

전달 보장은 실패 시점의 선택이다

소비자는 보통 사건을 조회하고, 업무 결과를 저장하고, 다음 오프셋을 기록한다. 이 세 단계 사이에서 프로세스가 언제든 중단될 수 있다.

결과를 저장하기 전에 오프셋부터 옮기면 중단 순간의 사건을 건너뛸 수 있다. 이를 최대 한 번(At-most-once) 처리라고 한다. 결과를 저장한 뒤 오프셋을 옮기면, 그 사이에 중단됐을 때 같은 사건을 다시 처리할 수 있다. 이를 최소 한 번(At-least-once) 처리라고 한다.

정확히 한 번 의미론(Exactly-once Semantics)은 Kafka의 트랜잭션 범위 안에서 소비한 레코드, 생성한 레코드와 오프셋을 하나의 결과처럼 다루도록 돕는다. 하지만 외부 데이터베이스 업데이트, 문자 발송과 결제 승인까지 자동으로 한 번만 실행해 주는 약속은 아니다. 정확히 한 번이라는 문구보다 어느 저장소까지 같은 트랜잭션에 포함되는가를 확인해야 한다.

중복을 없애기보다 결과가 같게 만든다

분산 시스템에서는 성공 응답이 유실되면 생산자나 소비자가 재시도한다. 실제 처리는 성공했지만 확인만 못 받은 경우 같은 사건이 다시 도착할 수 있다. 이를 완전히 막기보다 같은 사건을 여러 번 처리해도 결과가 한 번 처리한 것과 같게 만드는 편이 현실적이다. 이 성질을 멱등성(Idempotency)이라고 한다.

예를 들어 사건 식별자에 고유 제약을 걸어 이미 처리한 사건이면 건너뛸 수 있다. 재고를 현재 값 - 1로 무조건 바꾸는 대신 주문 식별자와 재고 차감을 한 트랜잭션에 기록해 중복 차감을 막을 수도 있다. 중복 제거 기록을 얼마나 오래 보관할지와 외부 부수 효과가 멱등한지도 함께 정해야 한다.

데이터베이스 커밋과 사건 발행 사이의 틈

주문 데이터베이스 커밋 뒤 Kafka 발행 전에 서버가 꺼지면 주문은 존재하지만 사건은 없다. 반대로 Kafka에 먼저 발행하고 데이터베이스 커밋이 실패하면 존재하지 않는 주문의 사건이 전달된다. 서로 다른 두 시스템을 한 번에 업데이트하는 이중 쓰기(Dual Write) 문제다.

트랜잭셔널 아웃박스(Transactional Outbox)는 주문 행과 발행할 사건 행을 같은 데이터베이스 트랜잭션에 저장한다. 별도 전달기가 아웃박스 행을 Kafka로 보내므로 업무 상태와 “보내야 할 사건”의 존재를 함께 커밋할 수 있다. CDC(Change Data Capture, 변경 데이터 캡처)는 데이터베이스 변경 로그를 조회해 아웃박스의 새 행을 Kafka로 전달하는 방법으로 사용할 수 있다.

아웃박스도 중복을 없애지는 않는다. Kafka 발행 성공 뒤 아웃박스 완료 표시 전에 중단되면 같은 사건을 다시 보낼 수 있다. 그래서 소비자의 멱등 처리와 발행 지연 관측이 여전히 필요하다.

Saga는 여러 서비스의 실패를 보상한다

Saga는 여러 서비스의 로컬 트랜잭션을 순서대로 실행하고, 중간에 실패하면 이미 끝난 업무를 보상 작업으로 상쇄하는 방식이다. 결제 승인 뒤 배송 준비가 실패하면 데이터베이스 시간을 되돌리는 것이 아니라 결제 취소라는 새 업무를 수행한다.

Kafka는 Saga 단계 사이의 사건 전달에 사용할 수 있지만 Saga의 보상 규칙을 대신 결정하지 않는다. 어떤 실패는 재시도하고, 어떤 실패는 보상하며, 보상도 실패하면 누가 수동으로 처리할지를 업무가 정해야 한다.

CQRS와 이벤트 소싱은 Kafka의 필수 조건이 아니다

CQRS(Command Query Responsibility Segregation, 명령과 조회 책임 분리)는 업데이트용 모델과 조회용 모델을 서로 다른 요구에 맞게 나누는 설계다. 주문 원본은 관계형 데이터베이스에 강한 규칙으로 저장하고, Kafka 사건을 소비해 검색용 조회 모델을 따로 만들 수 있다.

이벤트 소싱(Event Sourcing)은 현재 상태 자체보다 상태를 만든 사건 이력을 원본으로 보관하는 방식이다. 모든 Kafka 사용이 이벤트 소싱인 것은 아니고, CQRS와 이벤트 소싱도 같은 개념이 아니다. 과거 사건의 형식을 오래 호환하고, 재생할 때 이메일 같은 부수 효과를 다시 실행하지 않으며, 긴 이력을 빠르게 복원할 스냅샷을 운영할 준비가 있을 때 선택해야 한다.

소비 지연은 스트리밍 시스템의 대기열이다

생산 속도가 소비 속도보다 빠르면 파티션의 최신 오프셋과 소비자가 처리한 오프셋 사이가 벌어진다. 이 차이를 소비 지연(Consumer Lag)이라고 한다. 지연이 계속 커지면 단순히 소비자를 늘리기 전에 어느 파티션이 밀리는지, 한 사건의 처리 시간이 왜 늘었는지, 외부 저장소가 포화됐는지 본다.

역압은 생산자, 브로커와 소비자의 속도 차이를 제한하는 정책이다. 생산 요청을 늦추거나 거절할지, 덜 중요한 사건을 줄일지, 보관 기간과 디스크를 늘릴지 결정해야 한다. Kafka가 디스크에 버퍼를 제공한다고 해서 처리 용량이 무한해지는 것은 아니다.

스트림 처리는 끝없는 사건을 계속 계산한다

스트림 처리는 들어오는 사건을 지속적으로 변환·결합·집계한다. 최근 5분 결제 금액처럼 시간 구간을 묶는 윈도, 주문과 결제 사건을 연결하는 조인, 키별 상태가 필요할 수 있다. 늦게 도착한 사건을 어느 시간 구간에 반영할지와 결과를 다시 계산할 수 있는지가 정확성을 바꾼다.

Kafka Streams 같은 도구는 처리 상태와 변경 기록을 함께 관리하지만, 업무 시간과 서버 수신 시간의 차이, 상태 저장 크기, 재처리 시간과 외부 결과의 멱등성은 설계자가 결정해야 한다.

Kafka보다 작업 대기열이 나은 경우

작업 하나를 작업자 하나가 가져가 처리하면 끝이고, 오래된 기록을 다시 조회하거나 여러 소비자가 같은 사건을 볼 필요가 없다면 전통적인 작업 대기열이 더 단순하다. 작업별 우선순위, 지연 실행과 개별 재시도가 핵심인 시스템도 대기열 제품의 기능이 더 자연스러울 수 있다.

Kafka는 여러 소비자, 다시 처리할 수 있는 이력, 파티션 단위의 높은 처리량이 필요할 때 강하다. 그 이점을 사용하지 않는 작은 비동기 작업에 도입하면 브로커 운영, 파티션 설계, 스키마 호환과 소비 지연만 추가될 수 있다.

이 글을 한 문장으로 설명한다면

이벤트는 이미 일어난 사실이고, 이벤트 기반 구조는 그 사실에 반응하도록 구성 요소를 분리하는 설계다. 이벤트 스트리밍은 사건을 지우지 않고 로그에 보관해 여러 소비자가 각자의 위치에서 다시 조회하게 하며, Kafka는 이를 파티션과 오프셋으로 구현한다.

이 구조는 생산자와 소비자의 시간·장애를 분리하지만 순서는 파티션 안으로 제한되고, 재시도는 중복을 만들며, 느린 소비자는 지연을 쌓는다. 따라서 Kafka 도입의 핵심 질문은 “메시지를 보낼 수 있는가”가 아니라 어떤 키의 순서를 지킬지, 중복 결과를 어떻게 막을지, 얼마나 밀리면 어떤 부하를 줄일지다.

참고 자료

공식 문서와 명세

기술 글과 패턴 설명