쿠폰 발급 영수증 자동생성
목차
pay/receipt 영역에 쿠폰 발급 영수증 자동생성 기능을 붙였다. 같이 들어간 것들: 쿠폰 상세 페이지로 진입하는 동선 추가, 과거 발급 건에 대한 백필 적용. 변경 파일은 내부 클래스 4개, SQL 매퍼 1개, 뷰/스타일 1개.
숫자만 보면 작은 작업 같은데 실제로는 그렇지 않았다.
왜 쿼리부터 다시 짰나
기존 pay/receipt 화면은 결제 금액 기반으로 설계되어 있었고, 쿠폰 발급 자체를 하나의 "이벤트"로 기록하는 개념이 없었다. 할인 금액을 계산 결과에 포함시키는 방식이었는데, 이번에 "발급 시점에 영수증이 만들어져야 한다"는 요구사항이 생기면서 구조를 건드릴 수밖에 없었음.
화면에서 데이터를 조합해 보여주는 방식으로도 가능은 했다. 그런데 그렇게 하면 기존 결제 영수증과 표현 방식이 달라지고, 나중에 정산이나 감사 로그 쪽에서 쿠폰 발급 건을 조회할 때 기준이 흔들린다. 결제 도메인에서 "기준이 흔들린다"는 건 그냥 UX 문제가 아니라 숫자가 맞지 않는 상황으로 직결됨.
그래서 발급 이력 테이블과 연동해서 영수증 자동 생성 로직을 서버 레벨에서 처리하는 방향으로 잡았다. 쿼리 실행 계획도 미리 확인했는데, 발급 이력 조회가 메인 결제 쿼리에 조인으로 엮이는 구조라 인덱스를 어디에 두느냐가 성능에 영향을 줬다. 기존 인덱스를 최대한 재활용하는 방향으로 SQL 매퍼를 작성했음.
백필이 항상 제일 긴장된다
과거 발급 건 백필은 어떤 프로젝트에서나 가장 조심스러운 작업이다. 결제/쿠폰 도메인은 과거 데이터를 건드리면 이미 확정된 정산 수치가 달라 보일 수 있음. 실제 값이 변하는 게 아니라 집계 방식이 달라져 보이는 것뿐인데도 "숫자 틀렸다"는 리포트가 올 수 있다.
이번 백필 SQL은 세 가지를 기준으로 작성했다.
- 기존 발급 이력에서 영수증이 없는 건만 대상으로 함 (중복 생성 방지)
- 생성 시각은 발급 시각 그대로 기록 (현재 시각으로 채우면 감사 로그가 망가짐)
- WHERE 조건을 최대한 좁혀서 배치 범위 명확히 지정
-- 예시 구조 (실제 컬럼명은 내부 스키마 기준)
INSERT INTO coupon_receipt (coupon_issue_id, issued_at, created_at)
SELECT
ci.id,
ci.issued_at,
ci.issued_at -- 백필이므로 현재 시각 아닌 발급 시각으로
FROM coupon_issue ci
LEFT JOIN coupon_receipt cr ON cr.coupon_issue_id = ci.id
WHERE cr.id IS NULL
AND ci.status = 'ISSUED'
AND ci.issued_at < '2026-04-29 00:00:00' -- 신규 로직 적용 전 건만
;
운영 적용은 트랜잭션 단위로 나눠서 돌렸다. 한 번에 전체를 밀어넣으면 락이 길어질 수 있어서 적당한 단위로 끊어가며 확인했음. 중간에 이상한 케이스(status가 CANCELLED인데 발급 이력이 남아있는 건)가 나왔는데, WHERE에서 제외하고 별도로 확인한 뒤 스킵했다.
구현 시 방어한 케이스들
결제/쿠폰 쪽은 예외 케이스가 많다. 설계 단계에서 미리 리스트업해두지 않으면 배포 후에 하나씩 올라옴.
| 케이스 | 처리 방식 |
|---|---|
| 영수증이 이미 존재하는 발급 건 | 중복 생성 방지 (UNIQUE 제약 또는 SELECT 선체크) |
| 발급 취소된 쿠폰 | 영수증 생성 제외, 취소 이력은 별도 처리 |
| 권한 없는 접근 (타 사용자의 발급 건 조회) | 서버 레벨 본인 확인 후 거부 |
| NULL 가능 컬럼 (할인 금액 등) | 렌더링 전 fallback 처리 |
| 발급 이력은 있는데 금액 정보가 없는 레거시 건 | 빈 상태로 표시, 에러 아님으로 처리 |
실시간 갱신 여부도 결정해야 했다. 영수증은 발급 즉시 고정되는 문서이기 때문에 폴링이나 이벤트 기반 갱신이 필요 없다고 판단했음. 발급 트리거에서 한 번 생성하고 이후에는 READ-ONLY로 두는 것. 이 판단이 오히려 구현을 단순하게 해줬다.
사용자 접근 동선도 같이 설계했다. 쿠폰 상세 페이지에서 영수증으로 진입하는 링크인데, 존재하지 않는 영수증으로 접근할 때 "생성 중" 상태를 보여줄지 아니면 단순 리다이렉트를 할지 두 가지 안을 놓고 검토했다. 결국 단순하게 갔음. 영수증이 없으면 버튼 자체를 숨기는 방향으로. UI 상태를 복잡하게 가져갈 이유가 없었다.
배포 전 확인은 직접 화면에서 했다. 새로 생성된 영수증의 금액이 쿠폰 발급 화면에 표시된 금액과 일치하는지, 기존 결제 영수증 화면과 포맷이 어긋나지 않는지, 백필된 과거 건도 동일하게 표시되는지. Cross-check 대상 화면이 여러 개라 스크린샷을 찍어두고 비교했음.
커밋은 논리 단위로 쪼갰다. SQL 매퍼 변경, 서비스 레이어(내부 클래스) 변경, 뷰/스타일 변경, 백필 SQL을 각각 별도 커밋으로. 백필을 따로 두는 이유는 나중에 "이 데이터 언제 채워졌냐"고 물어볼 때 git log에서 바로 찾을 수 있어야 하기 때문이다. 서비스 로직과 섞이면 추적이 힘들어진다.
커밋 메시지는 "무엇"보다 "왜"를 담으려 한다. feat: 쿠폰 발급 영수증 자동생성보다 feat: 쿠폰 발급 시점에 영수증 자동 생성 - 발급 이력 기반으로 결제 영수증과 동일한 포맷 유지가 6개월 뒤에 훨씬 도움이 됨.
사내 서비스를 만들다 보면 기능 하나가 단순히 버튼 하나 추가로 끝나지 않는다는 걸 반복해서 체감한다. SQL 집계, 상태 머신, 예외 처리, 화면 렌더링, 권한 체크가 모두 엮여 있어서 어느 하나만 빠뜨려도 숫자가 맞지 않거나 특정 사용자에게 이상한 화면이 나타남. 금융/결제 도메인은 특히 그렇다. "대충 맞는 것 같다"로 넘어가면 반드시 다시 돌아오고, 그때는 원인 추적에 배로 시간이 든다.
댓글 0
첫 댓글 달아줘.