실제 환경에서의 Kafka Ordering: 성능을 저하시키지 않고 확장하는 방법

발행: (2026년 3월 24일 PM 05:21 GMT+9)
9 분 소요
원문: Dev.to

Source: Dev.to

위의 링크에 포함된 전체 텍스트를 제공해 주시면, 해당 내용을 한국어로 번역해 드리겠습니다. (코드 블록, URL 및 마크다운 서식은 그대로 유지됩니다.)

소개: 순차 처리 역설

이커머스에서는 이벤트의 순서가 절대 양보될 수 없습니다. OrderCreatedPaymentAuthorized보다 OrderFulfilled를 먼저 처리할 수 없습니다. 표준 Kafka 권장 사항은 메시지 키(예: order_id)를 사용하여 모든 관련 이벤트가 같은 파티션에 시간 순서대로 들어가도록 하는 것입니다.

성능 저해 요인: 이것은 head‑of‑line (HOL) blocking을 초래합니다. 파티션에 많은 주문이 존재하고, 하나의 “핫” 주문이 느린 외부 사기 검사나 데이터베이스 타임아웃을 일으키면, 같은 파티션에 있는 다른 모든 주문—건강한 주문조차도—가 지연됩니다. 이 글은 수평 확장성을 유지하면서 엄격한 순서를 보장하는 청사진을 제공합니다.

개념: 분산 무결성 체인

규모에 맞게 질서를 유지하려면 단순한 “Kafka‑only” 관점을 넘어 세 단계로 구성된 아키텍처 안전 체인으로 전환해야 합니다.

1. 트랜잭션 아웃박스 (Producer 안전)

비즈니스 로직에서 Kafka를 직접 호출하지 마세요. 데이터베이스는 커밋되었지만 Kafka가 다운된 경우, “이중 쓰기” 실패가 발생해 이벤트가 손실됩니다.

패턴: Order 업데이트 Message 항목을 동일한 데이터베이스 트랜잭션 안에서 로컬 OUTBOX 테이블에 저장합니다.

릴레이: 별도의 프로세스(“Relay”)가 이 테이블을 폴링하고 Kafka에 게시합니다. 이를 통해 최소 1회 전달이 보장됩니다.

2. 키 기반 파티셔닝 (시퀀스 파이프)

order_id를 Kafka 키로 사용하면 해당 주문에 대한 로그가 물리적으로 순차적으로 저장됩니다. Kafka는 파티션 내에서 v1v2보다 앞서도록 하는 무거운 작업을 자동으로 처리합니다.

3. 서브‑인박스 (Consumer 병렬 처리)

Kafka 리스너는 비즈니스 로직을 처리하지 않고, 단순히 메시지를 INBOX 테이블에 기록하고 Kafka 오프셋을 즉시 커밋합니다.

작동 원리: 이제 Kafka는 높은 인입량을 유지할 수 있습니다.

“서브” 로직: 여러 백그라운드 워커 스레드가 데이터베이스 락(예: SELECT … FOR UPDATE SKIP LOCKED)을 사용해 INBOX 테이블을 조회합니다. 워커 A가 Order‑101을 처리하는 동안, 워커 B는 동일한 테이블/파티션에서 Order‑102를 동시에 처리할 수 있습니다.

중요한 결정 및 미래‑대비

1. 파티션 전략: 성장 계획

파티션은 병렬성의 단위입니다. 키의 해시 결과를 변경하지 않고는 나중에 파티션 수를 쉽게 늘릴 수 없으며, 이는 순서를 깨뜨립니다.

가이드: 첫날부터 과다 파티션을 설정하세요. 현재 10개의 파티션이 필요하다면 60개를 생성합니다. 이렇게 하면 복잡한 데이터 마이그레이션 없이도 컨슈머 그룹을 60개의 인스턴스로 확장할 수 있습니다.

2. 의사결정 매트릭스: 전략 선택

Requirement (요구 사항)Raw Kafka KeyStandard InboxSub‑Inbox (Parallel)
Ordering Scope (정렬 범위)Per Partition (파티션당)Per Key (키당)Per Key (키당)
Concurrency (동시성)1 Thread/Partition (1 스레드/파티션)1 Thread/Service (1 스레드/서비스)N Threads/Service (N 스레드/서비스)
Failure Impact (실패 영향)Blocks Partition (파티션 차단)Blocks Service (서비스 차단)Blocks ONE Order Only (하나의 주문만 차단)
Complexity (복잡도)Low (낮음)Medium (중간)High (높음)

기술 가이드: Spring Boot 구현

Producer: 트랜잭션 아웃박스

@Transactional
public void placeOrder(OrderDTO dto) {
    // 1. Persist business state
    Order order = orderRepo.save(new Order(dto));

    // 2. Persist event to Outbox in the same transaction
    outboxRepo.save(new OutboxEvent(
        order.getId(),
        "ORDER_CREATED",
        json(order)
    ));
}

Consumer: 인박스 + 수동 Ack

application.ymlack-mode: manual_immediate를 설정합니다.

@KafkaListener(topics = "orders", groupId = "fulfillment-service")
@Transactional
public void onMessage(ConsumerRecord record, Acknowledgment ack) {
    // Unique constraint on (order_id + version) handles idempotency
    inboxRepository.save(new InboxEntry(
        record.key(),
        record.value(),
        record.offset()
    ));

    // Acknowledge ONLY after the DB transaction commits
    ack.acknowledge();
}

재시도와 “문제 보관함”(DLQ)

순서가 보장된 흐름에서, 실패한 PaymentAuthorized 메시지를 DLQ로 이동시키고 ShipmentRequested는 진행하도록 하면 상태가 손상됩니다.

  • 재시도 차단: 지수 백오프를 사용합니다. Sub‑Inbox 덕분에 Order‑101에 대한 재시도는 Order‑101만 차단합니다.
  • 주문 잠금: 메시지가 재시도 한도에 도달해 DLQ로 이동하면 해당 order_id를 데이터베이스에서 “Blocked”(차단됨)으로 표시합니다. 해당 ID에 대한 이후 이벤트는 사람이 DLQ 항목을 해결할 때까지 처리되지 않아야 합니다.

Specifying and Testing the Contract

  • Specification: 스키마 레지스트리를 사용합니다. order_id를 필수 파티셔닝 키로 정의합니다.
  • Verification: Pact(계약 테스트)를 사용합니다. 소비자는 pact를 정의합니다(예: “order_id가 null이 아니어야 합니다”), 그리고 생산자는 이를 CI/CD 파이프라인에서 검증합니다.

프로덕션 체크리스트

  • Partitions: 3년 성장에 충분한 여유를 두고 생성되었습니다.
  • Database index: INBOX 테이블은 (status, order_id)에 복합 인덱스를 가지고 있습니다.
  • Idempotency: 고유 제약 조건으로 재시도 시 중복 처리를 방지합니다.
  • Monitoring: “Inbox Age”(이벤트가 DB에서 대기하는 시간)에 대한 알림을 설정합니다.
  • Cleanup: 백그라운드 작업(TTL)이 처리된 행을 24 시간마다 삭제합니다.

요약

엄격한 순서는 종종 성능 비용으로 여겨집니다. 경직된 Kafka 파티션에서 데이터베이스‑백드 Sub‑Inbox로 로직을 이동함으로써, 두 세계의 장점을 모두 얻을 수 있습니다: 절대적인 순서 무결성을 폭발적인 병렬 속도와 함께.

0 조회
Back to Blog

관련 글

더 보기 »

일관성 사고

데이터 문제 오늘 우리는 데이터 문제에 대해 논의할 것입니다. 데이터가 항상 100 % 정확하고 즉시 그렇다고 해야 할까요? 대부분의 사람들은 그렇다고 말할 것입니다, 데이터는 …

터미널용 로그 파일 뷰어

터미널용 로그 파일 뷰어. Merge, tail, search, filter, 그리고 query 로그 파일을 손쉽게 할 수 있습니다. 서버가 필요 없습니다. 설정도 필요 없습니다. 여전히 풍부한 기능을 제공합니다. !Screenshot of lnav https:...

프로그래밍 동시성

!프로그램 동시성 커버 이미지 https://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads...