카프카의 정확히 한 번 처리 보장(Exactly-Once Semantics) 알아보기: 종합 가이드

신뢰할 수 있는 이벤트 처리를 위해 카프카의 정확히 한 번 처리 보장(EOS)을 알아보세요. 이 가이드는 EOS 달성을 위한 기술적 요구 사항을 분석하며, 데이터 손실이나 중복을 방지하기 위한 멱등성 프로듀서, 토픽 간 트랜잭션 쓰기, 그리고 소비자 격리 수준(`read_committed`) 및 수동 오프셋 관리의 중요한 역할을 다룹니다.

36 조회수

Kafka의 정확히 한 번 처리 의미론 이해하기: 종합 가이드

Apache Kafka는 분산 이벤트 스트리밍 플랫폼으로서 뛰어난 내구성과 확장성으로 잘 알려져 있습니다. 하지만 분산 시스템에서는 메시지가 정확히 한 번 처리되도록 보장하는 것은 네트워크 분할, 브로커 오류, 애플리케이션 재시작 등으로 인해 복잡해지는 중대한 과제입니다. 이 종합 가이드는 Kafka의 정확히 한 번 처리 의미론(Exactly-Once Semantics, EOS)을 명확히 설명하며, 이 중요한 수준의 안정성을 달성하기 위해 프로듀서와 컨슈머 모두에게 필요한 기본 메커니즘을 설명합니다.

EOS를 이해하는 것은 금융 거래나 재고 업데이트와 같이 중복되거나 누락된 데이터가 허용되지 않는 중요한 상태 변경을 처리하는 애플리케이션에 필수적입니다. 우리는 멱등성 쓰기(idempotent writes)와 정확한 소비(precise consumption)를 보장하기 위한 필수적인 구성 및 아키텍처 패턴을 탐구할 것입니다.

분산 시스템에서 데이터 보장의 과제

Kafka 설정에서 데이터 보장을 달성하려면 세 가지 주요 구성 요소, 즉 프로듀서(Producer), 브로커(Broker) (Kafka 클러스터), 컨슈머(Consumer) 간의 조정이 필요합니다.

데이터 처리 시 일반적으로 세 가지 수준의 전달 의미론이 논의됩니다.

  1. 최대 한 번 (At-Most-Once): 메시지가 손실될 수 있지만, 절대 중복되지 않습니다. 이는 프로듀서가 실패 후 메시지 전송을 재시도했지만 브로커가 이미 첫 번째 시도를 성공적으로 기록했을 때 발생할 수 있습니다.
  2. 최소 한 번 (At-Least-Once): 메시지는 손실되지 않지만, 중복될 수 있습니다. 이는 안정성을 위해 프로듀서가 구성된 경우(즉, 실패 시 재시도하도록 설정된 경우)의 기본 동작입니다.
  3. 정확히 한 번 (Exactly-Once, EOS): 메시지가 손실되지도 않고 중복되지도 않습니다. 이것이 가장 강력한 보장 수준입니다.

EOS를 달성하려면 생산 단계와 소비 단계 모두에서 문제를 완화해야 합니다.

1. Kafka 프로듀서의 정확히 한 번 처리 의미론

EOS의 첫 번째 핵심은 프로듀서가 Kafka 클러스터에 데이터를 정확히 한 번만 쓰도록 보장하는 것입니다. 이는 멱등성 프로듀서(Idempotent Producers)트랜잭션(Transactions)이라는 두 가지 주요 메커니즘을 통해 달성됩니다.

A. 멱등성 프로듀서

멱등성 프로듀서는 네트워크 오류로 인해 프로듀서가 동일한 배치(batch)를 재전송하더라도 특정 파티션에 전송된 레코드의 단일 배치가 오직 한 번만 기록되도록 보장합니다.

이는 브로커가 프로듀서 인스턴스에 고유한 Producer ID (PID)와 에포크 번호를 할당함으로써 가능해집니다. 브로커는 각 프로듀서-파티션 쌍에 대해 성공적으로 승인된 마지막 순서 번호를 추적합니다. 후속 요청이 마지막으로 승인된 순서 번호보다 작거나 같은 순서 번호로 도착하면 브로커는 중복된 배치를 자동으로 무시하고 폐기합니다.

멱등성 프로듀서를 위한 구성:

이 기능을 활성화하려면 다음 속성을 설정해야 합니다.

acks=all
enable.idempotence=true
  • acks=all (또는 -1): 프로듀서가 쓰기가 성공적이라고 간주하기 전에 리더와 모든 동기화된 복제본(ISRs)이 쓰기를 승인하도록 보장하여 내구성을 최대화합니다.
  • enable.idempotence=true: 필요한 내부 구성(예: retries를 높은 값으로 설정)을 자동으로 설정하며, 단일 파티션에 쓸 때 트랜잭션 보장이 암묵적으로 활성화되도록 합니다.

제한 사항: 멱등성 프로듀서는 단일 파티션에 대한 단일 세션 내에서만 정확히 한 번 전달을 보장합니다. 이는 파티션 간 또는 다단계 작업을 처리하지 못합니다.

B. 다중 파티션/다중 토픽 쓰기를 위한 프로듀서 트랜잭션

여러 파티션 또는 여러 Kafka 토픽에 걸쳐 EOS를 구현하기 위해서는 (예: 토픽 A에서 읽고 처리한 다음 토픽 B와 토픽 C에 원자적으로 쓰기) 트랜잭션을 사용해야 합니다. 트랜잭션은 여러 send() 호출을 원자적 단위로 묶습니다. 전체 그룹이 성공하거나, 전체 그룹이 실패하고 중단됩니다(aborted).

주요 트랜잭션 구성:

속성 설명
transactional.id 고유 문자열 트랜잭션에 필요한 식별자입니다. 애플리케이션 전체에서 고유해야 합니다.
isolation.level read_committed 커밋된 트랜잭션 데이터를 읽는 데 필요한 컨슈머 설정(나중에 설명).

트랜잭션 흐름:

  1. 트랜잭션 초기화 (Init Transactions): 프로듀서가 자신의 transactional.id를 사용하여 트랜잭션 컨텍스트를 초기화합니다.
  2. 트랜잭션 시작 (Begin Transaction): 원자적 작업의 시작을 표시합니다.
  3. 메시지 전송 (Send Messages): 프로듀서가 다양한 토픽/파티션에 레코드를 전송합니다.
  4. 커밋/중단 (Commit/Abort): 성공하면 프로듀서가 commitTransaction()을 실행하고, 그렇지 않으면 abortTransaction()을 실행합니다.

프로듀서가 트랜잭션 중간에 충돌하는 경우, 브로커는 해당 트랜잭션이 절대 커밋되지 않도록 보장하여 부분적인 쓰기를 방지합니다.

2. Kafka 컨슈머의 정확히 한 번 처리 의미론 (트랜잭션 소비)

프로듀서가 정확히 한 번 쓰기를 수행했더라도, 컨슈머는 해당 레코드를 정확히 한 번 읽고 처리해야 합니다. 이는 오프셋 커밋을 다운스트림 처리 로직과 조정해야 하므로 전통적으로 EOS 구현에서 가장 복잡한 부분입니다.

Kafka는 오프셋 커밋을 프로듀서의 트랜잭션 경계 내에 통합함으로써 트랜잭션 소비를 달성합니다. 이를 통해 컨슈머는 동일한 트랜잭션 내에서 결과 레코드(있다면)를 성공적으로 생성한 에만 레코드 배치를 읽었음을 커밋합니다.

컨슈머 격리 수준 (Consumer Isolation Level)

트랜잭션 결과물을 올바르게 읽으려면 컨슈머가 트랜잭션 경계를 존중하도록 구성되어야 합니다. 이는 컨슈머의 isolation.level 설정으로 제어됩니다.

격리 수준 동작
read_uncommitted (기본값) 컨슈머는 중단된 트랜잭션을 포함하여 모든 레코드를 읽습니다 (다운스트림 처리의 경우 최소 한 번 처리 동작).
read_committed 컨슈머는 프로듀서 트랜잭션에 의해 성공적으로 커밋된 레코드만 읽습니다. 컨슈머가 진행 중인 트랜잭션을 만나면, 대기하거나 건너뜁니다. 이는 엔드투엔드 EOS에 필수입니다.

구성 예시 (컨슈머):

isolation.level=read_committed
auto.commit.enable=false

auto.commit.enable=false의 결정적인 역할

EOS를 목표로 할 때 수동 오프셋 관리는 필수적입니다. auto.commit.enable=false로 설정해야 합니다. 자동 커밋이 활성화되면 컨슈머는 처리가 완료되기 전에 오프셋을 커밋할 수 있으며, 직후에 오류가 발생하면 데이터 손실 또는 중복으로 이어집니다.

스트림 프로세서 (읽기-처리-쓰기 루프)

진정한 엔드투엔드 EOS 파이프라인(일반적인 Kafka Streams 패턴)의 경우, 컨슈머는 트랜잭션을 사용하여 읽기 오프셋 커밋과 출력 생성을 조정해야 합니다.

  1. 트랜잭션 시작 (컨슈머의 transactional.id 사용).
  2. 배치 읽기: 입력 토픽에서 레코드를 소비합니다.
  3. 데이터 처리: 데이터를 변환합니다.
  4. 결과 쓰기: 동일한 트랜잭션 내에서 대상 토픽에 출력 레코드를 생성합니다.
  5. 오프셋 커밋: 동일한 트랜잭션 내에서 입력 토픽에 대한 읽기 오프셋을 커밋합니다.
  6. 트랜잭션 커밋.

어떤 단계라도 실패하면(예: 처리 중 예외 발생 또는 출력 쓰기 실패), 전체 트랜잭션은 중단됩니다. 재시작 시 컨슈머는 커밋되지 않은 동일한 배치를 다시 읽게 되므로, 레코드가 건너뛰거나 중복되지 않음을 보장합니다.

EOS 구현을 위한 모범 사례

정확히 한 번 처리 의미론을 사용하여 Kafka 애플리케이션을 성공적으로 배포하려면 다음의 중요한 모범 사례를 준수하십시오.

  • 프로듀서 출력에는 항상 트랜잭션 사용: 애플리케이션이 Kafka에 데이터를 쓰는 경우, 하나의 파티션에만 쓰더라도 EOS가 필요하다면 트랜잭션을 사용하십시오. 하나의 토픽/파티션에만 쓰는 경우 enable.idempotence=true를 사용하십시오.
  • read_committed 컨슈머 사용: EOS 프로듀서의 결과물을 읽는 모든 컨슈머가 isolation.level=read_committed로 설정되었는지 확인하십시오.
  • 자동 커밋 비활성화: 트랜잭션을 통한 수동 오프셋 관리는 EOS에 필수적이며 협상 불가능합니다.
  • 안정적인 transactional.id 선택: transactional.id는 애플리케이션 재시작 시에도 유지되어야 합니다. 애플리케이션이 재시작되면 동일한 ID를 사용하여 브로커와 트랜잭션 상태를 복구해야 합니다.
  • 애플리케이션 복원력: 가능하다면 처리 로직 자체를 멱등적으로 설계하십시오. Kafka가 브로커 내구성을 처리하지만, 외부 데이터베이스나 서비스 또한 잠재적인 재시도를 원활하게 처리하도록 설계되어야 합니다.

요약

Kafka의 정확히 한 번 처리 의미론은 신중하게 계층화된 메커니즘을 통해 달성됩니다. 즉, 단일 배치 안정성을 위한 프로듀서 멱등성, 원자적 다단계 작업을 위한 트랜잭션 API, 그리고 프로듀서의 트랜잭션 경계 내에 통합된 조정된 오프셋 커밋입니다. 개발자는 프로듀서에서 enable.idempotence=true를 설정하거나(간단한 경우), 트랜잭션 ID를 구성하고(복잡한 흐름의 경우), 컨슈머에서 isolation.level=read_committed를 설정하고 자동 커밋을 비활성화함으로써 최고 수준의 데이터 무결성을 보장하는 강력하고 상태 저장 스트리밍 애플리케이션을 구축할 수 있습니다.