프리비는 촬영 예약 및 고객 관리 통합 플랫폼 서비스이다. 현재는 촬영 예약을 희망하는 고객이 촬영 예약/취소를 하면 서버에서 해당 요청을 처리한 뒤, 곧바로 카카오 알림톡을 발송해 사진작가에게 실시간으로 알림을 주고 있다.
하지만 현재 구조는 API 요청 처리와 알림톡 발송이 하나의 동기적 흐름에 묶여 있다는 한계가 있다. 예를 들어 알림톡 API가 순간적으로 지연되거나 실패하면, 그 영향이 고스란히 사용자 경험으로 이어진다. 예약은 정상적으로 되었음에도 응답이 늦어지거나 에러가 나는 경험을 사용자에게 주게 되는 것이다.
이에 대한 고민을 하던 중, 최근 우아한형제들 기술블로그 「회원시스템 이벤트 기반 아키텍처 구축하기」 글을 읽으며 여러가지 인사이트를 얻었다. 특히, 서비스 간 강결합을 끊는 법과 도메인 이벤트 자체를 발행하는 방식 등에서 생각해 볼 점이 많았다.
이번 글에서는 얻은 아이디어를 바탕으로, 프리비 프로젝트에 적용하고자 하는 설계 방향을 정리해 본다.
1. 강결합 문제 해소
현재 구조는 예약 처리 로직이 알림톡 발송을 직접 호출하는 구조다. 이는 비즈니스 로직과 외부 API 호출이 강하게 연결되어 있음을 의미한다.

이벤트 기반 구조에서는 예약 처리 서비스는 단지 예약이 발생함/취소됨이라는 사건만 발행한다. 그 사건을 받아 알림톡을 보낼지 말지를 판단하는 건 알림 서비스의 책임이다.
이처럼 각 서비스의 역할과 책임을 분리하는 구조에서는 예약과 알림은 느슨한 결합을 갖게 된다.
2. 안정성 확보
현재 구조에서는 알림톡 API가 지연되거나 실패하면, 그 영향이 예약 API의 응답 지연이나 실패로 이어져 사용자에게 좋지 않은 경험을 주게 된다.
반면, 이벤트 기반 구조에서는 알림톡 전송과 같은 부가적인 작업을 메인 로직과 분리하여 비동기적으로 처리할 수 있다. 따라서 알림톡 시스템에 장애가 발생하더라도, 예약과 같은 핵심 기능은 빠르게 응답할 수 있어 사용자에게 항상 안정적인 서비스를 제공할 수 있게 된다.
3. 확장성
한 번 발행된 이벤트는 알림톡 외에도 여러 곳에서 소비할 수 있다.
예를 들어 reservation.created 이벤트는 알림 서비스에서는 “예약 확정 알림”을 보낼 수 있고, 통계 서비스에서는 “일별 예약 건수 증가”를 계산할 수 있다.
즉, 이벤트는 한 번만 기록하면 다양한 기능에서 재사용할 수 있는 자원이 된다.
우아한형제들 기술블로그에서는 이벤트를 크게 3단계 레이어로 나눠 설명한다.
이를 우리 서비스에 맞게 재구성하면 다음과 같다.
1. 도메인 이벤트 레이어
2. 아웃박스(Outbox) 레이어
dual-write를 방지한다.3. 소비자(Consumer) 레이어
이를 적용한 구조를 시퀀스 다이어그램으로 표현해 봤다.
Kafka, RabbitMQ, SQS 같은 메시지 브로커를 쓰면 더 강력한 이벤트 스트리밍 환경을 만들 수 있다. 하지만 우리 프로젝트는 추가 인프라나 비용 없이 구축하는 것이 필요했다.
그래서 선택한 방법이 DB Outbox + 스케줄러 워커 패턴이다.
Outbox:
Outbox는 트랜잭션의 원자성을 활용해서 데이터베이스의 상태 변경과 이벤트 발행을 하나의 논리적 단위로 묶고자 하는 전략이다. 이는 예약이 정상적으로 발생/취소 되었음에도 이벤트 기록이 누락되는 문제를 방지할 수 있다.
이때 Outbox 테이블에 단순히 이벤트 메시지만 저장하지 않고, 이벤트의 상태를 관리하는 필드를 포함시키면 스케줄러 패턴과 융합해서 이벤트 처리 실패(ex: 네트워크 오류) 시에도 대응할 수 있다.
status (READY/SENT/FAILED):
retry_count:
next_attempt_at:
스케줄러 워커:
스케줄러 워커는 Outbox 테이블에 기록된 이벤트를 읽어 후속 작업을 처리하는 역할을 담당한다.
주기적인 폴링(Polling):
비동기적 처리:
성공/실패 처리:
retry_count를 1 증가시키고 next_attempt_at 필드를 갱신하여 지수 백오프 전략을 적용한다. 이벤트는 다시 READY 상태로 두어 다음 주기 때 재시도를 유도한다. 만약 재시도 횟수가 특정 임계값(예: 5회)을 초과하면 status를 FAILED로 변경하여 별도 로그 분석 및 수동 처리가 가능하도록 한다.아웃박스 레이어의 필요성에 대해 설명해보고자 한다.
@TransactionalEventListener + @Async + @Retryable 등으로 리스너에서 바로 알림톡을 호출할 경우, 운영·내구성 관점에서 아래와 같은 한계가 남는다.
1. 트랜잭션과 내구성
AFTER_COMMIT으로 트랜잭션 오염은 피할 수 있지만, 커밋 직후 서버가 다운되는 등의 장애 상황에서는 이벤트가 유실될 수 있다.
Outbox는 비즈니스 변경과 이벤트 기록을 같은 트랜잭션에 남겨 유실을 방지한다.
2. 재시도·백오프의 지속성
리스너의 @Retryable은 프로세스 메모리 안에서 즉시 재시도에 가깝다. 배포 혹은 서버 재시작 시에는 상태가 사라지기 때문에 상태 추적이 어렵다는 한계가 있다.
Outbox는 retry_count/next_attempt_at/status 등을 DB에 레코드로 남기므로, 스케줄러와 함께 사용 시 지속적이고 가시적인 재시도를 제공한다.
결론적으로, 리스너는
이벤트를 기록하도록 트리거하는 곳까지만 맡기고, 실제 외부 호출은Outbox 워커가 담당하도록 분리하는 편이 더 안전하고 운영 친화적이라 판단했다.
이번 글에서는 우아한형제들의 사례를 참고해, 프리비에 이벤트 기반 아키텍처를 적용하기 위한 설계 아이디어를 정리했다.
핵심은 “도메인 로직과 외부 작업을 분리하고, 이벤트 자체를 기록한다”는 점이다.
후속편에서는 실제로 이 설계를 코드로 옮기면서, 어떤 어려움이 있었고 어떤 선택을 했는지 등을 공유해 볼 예정이다.