Post

05 데이터 정합성 - kafka

05 데이터 정합성 - kafka

1. Note

  • 전반적으로 내용의 반복,
    • 이벤트 방식으로 진행하는거고, 반드시 카프카는 아니여도됨.
    • 카프카를 기준으로 비슷한 패턴들을 검토하면 될 듯.

1. Kafka

1. Kafka

  • 대용량 데이터를 실시간으로 안정적으로 전달하기 위한 분산 메시지 시스템(Message Broker)
  • Event-Driven Architecture(EDA) 환경에서 서비스 간 이벤트를 비동기적으로 전달하는 역할

2. Kafka 흐름

  • 전체적인 흐름

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    Producer
    ↓ 이벤트 발행
    Kafka Topic 저장
    ↓
    Consumer 메시지 소비
    ↓
    비즈니스 로직 수행
    ↓
    필요 시 새로운 Event 발행
    
  • Order

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    Order Service (Producer)
    ↓
    OrderCreated Event 발행
    ↓
    (Kafka) order-topic 저장
    ↓
    Payment Consumer Group
    ├─ Consumer 1
    └─ Consumer 2
    ↓
    메시지 중 하나가 소비
    ↓
    결제 처리
    ↓
    PaymentCompleted Event 발행
    ↓
    (Kafka) payment-topic 저장
    ↓
    Inventory Consumer Group
    ↓
    재고 처리
    

2. Kafka 주요 구성요소

1. Broker

  • Kafka 서버(노드)를 의미하며, 메시지를 저장하고 관리하는 역할
  • Producer가 전송한 메시지를 저장하고, Consumer가 이를 읽을 수 있도록 제공
  • 여러 Broker를 클러스터 형태로 구성하여 대용량 처리 및 장애 대응이 가능하도록 설계
  • 위치
    1
    2
    3
    4
    5
    
    Producer
    ↓
    Broker
    ↓
    Consumer
    

2. Topic

  • 동일한 종류의 메시지를 구분하여 저장하는 논리적 공간
  • 메시지를 분류하는 카테고리 역할
  • 예시

    1
    2
    3
    4
    
    #도메인 별로 토픽을 구성
    OrderCreated event는 -> order-topic 저장  
    PaymentCreated event -> payment-topic 저장
    inventoryCreate evenet -> inventory-topic 저장
    
  • 분류

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    # 방법 1) 토픽안에 같은 도메인 내 여러 이벤트를 저장 
    order-topic
    ├─ OrderCreated 주문 성공
    ├─ OrderCanceled 주문 취소
    ├─ OrderCompleted 주문완료
    └─ OrderFailed 주문 실패
      
    # 방법 2) 동일한 이벤트에 타입을 주고 서비스단에서 구분해서 처리
    OrderCreated event
    {
      "eventType": "OrderCreated", # 이벤트 내에 타입으로 분류
      "orderId": 1
    }
    

3. Producer

  • 이벤트(Message)를 Kafka Topic으로 발행(Publish)하는 주체
  • 외부에 있음!
  • 이벤트를 생성하여 Kafka에 전달하는 역할
    • 특정 Topic 선택 가능
    • 필요 시 Key를 이용하여 메시지 분산 처리 가능
    • 데이터 작업
  • 역할

    1
    2
    3
    4
    5
    6
    7
    
    Order Service
    ↓
    OrderCreated Event 생성
    ↓
    order-topic 전송 # 여기까지가 프로듀서의 역할
    ↓
    카프카(브로커)에서 이벤트 분류 및 관리
    

4. Consumer

  • Kafka Topic의 메시지를 읽어(Consume) 처리하는 주체
  • Consumer가 지속적으로 가져가는(Pull) 방식
    • Consumer가 지속적으로 Broker에게 요청을보내서 데이터의 유무를 확인함.
    • Broker가 이벤트 발생마다 데이터를 Push 하는 구조가 X
  • Producer가 발행한 이벤트를 소비하여 필요한 비즈니스 로직을 수행하는 역할
  • 필요 시 새로운 이벤트 발행 가능
  • 흐름

    1
    2
    3
    4
    5
    6
    7
    
    Payment Service
    ↓
    order-topic 구독
    ↓
    OrderCreated Event 소비
    ↓
    결제 처리
    

5. Consumer Group

  • 여러 Consumer를 하나의 그룹으로 묶어 메시지를 분산 처리하는 구조
  • Consumer Group 내부에서 메시지를 자동으로 분배하여 처리
  • 토픽에 구성된 파티션은
    • 컨슈머별로 1개 이상의 파티션으로 매칭됨
    • 그래서 컨슈머 별로 병렬 처리가 가능해짐
  • Note
    • Consumer Group은 특정 Topic을 처리하는 Consumer들의 묶음이며,
    • 하나의 Topic은 여러 Group이 독립적으로 접근하여 각자의 목적에 맞게 메시지를 소비
  • 구조

    1
    2
    3
    4
    
    payment-group
    ├─ consumer-1
    ├─ consumer-2
    └─ consumer-3
    

3. Kafka 내부 개념

1. Partition

  • Topic 내부에서 메시지를 분산 저장하는 단위
  • Kafka는 하나의 Topic을 여러 Partition으로 나누어 메시지를 병렬 처리할 수 있도록 구성
  • 병렬로 구성되어있어서 컨슈머그룹의 여러 컨슈머가 동시에 처리가 가능해짐
  • 데이터의 순서
    • 파티션 내부에서는 보장하지만
    • P0, P1, P2 간에 순서는 보장하지 않음. (병렬처리)
  • 구성

    1
    2
    3
    4
    
    order-topic
    ├─ Partition 0 -> 주문1, 주문4, 주문7
    ├─ Partition 1 -> 주문2, 주문5, 주문8
    └─ Partition 2 -> 주문3, 주문6, 주문9
    

2. Offset

  • Partition 내부 메시지의 고유한 순번(위치 정보)
  • 메시지가 저장될 때마다 순차적인 번호를 부여

    1
    2
    3
    4
    5
    6
    
    Partition 0
    
    Offset 0 -> OrderCreated
    Offset 1 -> PaymentCompleted
    Offset 2 -> InventoryUpdated
    Offset 3 -> OrderCompleted 
    
  • 사용
    • Consumer Group이 어디까지 메시지를 읽었는지 관리하기 위해 사용
    • Consumer 장애 발생 시 마지막 Offset 이후부터 재처리 가능
    1
    2
    3
    4
    5
    6
    
    Partition 0
    
    Offset 0 -> OrderCreated -> 완료
    Offset 1 -> PaymentCompleted -> 완료
    Offset 2 -> InventoryUpdated -> 진행중
    Offset 3 -> OrderCompleted -> 다음 처리 대상
    

3. Replication

  • Partition 데이터를 복제하여 장애 상황에 대비하는 기능
  • 특정 Broker 장애 발생 시 데이터 유실을 최소화하기 위해 사용
  • Case

    1
    2
    3
    4
    5
    6
    7
    8
    
    Broker 1
    └─ Partition 0 (원본)
      
    Broker 2
    └─ Partition 0 (복제본)
      
    Broker 3
    └─ Partition 0 (복제본)
    

4. 메시지 처리 방법

1. Note

  • 메시지를 어떤 보장 수준(Guarantee)으로 처리할 것인지에 대한 부분
  • 비교표
방식특징위험구현 주체
at-most-once최대 1회 처리메시지 유실 가능Consumer 구현 (Offset 선 Commit)
at-least-once최소 1회 처리중복 처리 가능Consumer 구현 (처리 후 Commit)
exactly-once정확히 1회 처리구현 복잡 / 성능 비용Kafka 기능 + Consumer 구현

2. at-most-once

  • 메시지를 최대 한 번만 처리하는 방식
  • 중복은 없지만 메시지 유실 가능성이 존재
  • 흐름

    1
    2
    3
    4
    5
    6
    7
    
    메시지 수신
    ↓
    Offset 먼저 Commit
    ↓
    Consumer 장애 발생
    ↓
    메시지 처리 실패 # 이미 읽었다고 판단하여 재처리하지 않음  
    

3. at-least-once

  • 메시지를 최소 한 번 이상 처리하는 방식
  • 데이터 유실 방지에 집중
  • 같은 메시지가 중복 처리될 수 있음
  • 흐름

    1
    2
    3
    4
    5
    6
    7
    
    메시지 처리
    ↓
    Consumer 장애 발생
    ↓
    Offset Commit 실패
    ↓
    재시작 후 다시 처리
    

4. exactly-once

  • 메시지를 정확히 한 번만 처리하는 방식
  • 중복도 없고 유실도 최소화
  • 구현 복잡하고 성능 비용 존재함.
  • 흐름

    1
    2
    3
    4
    5
    
    메시지 처리
    ↓
    Transaction 보장
    ↓
    한 번만 Commit
    

5. 장애 처리

1. Retry

  • 메시지 처리 실패 시 다시 처리하는 방식
  • 일시적인 장애 상황(네트워크, 외부 API 오류 등)에 대응하기 위해 사용
  • 흐름

    1
    2
    3
    4
    5
    6
    7
    
    OrderCreated Event
    ↓
    Payment Service 처리 실패
    ↓
    Retry(Payment Service에서 일정시간 후 재시도 요청)
    ↓
    재처리 성공
    
  • 점검 포인트
    • 일시적 장애 복구 가능
    • 재시도 횟수 제한 필요
    • 무한 Retry 시 장애 전파 가능

2. DLQ (Dead Letter Queue)

  • Retry 이후에도 처리하지 못한 메시지를 별도로 저장하는 공간
  • 장애 메시지를 분리하여 서비스 전체 장애를 방지하기 위해 사용
  • 흐름

    1
    2
    3
    4
    5
    6
    7
    
    OrderCreated Event
    ↓
    Retry 3회 실패
    ↓
    DLQ 저장 # 정상 메시지 흐름을 막지 않고 실패 메시지만 격리
    ↓
    운영자 확인 / 수동 처리
    
  • 점검 포인트
    • 장애 메시지 추적 가능
    • 서비스 전체 중단 방지
    • 운영 관점에서 중요

3. Consumer 장애

  • Consumer 장애 발생 시 Partition을 다른 Consumer에게 재할당(Rebalance)
  • 흐름
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 정상적인 상황
payment-group
├─ consumer-A → P0
├─ consumer-B → P1
└─ consumer-C → P2

consumer-C 장애 발생
↓
Rebalance # 카프카에서 작업함
↓
consumer-A → P0 + P2 #C의 파티션을 consumer-A에 할당
consumer-B → P1

#추가적으로 
 
Offset 정보를 기반으로 마지막 처리 위치 이후부터 재처리
Consumer 장애가 발생하더라도 서비스가 지속될 수 있도록 설계됨

6. Kafka와 데이터 정합성

1. Eventual Consistency(최종 일관성)

  • Kafka 기반 비동기 구조에서는 즉시 데이터 정합성을 보장하기 어려움
  • 일정 시간이 지나면 최종적으로 데이터가 일관된 상태에 도달하는 방식
  • 흐름

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    주문 생성
    ↓
    주문 서비스 완료
    ↓
    결제 서비스 처리 중
    ↓
    재고 서비스 처리 중
      
    (일시적으로 데이터 상태 차이 발생)
      
    ↓ 일정 시간 후
      
    주문 완료
    결제 완료
    재고 차감 완료
    # 최종적으로 동일함
    

2. 순서 보장 이슈

  • Partition 내부에서만 순서를 보장됨
1
2
3
4
5
6
7
8
9
10
# 파티션 내부에서의 흐름은 보장됨.
P0 → 1번 이벤트 → 2번 이벤트 → 3번 이벤트 

# 파티션끼리는 흐름이 보장이 X 
# 실행된 스레드마다 작업속도나 상황이 다르기 때문에
# 종료되는 순서가 다르거나, 역전되는 상황이 발생할 수 있음
# 재고/결제 같은 민감 영역은 별도 보호 필요
P1 → 1번 이벤트 → 4번 이벤트
P3 → 2번 이벤트 → 5번 이벤트
P4 → 3번 이벤트 → 6번 이벤트

3. 중복 메시지 고려

  • 장애 상황에서 동일 메시지가 다시 처리될 수 있음 (at-least-once 기반)
  • 흐름

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    결제 완료
    ↓
    Offset Commit 전 Consumer 장애
    ↓
    재시작 후 동일 메시지 재처리
      
    # 최종적으로 결제 2번 실행
    # 중복 결제 위험
    # 멱등성(Idempotency) 처리가 필요
    

7. Spring&Kafka

1. 셋팅

1. 의존성

1
2
3
  implementation 'org.springframework.kafka:spring-kafka'
  // Spring Kafka 라이브러리
  // Producer / Consumer 기능 제공

2. properties

1
2
3
4
5
6
7
8
9
10
11
12
13
  spring:
    kafka:
      bootstrap-servers: localhost:9092
      # Kafka Broker 주소
  
      consumer:
        group-id: order-group
        # Consumer Group 이름
  
        auto-offset-reset: earliest
        # Offset 없을 경우 시작 위치
        # earliest : 처음부터 읽기
        # latest : 최신 메시지부터 읽기

3. Config

1
2
3
4
5
6
  @Configuration
  @EnableKafka
  // Kafka 기능 활성화
  public class KafkaConfig {
  
  }

2. Producer Service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  @Service
  @RequiredArgsConstructor
  public class OrderProducer {
  
      private final KafkaTemplate<String, Object> kafkaTemplate;
      // Kafka 메시지 발행 객체
  
      public void sendOrder(OrderEvent event) {
  
          kafkaTemplate.send(
                  "order-topic",
                  event
          );
          // 특정 Topic으로 메시지 발행
      }
  }

3. Consumer Service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  @Component
  public class OrderConsumer {
  
      // @KafkaListener가 내부적으로 계속 polling처리해서 별도 실행X
      @KafkaListener(
              topics = "order-topic",
              groupId = "payment-group"
      )
      // order-topic 구독
      // payment-group Consumer Group 사용
      public void consume(OrderEvent event) {
  
          System.out.println(event);
          // 메시지 소비 후 비즈니스 로직 처리
      }
  }
This post is licensed under CC BY 4.0 by the author.