# Consumer Internals

컨슈머는 토픽의 메시지를 읽어 처리하는 클라이언트이며, 내부적으로는 컨슈머 그룹이라는 개념을 통해 확장성과 안정성을 보장받는다.

## 그룹과 코디네이터

### 컨슈머 그룹

컨슈머 그룹은 단순히 컨슈머의 집합이 아니라, 카프카 브로커에 의해 중앙에서 관리되는 하나의 단위이다.

* 컨슈머(Consumer): 토픽에서 이벤트를 가져와서(Pull) 처리하는 클라이언트 애플리케이션
  * 각 컨슈머는 자신이 읽은 이벤트의 오프셋(Offset)을 관리하며, 필요에 따라 오프셋을 커밋하여 어디까지 읽었는지 기록
  * 오프셋 커밋 방식에는 자동 커밋(auto-commit)과 수동 커밋(manual commit)이 있으며, 일반적으로 처리 완료 후 배치 단위로 커밋하는 방식을 권장
* 컨슈머 그룹(Consumer Group): 동일한 목적을 위해 특정 토픽을 구독하는 컨슈머들의 집합
  * 카프카 컨슈머의 확장성과 고가용성을 구현하는 핵심 개념
  * 토픽의 각 파티션은 컨슈머 그룹 내 단 하나의 컨슈머에게만 할당
    * 예시
      * 10개의 파티션을 가진 토픽이 있다면, 한 컨슈머 그룹은 최대 10개의 컨슈머를 투입하여 병렬로 데이터를 처리 가능
        * 더 추가하더라도 파티션 수가 부족하여 추가 컨슈머는 유휴 상태
      * 만약 그룹 내 컨슈머 중 하나에 장애가 발생하면, 카프카는 리밸런스(Rebalance) 과정을 통해 다른 컨슈머에게 자동으로 재할당
  * 각 그룹이 독립적인 오프셋 관리
    * 동일한 토픽을 여러 컨슈머 그룹이 각자 독립적으로 구독 가능(동일한 이벤트를 서로 다른 목적으로 처리 가능)

### 그룹 코디네이터

각 컨슈머 그룹은 브로커 중 하나를 그룹 코디네이터(Group Coordinator)로 할당받아 관리된다.

* 그룹 코디네이터(Group Coordinator)
  * 각 컨슈머 그룹은 브로커 중 하나를 코디네이터로 할당
  * 코디네이터는 그룹 내 컨슈머들의 상태를 추적하고, 새로운 컨슈머가 참여하거나 기존 컨슈머가 이탈할 때 파티션 재할당(리밸런싱)을 주도하는 역할
* 컨슈머의 그룹 참여 과정
  * 컨슈머는 시작 시 코디네이터에게 요청을 보내 그룹에 참여
  * 그룹 내 첫 번째로 참여한 컨슈머가 리더 역할 수행
    * 리더는 코디네이터로부터 그룹 멤버 목록과 구독 토픽 정보를 받아 어떤 컨슈머가 어떤 파티션을 할당받을지 결정
  * 리더가 결정한 할당 계획을 코디네이터에게 전달하면, 코디네이터는 이 계획을 모든 그룹 멤버에게 전파하여 각자 자신이 담당할 파티션을 인지하고 메시지 처리 시작

## 그룹 생명주기

### 하트비트

코디네이터는 컨슈머가 정상 동작 중인지 주기적으로 확인하며, 이를 위해 하트비트(Heartbeat) 메커니즘을 사용한다.

* `heartbeat.interval.ms`
  * 컨슈머가 코디네이터에게 자신이 살아있음을 알리기 위해 하트비트를 보내는 주기
  * `session.timeout.ms`보다 반드시 낮게 설정
* `session.timeout.ms`
  * 코디네이터가 컨슈머로부터 하트비트를 받지 못했을 때, 해당 컨슈머가 비정상 종료되었다고 판단하기까지 기다리는 최대 시간
  * 이 시간이 초과되면 코디네이터는 해당 컨슈머를 그룹에서 제외하고 리밸런싱을 시작

### 리밸런싱

리밸런싱은 컨슈머 그룹의 확장성과 고가용성을 위해 파티션의 소유권을 동적으로 재분배하는 과정이다.

* 리밸런싱 발생 시점
  * 그룹에 새로운 컨슈머가 참여할 때
  * 그룹의 기존 컨슈머가 종료되거나, 장애로 인해 세션 타임아웃을 초과할 때
  * 구독 중인 토픽의 파티션 수가 변경될 때
* 리밸런싱 과정과 영향
  * 리밸런싱이 발생하는 동안, 해당 컨슈머 그룹의 모든 컨슈머는 일시적으로 메시지 처리를 중단
  * 이 중단 시간을 최소화하는 것이 중요
    * `session.timeout.ms`, `heartbeat.interval.ms` 등의 설정을 통해 리밸런스 감지 민감도 조절 가능

### 파티션 할당 전략

각 컨슈머 그룹 내에서 파티션을 컨슈머들에게 어떻게 분배할지 결정하는 전략이다.

* `Range`: 토픽별로 파티션을 연속된 범위로 계산하여 할당
* `RoundRobin`: 모든 토픽의 파티션을 모아 라운드로빈 방식으로 순서대로 할당
* `Sticky`: 기존의 파티션 할당을 최대한 유지하려 시도(리밸런싱 시 최소한의 파티션 이동만 발생시켜 안정성을 높임)
* `CooperativeSticky`: `Sticky` 전략을 개선하여, "Stop-the-world" 없이 일부 컨슈머는 계속해서 기존 파티션을 처리하도록 허용하는 점진적 리밸런싱을 지원

## 데이터 수신 방식

컨슈머는 브로커가 밀어주는(Push) 방식이 아닌, 능동적으로 데이터를 가져오는(Pull) 모델을 사용하며, `poll()` 메소드를 통해 주기적으로 데이터를 요청한다.

* `max.poll.interval.ms`
  * `poll()` 호출 사이의 최대 허용 시간.
  * 만약 메시지 처리 로직이 너무 오래 걸려 이 시간을 초과하면, 컨슈머는 비정상으로 간주되어 그룹에서 이탈되고 리밸런싱 발생
* 데이터 페치 관련 주요 설정
  * `fetch.min.bytes`: 브로커가 컨슈머에게 응답을 주기 전까지 기다리는 데이터의 최소 크기
  * `fetch.max.wait.ms`: `fetch.min.bytes`에 도달하지 못하더라도, 브로커가 응답을 주기까지 대기하는 최대 시간
  * `max.poll.records`: `poll()` 한번의 호출로 반환받는 최대 레코드 수

## 트랜잭션과 데이터 격리

트랜잭셔널 프로듀서가 보낸 메시지를 처리할 때, 컨슈머는 어떤 상태의 메시지까지 읽을지 결정할 수 있다.

* `read_uncommitted`: 트랜잭션의 커밋 여부와 상관없이 모든 메시지를 읽음
* `read_committed`: 성공적으로 커밋된 트랜잭션의 메시지만 읽음

## 메시지 커밋 전략

컨슈머가 메시지를 어디까지 처리했는지에 대한 오프셋(Offset)을 기록하는 행위를 커밋(Commit)이라고 하며, 이 커밋 전략에 따라 메시지 처리의 신뢰성 수준이 결정된다.

* 자동 커밋(`enable.auto.commit=true`)
  * `poll()` 호출 시, `auto.commit.interval.ms` 주기에 맞춰 이전에 `poll()`로 반환된 마지막 오프셋을 자동으로 커밋
  * 메시지 처리 완료 여부와 관계없이 커밋이 발생할 수 있어 데이터 유실이나 중복의 위험 존재
    * 유실: 메시지 처리에 실패했으나 다음 `poll()`에서 자동 커밋되면, 해당 메시지는 처리된 것으로 간주
    * 중복: 메시지 처리는 성공했으나 커밋 전에 장애가 발생하면, 재시작 후 마지막 커밋 지점부터 다시 처리
* 수동 커밋(`enable.auto.commit=false`)
  * 개발자가 코드에서 명시적으로 커밋 시점을 제어하는 방식
  * 동기 커밋(`commitSync`)
    * 브로커로부터 커밋 성공 응답을 받을 때까지 블로킹
    * 커밋이 확실하게 보장되어 신뢰성이 높음
  * 비동기 커밋(`commitAsync`)
    * 커밋 요청 후 응답을 기다리지 않고 즉시 다음 로직을 수행
    * 처리량은 높지만 커밋 실패 시 재시도가 복잡하고, 순서가 중요한 경우 주의 필요
