[공식 모듈] 짓미 페이를 따로 떼어낸 이유, 그리고 작동 원리
안녕하세요. zittme 개발팀입니다.
이번 개발자 노트에서는 zittme 페이 모듈이 어떻게 작동되는 것인지 좀 더 구체적으로 소개해드리려 합니다.
왜 결제(페이)를 따로 떼어냈나
커머스에도 결제가 필요하고, 예약에도 결제가 필요합니다. 그런데 두 모듈에 각각 결제를 넣으면 같은 코드를 두 번 만들게 됩니다. 문제는 중복 그 자체가 아닙니다. PG를 하나 추가할 때마다 두 곳을 고쳐야 하고, 그중 한 곳만 고치면 그 사실을 아무도 모른다는 게 진짜 문제입니다. 결제는 틀렸을 때 돈이 잘못 움직이는 영역이라, 이런 종류의 어긋남을 구조적으로 막아야 합니다.
그래서 결제를 별도 모듈로 분리하고, 결제 대상을 모듈명 + 주문번호 두 값으로만 가리키게 했습니다. 이 두 값 말고는 페이 모듈이 요청자에 대해 아는 것이 없습니다. 상품도, 옵션도, 장바구니도 모릅니다. 덕분에 커머스든 예약이든 처음 보는 서드파티 모듈이든 똑같이 붙습니다.
작동원리 소개

1. 주문 생성
연동 모듈이 createOrder()를 부르면 결제주문이 ready 상태로 만들어지고 결제 URL이 나옵니다. 이때 넘기는 금액은 할인·쿠폰·포인트가 모두 적용된 최종 금액입니다. 페이 모듈은 금액을 계산하지 않습니다 — 계산은 그 주문의 규칙을 아는 연동 모듈의 몫입니다.
주문번호(order_code)는 추측할 수 없는 값으로 발급됩니다. 결제 화면에 들어가는 열쇠가 이 값이기 때문입니다.

2. 결제 화면
활성화되어 있고 키까지 채워진 결제수단만 화면에 나옵니다. 키를 넣지 않은 결제수단이 보이면 사용자가 결제 도중에 막히기 때문에, 아예 목록에서 제외합니다.
이 시점에 티켓(state) 이 발급됩니다. 뒤에서 설명할 세션 문제를 피하기 위한 장치입니다.
3. 결제수단별 분기
결제수단은 두 부류입니다.
- 결제창이 필요한 수단 (카드 등) — PG의 JS SDK를 띄우고, 사용자가 결제하면 PG가 우리 콜백으로 돌려보냅니다.
- 필요 없는 수단 (무통장입금) — 서버에서 그 자리에서 처리합니다.
이 차이를 드라이버가 스스로 답합니다(requiresClientPayment()). 그래서 컨트롤러에도, 연동 모듈에도 "무통장이면 이렇게, 카드면 저렇게" 같은 분기가 없습니다. 새 PG를 붙일 때 고칠 파일이 늘어나지 않는 이유입니다.
4. 승인
PG가 브라우저를 통해 콜백으로 돌려주는 값에는 결제 금액도 들어 있습니다. 이 값을 그대로 승인에 쓰면 안 됩니다. 브라우저를 거쳐 온 값은 사용자가 바꿀 수 있습니다.
그래서 승인 직전에 서버가 들고 있는 주문 금액과 대조하고, 다르면 승인 요청 자체를 보내지 않고 실패 로그를 남긴 뒤 중단합니다. PG에 보내는 금액도 언제나 서버 값입니다.
5. 상태 확정
콜백은 중복으로 도착할 수 있습니다. 사용자가 새로고침하거나, 웹훅이 겹치거나, PG가 재시도합니다.
이때 "지금 상태를 읽어서 → paid가 아니면 → paid로 바꾼다"로 짜면, 두 요청이 동시에 들어왔을 때 둘 다 통과합니다. 그래서 검사와 변경을 한 문장 안에서 합니다.
UPDATE zittme_pay_order SET status='paid', ...
WHERE order_srl = ? AND status IN ('ready','pending')
실제로 한 행이 바뀌었는지로 판정합니다. 1이면 내가 이긴 것이고, 0이면 이미 다른 요청이 처리한 것이라 로그만 남기고 조용히 물러납니다. 그래서 결제완료 통지는 언제나 한 번만 나갑니다.
부분취소 누적도 같은 방식입니다. 취소 금액을 코드에서 더하지 않고 DB 안에서 더하며, 남은 금액을 넘는 취소는 조건에서 걸러 초과 환불이 원천적으로 불가능합니다.
6. 웹훅
가상계좌 입금처럼 사용자의 브라우저와 무관하게 나중에 도착하는 통지가 있습니다. 웹훅 본문은 누구나 흉내 낼 수 있습니다. "결제 완료됐다"는 JSON을 아무나 우리 서버로 보낼 수 있다는 뜻입니다.
그래서 본문의 내용은 "무언가 일어났다"는 신호로만 쓰고, 실제 판단은 PG 조회 API로 다시 물어본 결과로만 합니다. 상태와 금액이 모두 맞을 때에만 반영합니다.
7. 결과 통지
승인이 확정되면 페이 모듈이 트리거를 발생시킵니다. 연동 모듈은 이걸 듣고 자기 일을 합니다. (커머스는 배송을 잡고, 예약은 자리를 확정합니다.)
페이 모듈이 커머스를 직접 부르지 않는 이유는 단순합니다. 페이 모듈은 커머스가 있는지도 몰라야 하기 때문입니다. 알기 시작하면 서드파티 모듈은 붙을 수 없습니다.
8. 구매확정과 환불
돈을 받은 뒤에도 되돌릴 일이 남습니다. 여기서도 원칙은 4번(승인)과 같습니다.
무엇을 얼마나 돌려줄지는 연동 모듈이 정하고, 실제로 돈을 되돌리는 일은 페이 모듈이 합니다.
구매확정 : 확정 조건은 연동 모듈마다 다릅니다. 커머스는 배송완료 후 며칠, 예약은 이용일이 지난 뒤입니다. 페이 모듈은 이 조건을 알지 못하고, 연동 모듈이 확정됐다고 알려주면 그때 상태만 기록합니다.
PayService::confirm($my_pay_order_srl); // 연동 모듈이 통보
확정된 결제는 취소 버튼이 잠깁니다. 판단은 여전히 연동 모듈이 하지만, 실수로 확정된 건을 취소하는 것을 막는 집행 단계의 안전장치는 페이 모듈이 갖습니다.
환불 금액 : 위약금, 반품비, 청약철회 기간 계산은 전부 연동 모듈의 몫입니다. 숙박 예약의 "3일 전 취소 시 50%"와 커머스의 "배송 후 반품비 차감"은 상품과 서비스를 알아야 계산할 수 있습니다. 페이 모듈에는 계산이 끝난 금액만 넘어옵니다.
환불 집행 : 여기서부터가 페이 모듈의 일입니다. 결제수단에 따라 되돌리는 방법이 다릅니다.
* PG 취소가 가능한 경우 : 자동으로 되돌림
* PG 취소가 불가능한 경우 : 수동 환불 대기 목록에 올림
무통장입금은 애초에 자동 환불이 불가능하고, 카드도 정산이 끝나면 PG 취소 대신 계좌 환불로 전환해야 합니다. 이 판단은 PG를 아는 드라이버가 합니다. 그리고 "돌려주기로 했지만 아직 안 보낸 돈"은 관리자 화면에 남습니다. (이게 없으면 환불이 조용히 누락됩니다.)
짓미 페이(zittme PAY) 모듈의 작동 방식, 원리에 대해 소개해드렸습니다.
추가로 서드파티 개발자 분들이 짓미 페이를 기반으로 다양한 모듈을 손쉽게 개발할 수 있는 연동 모듈 제작 가이드는
가이드 페이지 오픈시 제공해드릴 예정입니다.
언제나 zittme CMS 엔진이나, 공식 모듈에 대한 궁금한 점, 제안/건의 사항은 자유게시판을 통해 남겨주세요.
감사합니다.
댓글 0
로그인 후 댓글을 작성할 수 있습니다.