본문 바로가기
카테고리 없음

Kafka 컨슈머 동작원리 설정

by sujupark54 2026. 2. 6.

코딩하는 이미지

Kafka 컨슈머는 프로듀서가 발행한 메시지를 읽어 실제 비즈니스 처리를 수행하는 역할을 담당한다. 단순히 메시지를 읽는 컴포넌트처럼 보이지만, 실제로는 파티션 할당, 오프셋 관리, 재처리 대응, 리밸런스와 장애 대응까지 고려해야 할 요소가 매우 많다. 이 글에서는 Kafka 컨슈머의 기본 개념부터 동작원리, 그리고 안정적인 처리를 위한 주요 설정들을 구조적으로 정리해본다.


Kafka 컨슈머 파티션 그룹

Kafka 컨슈머를 이해할 때 가장 먼저 짚고 넘어가야 할 개념은 토픽 파티션과 컨슈머 그룹의 관계다. Kafka에서 메시지는 토픽 단위로 저장되며, 각 토픽은 하나 이상의 파티션으로 나뉜다. 컨슈머는 단독으로 메시지를 읽지 않고, 반드시 컨슈머 그룹이라는 논리적 묶음에 속해 동작한다. 이때 파티션은 컨슈머 그룹 단위로 할당되며, 하나의 파티션은 같은 그룹 내에서 오직 하나의 컨슈머에게만 할당된다.

예를 들어 파티션이 2개이고 컨슈머가 1개라면, 하나의 컨슈머가 두 파티션의 메시지를 모두 처리한다. 반대로 파티션과 컨슈머 수가 1:1로 맞춰지면 각 컨슈머는 하나의 파티션만 담당하게 되어 병렬 처리가 극대화된다. 하지만 컨슈머 수가 파티션 수보다 많아지면, 남는 컨슈머는 파티션을 할당받지 못하고 유휴 상태로 대기하게 된다.

이러한 특성 때문에 실무에서는 컨슈머 수 ≤ 파티션 수 원칙을 유지하는 것이 일반적이다. 파티션 수는 곧 최대 병렬 처리 한계를 의미하므로, 처리량 확장이 필요하다면 컨슈머 수를 늘리는 것이 아니라 파티션 수를 늘리는 방향으로 설계하는 것이 바람직하다. 컨슈머 그룹과 파티션 관계를 잘못 설계하면 확장성이 떨어지고 리밸런스가 잦아지는 구조가 될 수 있다.


Kafka 컨슈머 오프셋 커밋

Kafka 컨슈머가 메시지를 읽을 때 기준이 되는 위치를 오프셋이라고 한다. 오프셋은 각 파티션에서 컨슈머가 어디까지 메시지를 읽었는지를 나타내는 값이며, 이 정보는 브로커에 저장된다. 컨슈머는 메시지를 처리한 뒤 “여기까지 처리했다”는 의미로 오프셋을 커밋한다.

예를 들어 0, 1, 2번 메시지를 처리한 후 오프셋 2를 커밋하면, 다음 poll() 호출 시 컨슈머는 3번 메시지부터 읽게 된다. 이 구조 덕분에 컨슈머가 재시작되거나 장애가 발생하더라도 마지막으로 커밋된 위치부터 처리를 재개할 수 있다. 오프셋 관리는 Kafka 컨슈머의 안정성을 좌우하는 핵심 요소다.

만약 커밋된 오프셋 정보가 없는 경우, auto.offset.reset 설정에 따라 읽기 시작 위치가 결정된다. earliest는 가장 처음 메시지부터, latest는 가장 최근 메시지부터 읽는다. none은 오프셋 정보가 없을 경우 예외를 발생시킨다. 이 설정은 신규 컨슈머 투입 시 처리 범위를 결정하므로 운영 환경에 맞게 신중하게 선택해야 한다.


Kafka 컨슈머 커밋 방식

Kafka 컨슈머는 오프셋 커밋을 자동 또는 수동으로 수행할 수 있다. enable.auto.commit=true로 설정하면 컨슈머는 일정 주기마다 자동으로 오프셋을 커밋한다. 이 방식은 구현이 간단하지만, 메시지 처리 완료 전에 오프셋이 커밋될 수 있어 메시지 유실 가능성이 존재한다.

반대로 enable.auto.commit=false로 설정하면 애플리케이션에서 직접 커밋 시점을 제어할 수 있다. commitSync()는 동기 방식으로, 커밋 성공 여부를 반드시 확인한다. 이 방식은 메시지 처리 신뢰성이 매우 높지만 성능은 상대적으로 떨어질 수 있다. 메시지 순서 보장이나 중복 처리가 치명적인 경우에 적합하다.

commitAsync()는 비동기 방식으로 커밋을 수행한다. 처리량이 높고 성능이 중요한 경우에 적합하며, 일부 커밋 실패를 허용할 수 있는 환경에서 사용된다. 다만 비동기 커밋은 실패 시 즉시 알기 어렵기 때문에 콜백을 통해 보완 로직을 설계하는 것이 좋다. 결국 커밋 방식 선택은 신뢰성과 처리량 사이의 균형 문제다.


Kafka 컨슈머 재처리 순서

Kafka 컨슈머는 여러 이유로 동일한 메시지를 다시 처리할 수 있다. 리밸런스, 일시적인 커밋 실패, 컨슈머 재시작 등은 모두 재처리를 유발할 수 있는 상황이다. 따라서 컨슈머 로직은 반드시 멱등성(idempotence)을 고려해야 한다.

멱등성이란 같은 메시지를 여러 번 처리하더라도 결과가 동일하게 유지되는 성질을 의미한다. 예를 들어 단순히 “값을 1 증가”시키는 로직은 중복 처리 시 치명적인 오류를 만든다. 이를 방지하기 위해 메시지에 고유 ID나 타임스탬프를 부여하고, 이미 처리한 메시지는 무시하는 방식이 자주 사용된다.

또한 재시도 과정에서 메시지 순서가 바뀔 수 있다는 점도 중요하다. max.in.flight.requests.per.connection 설정이 1보다 크면 재시도 타이밍에 따라 순서가 뒤바뀔 수 있다. 순서 보장이 중요한 시스템이라면 이 값을 1로 설정하거나, 파티션 단위 처리 전략을 엄격히 유지해야 한다.


Kafka 컨슈머 타임아웃 종료

Kafka 컨슈머는 브로커와의 연결을 유지하기 위해 주기적으로 하트비트를 전송한다. 브로커는 일정 시간 동안 하트비트를 받지 못하면 해당 컨슈머를 그룹에서 제거하고 리밸런스를 수행한다.

session.timeout.ms는 컨슈머 세션이 유효한 최대 시간을 의미하며, heartbeat.interval.ms는 하트비트 전송 주기다. 일반적으로 하트비트 주기는 세션 타임아웃의 1/3 이하로 설정하는 것이 권장된다. 또한 max.poll.interval.ms는 poll() 호출 간 최대 허용 간격을 의미한다. 이 시간을 초과하면 컨슈머는 비정상으로 간주된다.

컨슈머 종료 시에는 wakeup() 메서드를 활용하는 것이 안전하다. 다른 스레드에서 wakeup()을 호출하면 poll() 메서드가 즉시 중단되고 WakeupException이 발생한다. 이를 통해 정상적인 close() 호출로 리소스를 정리할 수 있다. Kafka 컨슈머는 스레드 세이프하지 않기 때문에 wakeup()을 제외한 대부분의 메서드는 단일 스레드에서만 호출해야 한다.