[TIL] 이벤트 기반 아키텍처(EDA)와 Kafka의 구조에 대해 알아보자

Loopy·2025년 6월 24일
0
post-thumbnail

기존에 채팅 시스템을 구현할 때 RabbitMQ를 사용했었는데, 이와 비교해서 왜 Kafka가 왜 대용량 스트리밍 시스템에 널리 사용되는지 학습해보고자 한다.

Event Driven Architecture

EDA(Event Driven Architecture)란, MSA 환경에서 분리된 서비스들 간에 상태 변화/데이터 변경/사용자 행동이 발생한 경우, 해당 이벤트를 비동기적으로 발행(Publish)하고, 소비자(Consumer)가 이벤트를 수신해 동작을 처리하는 아키텍처 를 의미한다.

MSA 환경에서 각 서비스가 서로 직접 호출하지 않고,
상태 변화나 비즈니스 이벤트를 비동기적으로 발행(Publish)하면
이를 구독(Consume)하는 다른 서비스가 독립적으로 처리할 수 있게 해주는 아키텍처.
이 방식은 서비스 간 결합도를 낮추고, 확장성과 장애 내성을 높여준다.

비동기 통신 방식이기 때문에, 대표적으로 메시지 브로커/이벤트 브로커가 Event Bus 역할을 수행한다. (ex) Kafka, RabbitMQ, Redis Stream..)

장점

핵심은 Producer가 Consumer를 직접 호출하지 않기 때문에 서비스 간 결합도가 낮아진다는 점이다. 이로 인해 다음과 같은 장점이 생긴다.

  1. 새로운 이벤트나 처리 로직을 추가할 때 기존 컴포넌트에 미치는 영향을 최소화해 구현할 수 있다.
  2. 한 서비스에서 장애가 발생했을 때(ex) Consumer) 전체 시스템에 미치는 영향을 최소화 할 수 있기에 장애가 격리되고, 가용성이 극대화된다.
  3. 시스템에 부하가 증가할 때 Producer와 Consumer를 수평적으로 확장해 유연하게 대응할 수 있다.

단점

구조적 복잡성도 증가하지만 가장 큰 단점은 비동기 방식이기 때문에, 추가적인 처리가 없다면 데이터 일관성을 100% 보장하기 어렵다는 점이다. 이를 해결하기 위해 분산 트랜잭션 환경에서 ACID를 보장하기 위한 SAGA 패턴 등 다양한 보완 기법이 필요하다.

EDA에서는 Event Sourcing, CQRS, SAGA 등 다양한 패턴이 존재하며, 이에 대해서는 추후 별도 포스팅에서 다룰 예정이다.

이벤트 기반 아키텍처의 통신 : Kafka

EDA를 위해서 RabbitMQ, Kafka등 다양한 기술이 사용될 수 있지만 이번 포스팅에서는 Kafka에 대해서 알아보겠다. 사실 밑에서 나오는 내용은 정말 기본 개념이라, 입문 느낌으로 학습해보고 추후에 더 깊이 있게 이해해보도록 하자.

🔗 카프카 배경

고성능 분산 이벤트 스트리밍 플랫폼인 카프카는 소스 애플리케이션과 타겟 애플리케이션의 커플링을 약하게 하기 위해 나왔다. 소스 애플리케이션은 카프카에 데이터를 전송하고, 타겟 애플리케이션은 카프카에서 데이터를 가져오는 방식이다.

주로 시스템 또는 애플리케이션 간에 실시간 데이터 파이프라인을 구축할 때, MSA 환경에서 서비스 간 이벤트가 실시간으로 처리되어야 할 때 사용된다.

카프카의 주요 장점으로는 확장성, 가용성, 높은 처리량 등이 존재한다.

  1. 확장성(Scalability)
    토픽을 여러 파티션으로 나눠 여러 브로커에 분산 → 처리량이 선형적으로 증가
  2. 가용성(Availability)
    파티션을 여러 브로커에 복제 → 일부 서버 장애에도 데이터 손실 없이 서비스 지속
  3. 높은 처리량(High Throughput)
    디스크 순차 쓰기, 배치 전송, 압축 등으로 초당 수백만 건 이상의 메시지를 처리

이러한 장점을 이해하려면, 먼저 Kafka의 기본 구조와 동작 원리를 알아야 하기 때문에 다음 절에서 이를 살펴봐보자.

🔗 TOPIC이란?

카프카에는 다양한 데이터가 들어갈 수 있고, 이러한 데이터가 들어가는 공간Topic이라고 한다. 토픽의 이름을 정할때는, 목적에 따라 무슨 데이터를 담는지 명확하게 명시하면 추후 유지보수 시 편리하게 관리할 수 있다.

TOPIC 구조

하나의 토픽은 여러개의 파티션으로 구성될 수 있으며, 첫 번째 파티션 번호는 0번부터 시작한다. 하나의 파티션은 큐와 같이 내부의 데이터가 파티션 끝에서부터 차곡차곡 쌓이게 된다. 이후 Kafaka Consumer가 데이터를 가져갈때는 가장 오래된 순서대로 가져간다.

🔗 파티션이란?
토픽이 카프카에서 일종의 논리적인 개념이라면, 파티션은 토픽에 속한 레코드를 실제 저장소에 저장하는 가장 작은 단위이다. 하나의 토픽에 여러 파티션을 가질 수 있다.

파티션의 중요한 특징은 다음과 같다.

  1. 로그 구조 : 각 파티션은 시간 순서가 유지되는 불변의 레코드 시퀀스로, 장기간 디스크에 append-only 방식으로 저장된다. 또한 가능한 연속적인 블록에 저장해 순차 I/O 방식으로 처리될 수 있도록 한다. (쓰기 성능 극대화)
  2. 오프셋(Offset) : 파티션 내 메시지의 고유 순번으로, 메시지 도착 시 브로커에서 부여되며 변경이 불가능하다.

따라서 중요한 점은, 메시지 브로커처럼 메모리에 저장되고 소비되면 삭제되는 구조가 아니라 디스크에 저장되기 때문에 Consumer가 데이터(record)를 가져가도 데이터는 삭제되지 않는다는 것이다.

메세지 보존 기간 내에는 언제든지 읽어갈 수 있으므로, push 가 아니라 컨슈머가 pull 해오는 방식이 가능해진다. 따라서 이렇게 남은 파티션의 데이터는, 새로운 Consumer가 붙었을 때 다시 오래된 데이터부터 가져올 수 있게 된다. 단, 아래와 같은 조건이 두가지 있다.

1) 컨슈머 그룹이 달라야함
2) auto.offset.reset = earliest

동일 데이터를 2번 이상 용도에 맞게 다르게 처리(로그 분석, 모니터링 등)할 수 있으며, 이는 카프카를 사용하는 중요한 이유 중 하나이기도 하다.

🔗 Kafka Producer

Kafka Producer는, 데이터를 보낼 때 키(Key)를 지정하여 어느 파티션으로 보낼 지 결정할 수 있다.

  1. Key가 NUll이고, 기본 파티셔너 사용
    라운드 로빈(Round Robin) 방식으로 할당된다.

  2. Key가 NUll이 아니고, 기본 파티셔너 사용
    키의 해시(hash)값을 구하고, 특정 파티션에 할당된다. 동일한 메시지 키를 가진 레코드들은, 동일한 파티션에 들어가기 때문에 순서대로 처리할 수 있다는 장점이 있다.

🔗 Kafka Consumer

각 토픽의 파티션에 데이터를 넣게 되면, 데이터 마다 오프셋(Offset)이 붙게 되고, 카프카 컨슈머는 이 오프셋을 기준으로 다음에 읽을 메시지를 결정한다. 컨슈머가 Offset을 갱신하는 과정을 COMMIT 이라고 하는데, 커밋의 종류는 두 가지 방식이 존재한다.

  1. Automatic Commit
    일정 간격마다 자동으로 저장되며, 편리하지만 처리 중 장애가 발생하면 중복 처리나 데이터 유실이 발생할 수 있다.

  2. Manual Commit
    개발자가 메시지 처리가 끝난 후 직접 commitSync() 또는 commitAsync() 호출하는 방식이다.

같은 컨슈머 그룹 내에서 하나의 파티션단 하나의 컨슈머 인스턴스에만 할당되기에, 다른 컨슈머 그룹에 있는 컨슈머 인스턴스가 읽는 것만 가능하다.

따라서 만약 컨슈머 개수를 늘려서 데이터 처리를 분산시키고 싶은 경우(데이터 처리 속도를 높이고 싶은 경우), 파티션을 다음과 같이 늘릴 수 있다. 하지만 한번 늘린 파티션은 다시 줄일 수 없다는 것을 기억하자.

당연히 처리 순서는 “파티션 내부”에서만 보장되기 때문에, 파티션이 여러 개면 토픽 전체의 전역 순서는 보장되지 않는다. 따라서 순서를 보장하고 싶다면 하나의 파티션만 두는 구조로 가져가자.

ex) 1개의 토픽에 4개의 파티션이 있으면, 컨슈머 그룹에서 최대 4개의 컨슈머 인스턴스가 병렬로 데이터를 처리 가능하다.

카프카의 장점 : 확장성, 고가용성, 높은 처리량

앞서 카프카의 기본 구조를 살펴봤으니, 이제 왜 Kafka가 대용량 데이터 처리에 강점을 가지는지 구체적으로 알아보자.

1. 여러 브로커에 파티션 분산 처리 가능 : 확장성 보장

Kafka는 처음부터 분산 로그 저장소로 설계됐다. 즉 하나의 토픽을 여러 파티션으로 쪼개고, 이 파티션들을 여러 브로커(카프카 서버)로 분산 배치함으로써 대량의 메시지를 병렬 처리할 수 있다.

Q.하나의 브로커에 여러 파티션을 두는 것도 병렬 처리로 처리량을 늘릴 수 있는것 아닌가요?

물론 그렇지만, 하나의 브로커 안에서 여러 파티션을 두었을 때는 다음과 같은 단점이 존재한다.

  1. 모든 파티션이 같은 하드웨어 리소스(CPU, 디스크, 네트워크 I/O, 메모리)를 공유하므로 처리량이 브로커 1대의 스펙 한계에 묶여버려 수직 확장(Scale-up)이 필요하다.
  2. 브로커 장애 시 해당 브로커의 모든 파티션이 동시에 영향을 받아 가용성이 떨어진다.

이와 반대로 여러 브로커로 분산 배치하면, 처리량이 브로커 수만큼 수평적으로 확장이 용이하고(Scale-out) 브로커 하나가 죽어도 다른 브로커에 있는 파티션이 계속 서비스를 할 수 있어 고가용성이 보장된다.

2. 파티션 복제 가능 : 고가용성 보장

만약 브로커가 3개인 카프카에서 replication=1 partition = 1 인 토픽이 존재한다고 가정해보자. 갑자기 브로커가 어떠한 이유로 사용 불가능하게 된다면, 해당 파티션 내부의 데이터들은 복구할 수 없게 된다.

허나 만약 레플리카가 존재한다면, 브로커 1개가 죽더라도 나머지 한개의 Follwer Partition이 존재하기 때문에 데이터의 복구가 가능해지는 것이다. 즉, Follower Partition이 Leader Partition 역할을 승계하게 된다.

단 레플리케이션은 많을 수록 좋은 것이 아니다. 그만큼 브로커의 리소스 사용량도 늘어나기 때문에, 카프카에 들어오는 데이터량과 저장 시간을 잘 생각해서 레플리케이션 개수를 정해야 한다.

그렇다면 EDA를 위해서는 Kafka가 항상 좋은 선택일까?

당연히 모든 기술에는 트레이드 오프가 있기 때문에 그렇지 않고, 상황에 따라 적절한 기술을 선택해야 한다고 생각한다.

예를 들어서 위에서 봤듯이 순서를 보장해야 하는 경우(ex) 대기열)라면 단일 파티션 + 단일 컨슈머 구조로 사용해야 하고, 그렇다면 분산 처리가 불가능하므로 kafka를 사용하는 이점이 사라질 수 있다. 따라서 이런 경우에는 단일 큐 기반의 플랫폼 이 더 적절할 것이다.

따라서 개인적인 생각이지만 아래와 같이 정리해볼 수 있다.

kafka와 같은 고성능 기술이 더 적절한 경우

  1. 대규모의 실시간 데이터를 처리해야 해서, 높은 처리량/고가용성/분산 처리/스케일 아웃 등이 중요한 경우
  2. 하나의 메시지를 여러 컨슈머에서 처리해야 해서 데이터 영속성이 필요한 경우

RabbitMQ와 같은 다른 경량화 기술이 적절한 경우

  1. 메시지 순서 전역 보장이 필수이며, 처리량보다 순차성이 중요한 경우
  2. 데이터 영속성이 불필요하고, 즉시 처리 후 폐기되는 메시지인 경우
  3. 인프라/운영 비용과 관리 복잡성을 최소화해야 하는 소규모 환경

위와 같은 환경이라면 오히려 kafka 클러스터가 최소 3대의 브로커(서버)로 구성되므로 비용과 관리 복잡도가 증가해 오버 엔지니어링이 될 수 있다. 항상 적절하게 기술을 선택하자.

참고 자료
https://ibm-cloud-architecture.github.io/refarch-eda/technology/kafka-consumers/
https://www.inflearn.com/course/아파치-카프카-입문/dashboard

profile
개인용으로 공부하는 공간입니다. 피드백 환영합니다 🙂

0개의 댓글