Outbox Pattern과 멱등성(Idempotency)

null·2026년 8월 6일

개발지식

목록 보기
3/10

백엔드 면접 질문 #2 - Outbox Pattern과 멱등성(Idempotency)

❓ 면접 질문

DB 저장은 성공했는데 Kafka 전송이 실패하면 어떻게 처리하시겠습니까?


문제 상황

MSA 환경에서는 하나의 작업이 여러 서비스와 연결되는 경우가 많다.

예를 들어 주문 서비스가 있다고 가정해보자.

사용자가 주문을 하면

  1. 주문 정보를 DB에 저장한다.
  2. Kafka에 주문 완료 이벤트를 발행한다.
  3. 배송 서비스, 쿠폰 서비스, 알림 서비스가 해당 이벤트를 소비한다.

흐름은 다음과 같다.

주문 요청

↓

DB 저장

↓

Kafka 이벤트 발행

↓

배송 / 쿠폰 / 알림 서비스

어떤 문제가 발생할 수 있을까?

다음과 같은 코드가 있다고 가정하자.

@Transactional
public void order(Order order){

    orderRepository.save(order);

    kafkaTemplate.send("order-topic", order);

}

겉보기에는 문제가 없어 보인다.

하지만 다음과 같은 상황이 발생할 수 있다.

DB 저장 성공

↓

DB Commit 완료

↓

서버 다운

↓

Kafka 전송 실패

이 경우

  • 주문은 DB에 저장되어 있다.
  • 배송 서비스는 주문을 모른다.
  • 쿠폰도 발급되지 않는다.
  • 알림도 발송되지 않는다.

즉 데이터 정합성이 깨진다.


try-catch로 해결할 수 있을까?

많은 사람들이 이렇게 생각한다.

try{

    orderRepository.save(order);

    kafkaTemplate.send(...);

}catch(Exception e){

    rollback();

}

하지만 이것으로는 해결되지 않는다.

왜냐하면

DB Commit 완료

↓

서버 종료

가 되면

catch 자체가 실행되지 않는다.

이미 트랜잭션은 끝났기 때문이다.

즉

DB Commit 이후 발생한 장애는 try-catch로 복구할 수 없다.


Outbox Pattern이란?

이 문제를 해결하기 위해 사용하는 패턴이 Outbox Pattern이다.

핵심 아이디어는

Kafka에 바로 보내지 말고, 먼저 이벤트를 DB에 저장하자는 것이다.

기존 구조

주문 저장

↓

Kafka 전송

↓

Outbox Pattern

주문 저장

+

Outbox 저장

↓

같은 트랜잭션으로 Commit

↓

별도 Worker가 Kafka 전송

실제 저장되는 데이터

orders 테이블

idproduct
1MacBook

outbox 테이블

event_idevent_typestatus
1001ORDER_CREATEDREADY

두 테이블은 같은 DB에 존재한다.

따라서

@Transactional
public void order(){

    orderRepository.save(order);

    outboxRepository.save(event);

}

처럼 하나의 트랜잭션으로 Commit할 수 있다.


그 다음은?

별도의 Worker(또는 Scheduler)가

주기적으로 Outbox를 조회한다.

READY

↓

Kafka 전송

↓

성공

↓

SENT 변경

즉

Kafka 전송은 트랜잭션 밖에서 수행된다.


서버가 중간에 죽으면?

예를 들어

주문 저장

↓

Outbox 저장

↓

Commit

↓

서버 다운

되었다고 가정해보자.

괜찮다.

왜냐하면

Outbox에는

ORDER_CREATED

READY

가 그대로 남아 있기 때문이다.

서버가 다시 살아나면

READY 상태를 다시 읽어서 Kafka에 전송하면 된다.

즉 이벤트가 유실되지 않는다.


멱등성(Idempotency)이란?

멱등성이란

같은 요청을 여러 번 수행해도 결과는 한 번 수행한 것과 같아야 하는 성질이다.


왜 필요한가?

Kafka는 기본적으로

At-Least-Once Delivery를 사용한다.

즉

최소 한 번은 보낸다.

를 보장한다.

하지만

Kafka 전송 성공

↓

응답 받기 전에 네트워크 끊김

이라면

Producer는

보냈나?

안 보냈나?

를 알 수 없다.

그래서 안전하게 한 번 더 보낸다.

그러면

ORDER-100

ORDER-100

처럼 같은 이벤트가 두 번 전달될 수 있다.


멱등성이 없으면?

배송 서비스가

ORDER-100

↓

배송 생성

했는데

같은 이벤트가 다시 오면

ORDER-100

↓

배송 생성

배송이 두 개 생성된다.


멱등성이 있다면?

Consumer는 eventId를 저장한다.

예를 들어

processed_event

event_id
1001

다음과 같이 구현한다.

if(processedEvent.exists(eventId)){
    return;
}

배송 생성

processedEvent.save(eventId);

이미 처리한 event라면

그냥 무시한다.

즉

같은 이벤트가 10번 와도

한 번만 처리된다.


왜 일부러 중복을 허용할까?

분산 시스템에서는

중복은 제거할 수 있지만, 유실은 복구하기 어렵다.

그래서 대부분의 메시지 시스템은

At-Least-Once Delivery

전략을 사용한다.

그리고 Consumer가

멱등성을 구현하여 중복을 제거한다.


핵심 정리

기존 방식Outbox Pattern
DB 저장 후 Kafka 전송DB에 이벤트 저장 후 Kafka 전송
장애 발생 시 이벤트 유실 가능이벤트 유실 방지
try-catch로 해결 불가Outbox 재전송 가능

멱등성 없음멱등성 있음
같은 이벤트 여러 번 처리한 번만 처리
중복 데이터 발생중복 방지

면접 답변

DB Commit 이후에는 try-catch로 복구할 수 없습니다. 그래서 주문 데이터와 이벤트를 Outbox 테이블에 같은 트랜잭션으로 저장합니다. 이후 별도의 Worker가 Outbox를 읽어 Kafka에 전송합니다. Kafka는 At-Least-Once Delivery를 사용하기 때문에 이벤트가 중복 전달될 수 있으며, Consumer에서는 eventId 등을 이용한 멱등성을 구현하여 동일한 이벤트가 여러 번 와도 한 번만 처리하도록 합니다.

0개의 댓글