중계결제 v2 UI 복구
목차
relay-pay-v2 영역에 기능을 추가했다. 정확히는 세 가지가 묶인 작업이었는데, 중계결제 v2 UI 복구, 결제대행사 usePoint 연동, 바코드 멱등성 처리. 파일 수로는 SQL 매퍼 2개, 내부 클래스 1개, 뷰/스타일 1개. 숫자만 보면 소소해 보이지만 결제 도메인 특성상 각 파일이 건드리는 영향 범위가 커서 그냥 빠르게 끝낼 수 있는 작업은 아니었다.
왜 v2 UI가 "복구"인가
중계결제 v2는 이미 백엔드 로직은 붙어 있는 상태였다. 문제는 특정 화면에서 노출되던 일부 UI가 어느 시점에 누락된 채로 방치됐던 것. 원인을 추적해보니 v1 to v2 전환 과정에서 뷰 레이어가 미처 따라오지 못한 거였다. 기능이 없는 게 아니라 연결이 끊긴 것에 가까웠음.
그래서 "추가"보다는 "복구"에 가깝다고 표현하는 게 맞다. 백엔드에는 데이터가 있는데 화면에 안 나타나는 상황, 결제 쪽에서 이게 가장 당혹스럽다. 누군가 잘못된 정보를 보고 판단을 내렸을 수도 있으니까.
usePoint 연동은 여기에 얹히는 형태였다. 결제대행사 쪽에서 포인트 사용 여부를 별도 플래그로 넘기는 구조인데, 내부 클래스에 해당 필드가 아예 없었다. 클래스에 메서드 추가하고, SQL 매퍼에 컬럼 추가하고, 뷰에서 렌더링하는 흐름이 한 묶음으로 이어졌다.
바코드 멱등성은 결이 약간 다른 문제였다. 같은 결제 건에 대해 바코드 발급 요청이 중복으로 들어왔을 때 동일한 결과를 돌려줘야 하는데, 기존 처리 로직에 그 보장이 없었다. 멱등성 키 기반으로 중복 요청을 걸러내는 쿼리를 매퍼에 추가했다.
설계하면서 실제로 고민한 것들
이번 작업에서 선택지가 있었던 부분들을 정리하면 이렇다.
| 고민 포인트 | 선택한 방향 | 이유 |
|---|---|---|
| usePoint 필드 위치 | 내부 클래스에 직접 추가 | DTO 분리보다 기존 구조 유지가 변경 범위를 줄임 |
| UI 갱신 방식 | 요청 시점 1회 조회 | 실시간성 요구가 없는 화면, 폴링 추가는 오버 |
| 멱등성 보장 레이어 | DB 쿼리 레벨 | 앱 레이어에서 막으면 race condition 여지 남음 |
| NULL 처리 | 쿼리에서 COALESCE | 뷰에서 null guard 이중으로 두는 것보다 출처에서 처리 |
폴링 vs 이벤트 고민은 결제 화면마다 매번 나오는 주제다. 이번 케이스는 운영자가 수동으로 조회하는 맥락이어서 실시간 푸시가 필요 없었고, 오히려 폴링을 붙였을 때 불필요한 쿼리 부하만 생긴다. 단순한 선택이었지만 항상 "왜 폴링/이벤트가 아닌가"를 명확히 해두는 편이다. 나중에 다른 사람이 "왜 이렇게 했어요?"라고 물어볼 때 답이 있어야 하니까.
멱등성 처리는 코드로 보면 이런 형태였다.
-- 바코드 발급 이력 조회 (멱등성 키 기준)
SELECT barcode_no, issued_at
FROM relay_barcode_log
WHERE idempotency_key = #{idempotencyKey}
AND status = 'ISSUED'
LIMIT 1
이미 발급된 건이 있으면 새로 만들지 않고 기존 값을 반환하는 패턴. 앱 레이어에서 체크하는 방식도 가능하지만, 트랜잭션 경계 바깥에서 체크하면 동시 요청이 몰렸을 때 둘 다 통과할 수 있다. DB 레벨에서 막는 게 더 안전하다.
쿼리 실행 계획은 매퍼 작성 후 바로 EXPLAIN으로 확인했다. 신규 컬럼이 인덱스 대상이 아닐 때 의도치 않은 풀스캔이 붙는 경우가 있어서, 그냥 넘어가지 않는다. 이번엔 기존 인덱스가 커버해줘서 추가 작업은 없었다.
결제 도메인에서 "꼼꼼함"이 기본값이어야 하는 이유
금융/결제 쪽에서 일하다 보면 "대충 맞는 것 같다"는 게 얼마나 위험한 표현인지 계속 체감하게 된다. 화면에 보이는 숫자가 1원이라도 다르면 신뢰가 흔들리고, 한번 흔들린 신뢰는 회복하기 어렵다.
이번 작업에서도 구현 후 바로 화면 확인과 cross-check를 했다. 절차는 단순하다.
- 변경 전 화면 수치 메모 혹은 스크린샷
- 변경 후 동일 케이스로 수치 재확인
- usePoint 값이 있는 건, 없는 건 양쪽 모두 테스트
- 관련된 다른 화면에서도 같은 값이 노출되는지 확인
NULL이나 빈 데이터가 들어왔을 때 화면이 이상하게 깨지는 것도 이 단계에서 잡는다. "정상 케이스"만 테스트하고 넘어가면 나중에 엣지 케이스에서 반드시 다시 돌아온다.
커밋은 UI 복구, usePoint 연동, 바코드 멱등성 세 묶음을 논리적으로 분리해서 올렸다. 한 번에 몰아넣으면 커밋 로그만 봐서는 어떤 변경이 무슨 이유로 들어갔는지 추적이 안 되고, 이슈가 생겼을 때 어느 시점에서 문제가 생겼는지 bisect도 어려워진다. 이 습관은 지속적으로 유지하고 있고, 실제로 디버깅할 때 도움이 된 경험이 여러 번 있었다.
사내 서비스라도 코드가 쌓이면 히스토리가 유일한 문서가 된다. 커밋 메시지에 "무엇을"보다 "왜"를 담으려고 노력하는 이유가 거기 있다.
댓글 0
첫 댓글 달아줘.