개발지식/Backend Engineering

Kafka Outbox 패턴으로 비동기 이벤트 발행하기

우루쾅 2026. 1. 31. 10:45
728x90
반응형
SMALL

비동기 처리 방식을 공부하면서 Kafka 통신 흐름을 로그를 통해 확인하면서

동작 원리를 파악하는 과정에서 깨달음을 얻어 게시글을 작성합니다!

출처 : https://bit.ly/3CRqCeb

Kafka 를 처음 사용하면서 저는 Kafka 로 이벤트를 발행할 때 이게 언제, 어디서, 어떤 흐름으로 실행되는건지 헷갈렸습니다.

특히 Outbox 패턴을 적용하면 Kafka 발행 로직이 Controller 와 분리되기 때문에

요청이 끝났음에도 Kafka 가 동작하는 것을 알 수 있는데요

 

이를 기반으로 아래 내용들을 정리해보겠습니다!

  • HTTP 요청 처리 흐름
  • Filter / Interceptor / Service의 역할
  • Kafka Outbox 발행이 Controller와 분리되는 이유
  • 로그로 Kafka 통신을 확인하는 방법
  • 실무에서는 Outbox를 어떻게 활용하는지

 

1. 전체 구조 한눈에 보기

[Client]
   ↓
[Filter]  → correlationId 생성 (MDC)
   ↓
[Interceptor] → 요청 시간 측정
   ↓
[Controller] → 주문 요청
   ↓
[Service @Transactional]
   ├─ Order 저장
   └─ OutboxEvent 저장 (status = NEW)
   ↓ (트랜잭션 종료)
[Scheduler]
   ↓
[Kafka 발행]
   ↓
OutboxEvent status = SENT

 

핵심은 Kafka 발행은 HTTP 요청 흐름과 완전히 분리되어있다는 것입니다!

 

2. HTTP 요청 로그로 보는 흐름 분석

2-1 주문 요청 시 로그

[FILTER] start method=POST, uri=/orders
[INTERCEPTOR] preHandle uri=/orders
[INTERCEPTOR] afterCompletion uri=/orders, duration=724ms
[FILTER] end uri=/orders

 

이 로그는 제가 공부를 하면서 Filter 와 Interceptor 를 탈때마다 log 를 찍어놓은 건데요(Controller와 Service 는 제외)

해당 로그를 보자면

 

  • Filter → Interceptor(preHandle) → Controller/Service → Interceptor(afterCompletion) → Filter
  • 요청이 끝나면 Interceptor afterCompletion
  • 그리고 Filter 종료

👉 이 시점에 HTTP 요청은 이미 완전히 끝났다.

 

2.2 Kafka Outbox 발행 로그

[scheduling-1] INFO  OutboxPublisher - [OUTBOX] sent topic=order.created, outboxId=4, aggregateId=9

 

 

이 로그의 포인트는 두가지 입니다.

 

① 스레드 이름

[scheduling-1]

 

→ HTTP 요청 스레드(http-nio-8080-exec-*)가 아님
→ @Scheduled로 실행된 백그라운드 스레드

 

② correlationId 없음

[cid=]

 

→ HTTP 요청 컨텍스트(MDC)와 완전히 분리됨

 

즉,

Kafka 발행은
Controller를 전혀 거치지 않고,
Filter / Interceptor도 타지 않는다.

 

3. 왜 Kafka 발행을 Service 에서 바로 안할까?

3.1 단순 방식(권장 X)

orderRepository.save(order)
kafkaTemplate.send("order.created", ...)

 

이 방식의 문제점은 Kafka 발행은 성공할 수 있으나 DB 트랜잭션 Rollback이 안됩니다.

또한 DB 저장은 성공할 수 있으나 Kafka 발행이 실패하며, 데이터 정합성이 깨지게 됩니다.

 

3.2 Outbox 패턴 방식(권장!)

@Transactional
fun createOrder() {
    orderRepository.save(order)
    outboxRepository.save(
        OutboxEvent(status = NEW)
    )
}

 

여기에 별도 스캐줄러

@Scheduled
fun publishNewEvents() {
    NEW 상태 이벤트 조회
    Kafka 발행
    성공 → SENT
    실패 → FAILED
}

 

위 방식은 DB 트랜잭션과 이벤트 기록을 먼저 일관되게 보장합니다.

또한 Kafka 는 결국 재시도 가능한 외부 전송을 하게 됩니다.

 

4. Outbox 조회 메서드

fun findByStatusOrderByCreatedAtAsc(
    status: OutboxStatus,
    pageable: Pageable
): List<OutboxEvent>

 

이 메서드는 이렇게 동작합니다.

  • status = NEW 인 이벤트만 조회
  • createdAt ASC → 오래된 것부터
  • PageRequest.of(0, 20) → 한 번에 최대 20건

중요한 포인트는 페이지를 쌓는 개념이 아닌 NEW 상태를 천천히(20개씩) 줄여나가는 방식 입니다.

처리된 이벤트는 SENT 로 바뀌기 때문에 다음 스캐줄에서는 다음 이벤트 묶음을 가져오게 됩니다.

 

5. Kafka 통신 확인 방법

5.1 애플리캐이션 로그

[OUTBOX] sent topic=order.created, outboxId=4, aggregateId=9

 

  • .get()으로 동기 대기
  • Kafka ACK를 받은 후에만 SENT 처리
  • 실패 시 FAILED 로그 출력

6.2 Kafka 콘솔 컨슈머로 직접 확인

kafka-console-consumer \
  --bootstrap-server localhost:9092 \
  --topic order.created \
  --from-beginning

 

  • 실제 메시지가 Kafka에 저장되고, 컨슈머가 읽을 수 있음을 확인

 

정리

  • Kafka는 HTTP 요청과 분리된 비동기 메시징 시스템
  • Outbox 패턴은 트랜잭션 정합성 문제를 해결한다
  • Kafka 발행은 Controller를 거치지 않아도 된다
  • 로그를 보면 스레드 이름만으로도 흐름을 추적할 수 있다
Kafka 는 요청 처리의 일부가 아닌 시스템 간 상태 전달을 위한 독립된 흐름 입니다!

 

Outbox 패턴은 그 경계를 명확하게 만들어주며, 그 과정에서는 Filter나 Interceptor 를 타지 않습니다.

 

참조

  https://www.youtube.com/watch?v=IjI4DJvZcAs

https://bit.ly/3CRqCeb

https://yeon-kr.tistory.com/178

 

 

728x90
반응형
LIST