Kafka에서 데이터를 읽는 애플리케이션은 여러 컨슈머(Consumer) 인스턴스를 실행할 수 있다. 이 컨슈머들이 그룹을 이루어 데이터를 읽는 구조를 ‘컨슈머 그룹’이라고 부른다.
컨슈머 그룹은 확장성과 병렬 처리의 핵심이다.
예를 들어, 5개의 파티션을 가진 Kafka 토픽이 있다고 해보자
컨슈머 그룹(예: consumer-group-application
)을 만들고, 그 안에 컨슈머 1, 2, 3이 있다고 가정하면,
각 컨슈머는 서로 다른 파티션에서 데이터를 읽게 된다.
즉, 컨슈머 그룹 내의 컨슈머들은 파티션을 나누어 맡아 전체 토픽 데이터를 병렬로 소비한다.
이 구조는 확장성과 장애 복구에 유리하다. 만약 한 컨슈머가 장애가 나면, 대기 중인 컨슈머가 그 파티션을 대신 맡아 데이터를 이어서 읽을 수 있다.
하나의 토픽에 여러 컨슈머 그룹이 존재할 수 있다.
각 컨슈머 그룹은 독립적으로 오프셋을 관리하며, 동일한 데이터를 여러 서비스가 각자 읽어 처리할 수 있다.
예를 들어,
이렇게 하면 두 서비스가 같은 데이터 스트림을 독립적으로 소비할 수 있다.
Kafka 컨슈머는 group.id
라는 프로퍼티로 자신이 속한 그룹을 지정한다.
이 설정만으로도 컨슈머는 그룹 내에서 자동으로 파티션을 할당받아 데이터를 읽게 된다.
컨슈머 그룹의 또 다른 핵심은 오프셋 관리다.
__consumer_offsets
에 저장한다.컨슈머가 데이터를 읽고 처리한 후 오프셋을 커밋하면, 만약 컨슈머가 장애로 중단되었다가 다시 시작해도, 마지막으로 커밋한 오프셋 이후부터 데이터를 이어서 읽을 수 있다.
이 덕분에 데이터 손실 없이 장애 복구가 가능하다.
이 전략은 가장 간단하고 널리 사용되는 방식으로, Kafka가 자동으로 오프셋을 커밋한다.
작동 방식:
enable.auto.commit
속성을 true
로 설정 (기본값)auto.commit.interval.ms
에 지정된 간격(기본값 5초)마다 자동으로 오프셋 커밋poll()
메서드가 호출될 때만 발생코드 구조:
while (true) {
ConsumerRecords records = consumer.poll(Duration.ofMillis(100));
// 배치 처리 (동시에 처리)
for (ConsumerRecord record : records) {
// 각 레코드 처리
}
// 다음 poll() 호출 시 auto.commit.interval.ms가 경과했다면 자동으로 오프셋 커밋
}
장점:
단점:
이 전략은 개발자가 오프셋 커밋 시점을 직접 제어하면서도 배치 처리의 효율성을 유지한다.
작동 방식:
enable.auto.commit
속성을 false
로 설정commitSync()
또는 commitAsync()
호출코드 구조:
List> buffer = new ArrayList<>();
while (true) {
ConsumerRecords records = consumer.poll(Duration.ofMillis(100));
for (ConsumerRecord record : records) {
buffer.add(record);
}
// 배치 크기가 충분하거나 일정 시간이 경과했을 때
if (isReady(buffer)) {
processBatch(buffer);
consumer.commitAsync(); // 또는 commitSync()
buffer.clear();
}
}
장점:
단점:
이 전략은 Kafka의 내부 오프셋 저장소 대신 외부 데이터베이스나 저장소를 사용하여 오프셋을 관리한다.
작동 방식:
enable.auto.commit
속성을 false
로 설정ConsumerRebalanceListener
인터페이스 구현하여 리밸런싱 시 오프셋 관리장점:
단점:
수동 커밋 시 사용할 수 있는 두 가지 메서드의 차이점을 이해하는 것이 중요하다.
일반적인 사용 사례: 자동 커밋(전략 1)을 사용하되, 메시지 처리가 완료되기 전에 다음 poll()
을 호출하지 않도록 주의
데이터 손실에 민감한 경우: 수동 커밋(전략 2)을 사용하여 배치 처리 완료 후 명시적으로 커밋
정확히 한 번 처리가 필요한 경우: 외부 저장소를 활용한 오프셋 관리(전략 3) 또는 Kafka Streams API 사용 고려
성능과 신뢰성 균형: commitAsync()
를 주로 사용하고, 애플리케이션 종료 전에는 commitSync()
로 마무리
Kafka 컨슈머의 오프셋 커밋 전략은 애플리케이션의 요구사항과 처리 의미론(최대 한 번, 최소 한 번, 정확히 한 번)에 따라 신중하게 선택해야 한다. 자동 커밋은 간단하지만 제한적이며, 수동 커밋은 더 많은 제어를 제공하지만 구현이 복잡하다. 외부 저장소를 활용한 오프셋 관리는 정확히 한 번 처리를 보장할 수 있지만, 고급 기술이 필요하고 성능 오버헤드가 발생할 수 있다.
애플리케이션의 요구사항을 명확히 이해하고, 적절한 오프셋 커밋 전략을 선택하여 Kafka 컨슈머의 신뢰성과 성능을 최적화해야한다.
Apache Kafka에서 메시지 처리 방식을 결정하는 전달 의미론(Delivery Semantics)은 데이터 파이프라인의 신뢰성과 일관성에 직접적인 영향을 미친다.
최대 한 번 전달 방식은 메시지가 최대 한 번만 처리되며, 경우에 따라 메시지가 처리되지 않을 수 있는 방식이다.
컨슈머가 오프셋을 커밋한 후 메시지 처리 중에 충돌하면:
최소 한 번 전달 방식은 모든 메시지가 최소 한 번 이상 처리되며, 일부 메시지는 여러 번 처리될 수 있는 방식이다.
컨슈머가 메시지 처리 후 오프셋을 커밋하기 전에 충돌하면:
정확히 한 번 전달 방식은 모든 메시지가 정확히 한 번만 처리되는 이상적인 방식이다.
Kafka 0.11 버전부터 도입된 정확히 한 번 의미론은 다음 두 가지 메커니즘을 통해 구현된다:
멱등 프로듀서(Idempotent Producer):
트랜잭션 API:
Kafka에서 외부 시스템(예: OpenSearch, 데이터베이스)으로 데이터를 전송할 때는 완전한 정확히 한 번 의미론을 달성하기 어렵다. 이 경우 멱등 컨슈머 패턴을 구현해야 한다.
의미론 | 설명 |
---|---|
At-least-once | 메시지 처리 후 오프셋을 커밋. 장애 시 메시지가 중복 처리될 수 있음(기본값). |
At-most-once | 메시지 수신 즉시 오프셋을 커밋. 장애 시 일부 메시지가 유실될 수 있음. |
Exactly-once | 메시지가 정확히 한 번만 처리됨. 트랜잭션 API 등 추가 기능 필요. |
대부분의 실제 애플리케이션에서는 최소 한 번(At-least Once) 전달 방식이 권장된다. 이 방식은 데이터 손실을 방지하면서도 구현이 상대적으로 간단하다. 다만, 멱등적인 처리 로직을 구현하여 중복 처리에 대비해야 한다.
정확히 한 번(Exactly Once) 의미론은 이상적이지만 구현이 복잡하고 성능 오버헤드가 있으므로, 금융 거래와 같이 데이터 정확성이 절대적으로 중요한 경우에만 고려하는 것이 좋다.
어떤 전달 의미론을 선택하든, 시스템의 요구사항과 장애 시나리오를 철저히 이해하고 적절한 오류 처리 메커니즘을 구현하는 것이 중요하다.
Kafka 컨슈머는 일반적으로 토픽의 로그에서 지속적으로 데이터를 읽어들이지만, 애플리케이션에 버그가 발생하거나 시스템 장애가 발생하면 컨슈머가 일정 기간 동안 중단될 수 있다. 이런 상황에서 컨슈머가 다시 시작될 때 어떤 오프셋부터 읽어야 하는지 결정하는 메커니즘을 이해하는 것이 중요하다.
Kafka에는 두 가지 중요한 리텐션(보존) 기간이 있다:
기본적으로 Kafka는 7일의 오프셋 리텐션 기간을 가지고 있다. 이는 컨슈머 그룹이 7일 이상 비활성 상태(즉, 해당 그룹의 컨슈머가 없음)가 되면 해당 그룹의 커밋된 오프셋 정보가 삭제된다는 의미다.
참고: Kafka 2.0 이전 버전에서는 오프셋 리텐션 기간이 단 24시간이었다.
컨슈머 그룹의 오프셋 정보가 없거나 유효하지 않을 때(만료되었거나 범위를 벗어난 경우), auto.offset.reset
설정이 적용된다. 이 설정은 다음 세 가지 값 중 하나를 가질 수 있다:
latest (기본값): 컨슈머가 토픽의 가장 최신 메시지부터 읽기 시작한다. 즉, 컨슈머가 시작된 시점 이후에 생성된 메시지만 읽는다.
earliest: 컨슈머가 토픽의 가장 처음 메시지부터 읽기 시작한다. 이는 토픽에 있는 모든 메시지(리텐션 기간 내)를 처리하고자 할 때 유용하다.
none: 오프셋 정보가 없거나 유효하지 않을 때 예외를 발생시킨다. 이는 데이터 손실이 발생했을 때 자동으로 처리하지 않고 수동 개입을 원할 때 사용한다.
컨슈머 오프셋 리텐션 기간은 브로커 설정의 offsets.retention.minutes
파라미터로 조정할 수 있다. 많은 프로덕션 환경에서는 이 값을 기본값인 7일보다 더 길게(예: 30일) 설정하는 것이 일반적이다.
# 오프셋 리텐션 기간을 30일(43,200분)로 설정
offsets.retention.minutes=43200
특정 시점부터 데이터를 다시 처리해야 할 경우, 다음과 같은 절차를 따를 수 있다:
kafka-consumer-groups
명령을 사용하여 원하는 오프셋으로 재설정한다.이 방법을 통해 특정 시점부터 데이터를 다시 처리할 수 있다.
적절한 오프셋 리텐션 기간 설정: 애플리케이션의 요구사항에 맞게 offsets.retention.minutes
값을 조정해야한다. 장기간 중단될 가능성이 있는 경우 더 긴 기간을 설정하는 것이 좋다.
적절한 auto.offset.reset 설정: 애플리케이션의 데이터 처리 요구사항에 맞는 값을 선택해야한다.
earliest
latest
none
재처리 메커니즘 이해: 필요할 때 데이터를 다시 처리할 수 있도록 오프셋 재설정 방법을 숙지해야한다.
이러한 설정과 메커니즘을 이해하면 Kafka 컨슈머의 예기치 않은 동작을 방지하고, 데이터 처리의 신뢰성을 높일 수 있다.
Kafka 컨슈머 그룹은 대규모 데이터 스트림을 병렬로 처리하고, 장애 복구와 확장성을 보장하는 핵심 구조다.
오프셋 커밋 전략과 메시지 전달 의미론을 이해하고 적절히 활용하면, 신뢰성 높은 실시간 데이터 파이프라인을 구축할 수 있다.