비동기 처리 방식을 공부하면서 Kafka 통신 흐름을 로그를 통해 확인하면서
동작 원리를 파악하는 과정에서 깨달음을 얻어 게시글을 작성합니다!

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://yeon-kr.tistory.com/178
'개발지식 > Backend Engineering' 카테고리의 다른 글
| 비동기 처리란 무엇인가?(MQ / Kafka) (0) | 2026.01.23 |
|---|---|
| 전략 패턴 + AOP로 정책 로직을 분리한 설계 (0) | 2025.12.28 |
| 🌿 Spring Data JPA – 반환 타입 정리 (0) | 2025.11.13 |
| 📚 Spring Data JPA Repository 제대로 써보기 (0) | 2025.11.12 |
| Kotlin 기본 구조(예시 코드) (8) | 2025.08.14 |