결제 시스템 재설계 (2) - SQS를 걷어내고 동기 API로
이벤트 드리븐의 매력에 빠져 도입한 SQS였지만, 결제 도메인에서는 오히려 복잡도만 높였다. SQS를 제거하고 동기 API + Portone 웹훅 기반으로 전환하면서 결제 프로세스를 안정화한 이야기.
1편 요약
1편에서는 PHP 레거시 결제 시스템을 Java/Spring + SQS 기반 이벤트 드리븐 아키텍처로 전환한 과정을 다뤘다. 결과적으로 PHP 탈출과 중복 결제 방지에는 성공했지만, SQS 기반 구조에서 예상치 못한 문제들이 드러났다. 결제 → 티켓 발급 → 알림이 각각 다른 SQS 메시지로 처리되어 전체 흐름 추적이 어려움 SQS 발행과 DB 트랜잭션이 분리되면서 정합성 문제 발생 디버깅 시 SQS 로그, 애플리케이션 로그, DB 상태를 크로스 체크해야 하는 복잡도
이 글에서는 SQS를 걷어내고 동기 API 방식으로 전환한 과정을 다룬다.
SQS가 만든 문제들
트랜잭션 경계가 깨진다
SQS 기반 구조의 가장 큰 문제는 트랜잭션 경계였다. 결제 처리 흐름을 다시 보면:
DB에 결제 정보를 저장하고, SQS에 다음 단계 메시지를 발행하는데 - 이 두 작업이 하나의 트랜잭션이 아니다. DB 커밋은 성공했는데 SQS 발행이 실패하면? 결제 정보는 저장됐지만 티켓 발급은 영원히 안 된다. 반대로 SQS 발행은 성공했는데 DB 롤백이 일어나면? 존재하지 않는 결제에 대해 후처리가 실행된다.
디버깅의 악몽
결제 관련 CS가 들어오면 이런 식이었다: 결제 건의 merchantUid로 결제 테이블 조회 SQS 메시지가 정상 발행됐는지 Grafana 로그 확인 PAYMENT_DATA 메시지가 소비됐는지 Slack 알림 확인 티켓 발급이 됐는지 티켓 테이블 조회 PAYMENT_SCHEDULE 메시지로 다음 결제가 예약됐는지 확인
하나의 결제 건을 추적하는 데 여러 시스템을 넘나들어야 했다. 결제 도메인에서 비동기는 오버 엔지니어링이었다.
설계: 동기 API + Portone 웹훅
핵심 결정
SQS를 제거하고, Portone 웹훅이 백엔드를 직접 호출하는 구조로 전환했다.
SQS 때와 뭐가 다른가
| | SQS 기반 (1차) | 동기 API (2차) | |||| | 진입점 | BFF → SQS 메시지 | Portone 웹훅 → Backend API | | 트랜잭션 | 메시지 단위 (느슨) | 요청 단위 (단일 트랜잭션) | | 처리 지연 | 큐 지연 있음 | 즉시 처리 | | 에러 처리 | 비동기 재시도 | 트랜잭션 롤백 | | 디버깅 | 멀티 시스템 로그 | 하나의 콜스택 | | 결제 타입 분기 | if-else 체인 | Strategy 패턴 |
가장 큰 변화는 모든 결제 처리가 하나의 트랜잭션 안에서 완결된다는 점이다. 결제 검증, 티켓 생성, 구독 매핑, 알림 발송이 전부 하나의 HTTP 요청 안에서 처리되고, 하나라도 실패하면 전부 롤백된다.
웹훅 기반 결제 흐름
프론트엔드: 웹훅 URL 생성
클라이언트에서 결제를 시작할 때, 백엔드 웹훅 URL을 미리 구성해서 Portone SDK에 전달한다.
결제 유형, 사용자 ID, 구독 ID, 쿠폰 ID 등 후처리에 필요한 정보를 웹훅 URL의 쿼리 파라미터에 실어 보낸다. Portone이 결제를 완료하면 이 URL로 웹훅을 쏜다.
프론트엔드: 폴링으로 결과 확인
웹훅은 백엔드로 직접 가기 때문에, 프론트엔드는 결제 결과를 폴링으로 확인한다.
이전에는 BFF가 SQS에 메시지를 보내고, SQS가 백엔드로 전달하는 구조였다. 지금은 Portone이 백엔드에 직접 웹훅을 보내니까, BFF의 역할이 대폭 줄었다.
백엔드: 웹훅 수신 및 처리
웹훅 엔드포인트에서는 먼저 웹훅 타입을 필터링한다.
Portone은 결제 상태가 변할 때마다 웹훅을 보낸다. 결제창을 열었을 때(Transaction.Ready), 부분 취소(Transaction.PartialCancelled) 등은 우리가 처리할 필요가 없으므로 걸러낸다.
필터를 통과하면 Redis 분산 락을 걸고 동기적으로 결제를 처리한다.
Strategy 패턴으로 결제 타입 분리
1차 리팩토링의 if-else 지옥
SQS 기반 1차 구조에서는 결제 타입별 후처리가 거대한 if-else 체인이었다.