트랜잭셔널 아웃박스 패턴 🎯
왜 필요할까? 🤔
온라인 쇼핑몰을 운영한다고 생각해보세요! 고객이 주문을 완료하면:
- 주문 정보를 DB에 저장 💾
- 결제 시스템에 알림 💳
- 재고 시스템에 알림 📦
- 이메일 발송 📧
이 모든 작업이 동시에 성공해야 하는데, 만약 주문은 저장됐는데 결제 시스템 알림이 실패하면? 😱 고객은 돈만 빠져나가고 상품은 못 받는 상황이 발생할 수 있어요!
기존 방식의 문제점 💥
// 위험한 코드 예시
async function processOrder(orderData) {
await db.orders.create(orderData); // DB 저장 성공
await paymentService.notify(orderData); // 갑자기 네트워크 오류! 💀
}
이런 상황을 이중 쓰기 문제라고 해요. 한쪽은 성공하고 한쪽은 실패하는 거죠!
트랜잭셔널 아웃박스 패턴 해결책 ✨
1단계: 아웃박스 테이블 만들기 📋
일반 데이터와 함께 "할 일 목록"도 같은 DB에 저장해요!
// 안전한 방식
async function processOrder(orderData) {
const transaction = await db.transaction();
// 같은 트랜잭션 안에서
await transaction.orders.create(orderData);
await transaction.outbox.create({
eventType: 'ORDER_CREATED',
payload: orderData,
status: 'PENDING'
});
await transaction.commit(); // 둘 다 성공하거나 둘 다 실패!
}
2단계: 백그라운드 워커 실행 🔄
별도 프로세스가 주기적으로 아웃박스를 확인하고 처리해요!
실생활 비유 📮
우체통 시스템과 비슷해요!
- 편지 쓰기 = 데이터 저장
- 우체통에 넣기 = 아웃박스에 이벤트 저장
- 우체부가 수거 = 백그라운드 워커가 처리
편지를 쓰고 우체통에 넣는 건 한 번에 할 수 있지만, 실제 배달은 우체부가 나중에 처리하죠! 📬
추가로 알아두면 좋은 것들 💡
장점
- 데이터 정합성 보장 ✅
- 최종 일관성 달성 🎯
- 시스템 복원력 향상 💪
단점
- 복잡성 증가 📈
- 지연 발생 ⏰ (즉시 처리 X)
- 중복 처리 가능성 🔄
실제 구현 팁
- 멱등성 보장 (같은 작업을 여러 번 해도 결과 동일)
- 재시도 로직 구현
- 데드레터큐 활용으로 실패 처리
Next.js에서 활용 예시 🚀
API 라우트에서 주문 처리 후, 백그라운드 작업으로 알림 발송하는 방식으로 많이 사용해요!
면접 답변 💼
"트랜잭셔널 아웃박스 패턴은 데이터베이스 저장과 외부 시스템 호출을 동시에 해야 할 때 발생하는 이중 쓰기 문제를 해결하는 패턴입니다. 핵심은 이벤트 정보를 별도 아웃박스 테이블에 같은 트랜잭션으로 저장한 후, 백그라운드 프로세스가 주기적으로 처리하여 데이터 정합성을 보장하는 것입니다. 즉시성은 포기하지만 최종 일관성과 시스템 안정성을 얻을 수 있는 트레이드오프 관계입니다."
트랜잭셔널 아웃박스 패턴에 대해서 설명해주세요.
트랜잭셔널 아웃박스 패턴(Transactional Outbox Pattern) 은 분산 시스템에서 단일 작업에 데이터베이스 쓰기 작업과 메시지 혹은 이벤트 발행이 모두 포함된 경우 발생하는 이중 쓰기 문제를 해결하기 위해서 사용할 수 있습니다. 예를 들어, 다음과 같은 코드가 존재한다고 가정하겠습니다.
@Transactional
public void propagateSample() {
Product product = new Product("신규 상품");
productRepository.save(product);
eventPublisher.propagate(new NewProductEvent(product.getId()));
}
위와 같이 신규 상품을 생성하고, 이벤트를 발행하는 코드를 트랜잭션 AOP 로직이 적용된 간단한 의사코드로 작성한다면 다음과 같을 텐데요.
public void doInTransaction() {
try {
transaction.begin();
Product product = new Product("신규 상품");
productRepository.save(product);
eventPublisher.propagate(new NewProductEvent(product.getId()));
transaction.commit();
} catch(Exception e) {
transaction.rollback();
}
}
위와 같은 코드에서 트랜잭션은 커밋됐지만 이벤트 발행은 실패할 수 있으며, 반대로 이벤트 발행은 성공했지만 커밋 연산이 모종의 이유로 실패하여 트랜잭션은 롤백 될 수 있습니다. 이러한 이중 쓰기로 인해 발생하는 문제는 전체 서비스의 데이터 정합성에 문제를 만들거나 서비스 장애로 이어질 수 있습니다. 이 문제를 해결하기 위해서 서비스 로직의 실행과 이벤트 발행을 원자적으로 함께 수행하는 것을 트랜잭셔널 메시징(Transactional Messaing) 이라고 하며, 트랜잭셔널 아웃박스 패턴의 사용 이유기도 합니다.
@Transactional
public void propagateSample() {
Product product = new Product("신규 상품");
productRepository.save(product);
productOutboxRepository.save(new ProductEvent(product.getId()));
}
Product 발행 이벤트를 저장하기 위한 Outbox 테이블을 만들고, 같은 트랜잭션 내부에서 이벤트를 저장합니다. 원자성을 보장해 주는 데이터베이스 트랜잭션을 사용하기 때문에 이벤트와 신규 상품은 모두 저장되거나, 모두 저장에 실패합니다. 그리고, 별도의 프로세스가 Outbox 테이블에 저장된 레코드들을 주기적으로 폴링하여 외부 시스템에 성공할 때까지 이벤트를 발행하는 것이 트랜잭셔널 아웃 박스 패턴의 기본적인 구현 방식입니다.
'1일 1CS(Computer Science)' 카테고리의 다른 글
CORS 설정 없이 SOP를 우회하여 외부 서버와 통신할 수 있는 방법이 있을까요? (4) | 2025.06.25 |
---|---|
CSRF 공격에 대해서 설명해주세요. (3) | 2025.06.24 |
css box-sizing 속성에 대해 설명해주세요. (0) | 2025.06.24 |
NAT 기능을 사용하는 이유를 알고 계신가요? (3) | 2025.06.23 |
알고 있는 이미지 포맷과 각 포맷별 특징을 알려주세요. (4) | 2025.06.23 |