Spring/Kafka

[Kafka] 좌석 예매 시스템의 성능 병목 해결기와 데이터 정합성 보장

챛채 2025. 5. 27. 16:07

- FeignClient에서 Kafka로의 변환 : 좌석 예매 시스템의 성능 병목 해결기

1. 도입 배경: FeignClient 기반 구조의 한계

초기 티켓팅 시스템은 서비스 간 통신에 FeignClient를 사용하고 있었습니다.
구현이 간편하고 직관적이지만 실서비스 시나리오에서 아래와 같은 문제를 마주했습니다:

  • 수백 명이 동시에 좌석을 예매할 때 동기 호출의 병목 현상
  • 일부 서비스 실패 시 장애 전파 위험
  • 서비스 간 결합도가 높아 확장성 제한

이러한 문제들을 해결하기 위해 Kafka 기반의 이벤트 처리 구조로 전환을 결정했습니다.

 

2. Kafka 기반 구조로의 전환

Kafka를 도입하면서 서비스 간 통신을 비동기 이벤트 기반으로 재설계하였고 구조는 다음과 같습니다.

1. 공통 이벤트 구조 정의

모든 메시지를 공통 구조로 감싸 Kafka로 발행합니다.

public record GenericKafkaEvent<T>(String topic, T payload) implements KafkaEvent {}

 

  • 다양한 도메인 메시지를 GenericKafkaEvent로 통일
  • topic 필드로 토픽 라우팅

2. Kafka 발행 로직 (Producer)

예매 확정, 실패, 좌석 만료 등의 시점에 이벤트를 발행합니다.

GenericKafkaEvent<SeatConfirmedMessage> event =
    new GenericKafkaEvent<>(KafkaTopicType.SEAT_CONFIRM.getTopic(), new SeatConfirmedMessage(userId));
applicationEventPublisher.publishEvent(event);

 

→ @TransactionalEventListener를 통해 실제 Kafka로 발행:

@EventListener
public void handleKafkaMessage(KafkaEvent event) {
    String json = objectMapper.writeValueAsString(event);
    kafkaTemplate.send(event.topic(), json);
}

 

3. Kafka 수신 로직 (Consumer)

Ticket 서비스에서 발행한 메시지를 수신하여 좌석 상태를 변경합니다.

@KafkaListener(topics = "ticket_confirm")
public void consumeTicketConfirm(String json) {
    TicketConfirmedMessage message = parse(json);
    seatService.confirmSeats(message.seatIds(), message.userId());
}
  • 예매 확정(ticket_confirm)
  • 예매 취소(ticket_cancel)
  • 좌석 만료(seat_expired) 등 다양한 메시지를 수신 처리

4. DLQ(Dead Letter Queue) 구성

Kafka 메시지 수신 중 오류가 발생하면 .DLQ 토픽으로 전송하여 장애를 격리하고 추적합니다.

@KafkaListener(topics = "ticket_cancel.DLQ")
public void consumeTicketCancelDlq(ConsumerRecord<String, String> record) {
    log.error("DLQ 처리 실패 메시지: {}", record.value());
}

 

3. 성능 개선 효과

 

항목 V2 (FeignClient 기반) V3 (Kafka 기반)
처리량 246.4건/초 346.9건/초 (1.5배↑)
응답 블로킹 전체 흐름이 지연에 취약 비동기 처리로 블로킹 제거
사용자 응답 속도 상대적으로 느림 대기 시간 감소로 향상
오류율 낮음 (정상 처리 유지) 동일한 오류율 유지 (99.9%)
 

👉 동기 호출로 인해 발생하던 응답 지연 및 전체 흐름 블로킹 문제를 Kafka 도입을 통해 해소했으며,
결과적으로 처리량은 약 1.5배 향상, 사용자 체감 속도도 대폭 개선되었습니다.

- Kafka 기반 구조 전환 이후: SAGA 패턴을 통한 데이터 정합성 보장

Kafka 기반 이벤트 처리 구조로 전환한 것은 단순한 메시징 시스템 도입을 넘어서
서비스 간 느슨한 결합과 함께 분산 환경에서의 데이터 정합성을 실질적으로 보장하기 위함이었습니다.
이를 위해 우리는 Kafka 메시지를 기반으로 하는 SAGA 패턴을 설계하고 적용했습니다.

1. 왜 SAGA 패턴이 필요한가?

티켓 예매와 결제 시스템에서는 다음과 같은 시나리오가 자주 발생합니다:

  • 결제는 실패했지만 좌석은 이미 예약된 상태로 남아있는 경우
  • 예매 도중 오류가 발생해 티켓은 생성되지 않았지만 좌석은 선점된 상태인 경우
  • 예매 시간이 초과되었지만 좌석이 자동으로 풀리지 않아 사용자가 다시 시도할 수 없는 경우

이러한 상황은 마이크로서비스 아키텍처에서 서비스 간 직접적인 트랜잭션 공유가 불가능하기 때문에 정합성 유지에 매우 취약할 수 있습니다.
우리는 이 문제를 해결하기 위해 보상 트랜잭션 기반의 SAGA 패턴을 이벤트 흐름 안에 통합하였습니다.

 

2. 전체 SAGA 흐름 요약

Kafka 메시지를 기반으로 서비스 간 상태 변화를 트래킹하며 각 단계에서 실패하거나 취소되는 경우에는 이전 상태로 복원하는 보상 메시지를 발행합니다.

 

[결제 요청 → 예매 완료] 흐름 예시

PAYMENT (payment_create)
   ↓ Kafka
TICKET (ticket_confirm)
   ↓ Kafka
SEAT (seat_confirm)
   ↓ Kafka
NOTIFICATION (예매 완료 안내 전송)
  • 정상 플로우에서는 각 단계별 상태가 확정되며 최종적으로 알림이 발송됩니다.

[결제 실패 → 좌석/티켓 복구] 보상 흐름 예시

PAYMENT (payment_fail)
   ↓ Kafka
TICKET (ticket_cancel)
   ↓ Kafka
SEAT (seat_cancel)
   ↓ Kafka
NOTIFICATION (예매 실패 안내 전송)

 

  • 실패 발생 시 자동으로 보상 트랜잭션이 순차적으로 실행되며,
  • 좌석은 다시 AVAILABLE 상태로 복원되고 티켓은 생성되지 않습니다.

3. 구현 포인트

  • 각 서비스는 Kafka 메시지를 수신하고, 본인의 상태만 처리
  • 메시지 구조는 GenericKafkaEvent<T> 형태로 일관성 유지
  • DLQ(Dead Letter Queue)를 통해 실패 메시지 추적 가능
  • Redis TTL 기반으로 좌석 만료 복구 → seat_expired 이벤트 발행
@KafkaListener(topics = "ticket_cancel")
public void consumeCancel(String message) {
    // Ticket 취소 시 seat 복구
    seatService.revertSeats(...);
}

 

또한 좌석 보상 처리 외에도 예매 만료 시 자동 복구 로직도 포함되어 있어 시간 초과로 인한 선점 좌석도 정상적으로 AVAILABLE 상태로 돌아갑니다.

 

4. Kafka + SAGA의 효과

SAGA 패턴을 적용한 후 다음과 같은 운영상 안정성과 복원력을 확보할 수 있었습니다.

항목 적용 전 적용 후
장애 발생 시 좌석 상태 수동 복구 필요 자동 복구 (seat_cancel)
트랜잭션 보장 서비스 단위 처리 이벤트 체인 기반 보상 처리
사용자 경험 예외 발생 시 오류 실패 상황 안내 및 자동 좌석 반환
확장성 서비스 간 직접 호출로 제한 메시지 기반 유연한 흐름 가능

5. 마무리 요약

Kafka 기반 구조 전환은 단순한 기술 변경이 아닌 서비스 안정성, 확장성, 고가용성을 확보하기 위한 필수 선택이었습니다.

Kafka 기반 구조를 도입한 후 SAGA 패턴을 활용한 보상 트랜잭션 흐름까지 적용함으로써 다음을 실현할 수 있었습니다.

  • ✅ 서비스 간 독립성과 느슨한 결합 유지
  • ✅ 실패 복구 및 상태 정합성 확보
  • ✅ 사용자에게 더 안정적인 예매 경험 제공

현재는 Kafka 기반 단순 이벤트 발행/소비 구조를 도입한 상태이며,
향후 다음 기능도 점진적으로 적용할 예정입니다.

  • 📦 Outbox 패턴 도입 → DB 트랜잭션과 메시지 발행의 원자성 보장
  • 🔁 이벤트 재처리 및 재시도 정책 구성 → DLQ 외 복구 전략 고도화
  • 🧪 Schema Registry 및 메시지 검증 → 서비스 간 안정성 강화

이 구조는 단순한 기술 적용을 넘어서 신뢰할 수 있는 분산 시스템 아키텍처의 실전 예시로 활용될 수 있습니다.
티켓팅처럼 고부하 환경에서도 안전한 트랜잭션 흐름을 만들고자 하는 분들께 도움이 되길 바랍니다.