일기 slecs

오늘은 가상계좌 탈퇴-재가입 케이스가 네 갈래로 갈라졌다

목차

KP페이 가상계좌 쪽을 빨리 잡고 넘어가려고 오전에 들어갔다가, 오후까지 붙들렸다.

탈퇴 후 재가입한 회원이 가상계좌를 정상적으로 못 쓰는 케이스가 제보됐는데, 처음엔 상태값 초기화 하나면 될 줄 알았다. 파고들면서 케이스가 계속 불어났다. 탈퇴-재가입이라는 단일한 사용자 행위가 가상계좌 입장에서는 전혀 다른 여러 상황으로 갈라진다는 걸 하나씩 마주쳤다.

근본적인 문제는 KP페이 측 API가 이 시나리오를 정의해주지 않는다는 것이다. "기존에 가상계좌가 있는 사람이 탈퇴하고 다시 가입하면 어떻게 하냐"에 대한 스펙이 API 문서에 없다. 그러니까 우리가 케이스를 직접 나눠서 처리해야 한다는 결론에 도달하기까지 시간이 좀 걸렸다.

첫 번째는 자동해지 폴백. 정상 해지 API 호출이 실패하는 상황에서 로컬 상태라도 정리해주는 안전망이다. KP페이 API가 오류를 반환하더라도 우리 DB의 가상계좌 상태를 "해지됨"으로 마킹해야 재가입 시 새 계좌 발급이 가능하다. 이게 없으면 API는 실패했는데 우리 쪽은 아직 "활성"으로 들고 있는 불일치 상태가 된다. 재가입한 사람이 가상계좌를 신청하면 이미 있다고 튕겨나오는 것이다.

두 번째는 인증표 승계. KP페이 인증을 완료한 상태에서 탈퇴한 경우, 재가입 후에도 그 인증 정보를 이어받을 수 있는 케이스가 있다. 인증 유효성 조건을 검증하고 조건을 만족하면 재가입 계정에 연결하는 로직이다. 처음부터 인증을 다시 밟게 하는 게 안전하긴 하지만, 인증 자체가 사용자에게 부담이 있는 프로세스라 기존 인증을 승계할 수 있으면 하는 게 맞다고 봤다. 조건 검증 부분이 좀 신경 쓰였다. 인증 유효기간, 계좌 상태, 개인정보 동일성 같은 걸 다 따져야 하기 때문에.

세 번째는 관리자 해지. 자동 로직이 커버 못하는 케이스에 대한 운영팀 개입 경로다. 극단적인 엣지 케이스를 시스템이 전부 자동으로 처리할 수 있다고 믿으면 언젠가 문제가 생긴다. 운영팀이 수동으로 가상계좌를 해지할 수 있는 API와 관리자 화면을 열어두는 게 안전밸브 역할을 한다. 완전 자동화보다 이런 탈출구가 더 중요한 순간이 있다.

네 번째는 탈퇴 배치 정리. 탈퇴 처리 배치에 가상계좌 상태 청소 로직을 추가했다. 탈퇴가 완료되면 연결된 가상계좌 정보도 같이 처리하도록. 이게 빠져있으면 탈퇴-재가입 사이클이 반복될수록 garbage가 쌓인다.

네 케이스를 하나의 커밋으로 묶은 건 의도적인 판단이었다. 하나씩 넣으면 partial fix 상태로 존재하는 구간이 생긴다. 두 번째 케이스를 고치기 전까지는 첫 번째 픽스만 있고, 그 사이에 두 번째 케이스를 밟는 사용자가 있으면 여전히 막힌다. 이미 네 케이스를 전부 파악한 시점에서 반씩 넣는 건 그냥 자해다. 건드린 파일이 멤버 웹, 배치 탈퇴, 월렛 웹, 유틸 클래스, SQL 매퍼까지 다섯 개에 settlement-domain.md까지 갱신했다. 도메인 지식이 코드 밖에서도 정리가 돼야 다음에 이 영역을 건드리는 사람이 또 같은 걸 처음부터 파지 않는다.

PENDING이 화면에서 죽지 않고 있었다

가상계좌 작업을 마무리하고 나서 KP페이 쪽 다른 케이스가 기다리고 있었다.

당일 발행 불가 정책으로 거절된 경우에 미발급인데 PENDING 레코드가 살아있는 문제다. 충전 화면에서 KP페이로 충전을 시도하면 우리 쪽에서 먼저 PENDING 레코드를 생성하고 KP페이 API를 호출하는 순서로 흐른다. 당일 발행 불가 시간대에 시도하면 KP페이에서 거절 응답이 오는데, 이때 PENDING을 즉시 만료 처리하지 않으면 그 레코드가 화면에 살아있게 된다.

사용자 입장에서는 충전이 처리 중인 것처럼 보인다. 실제로는 아무것도 처리 중이 아닌데. 기다리다가 시간이 지나도 바뀌지 않으면 고객센터에 문의하게 되는 흐름이 나온다.

거절 응답 받는 시점에 PENDING을 즉시 만료로 처리하고, 화면에서 오해를 살 수 있는 텍스트도 같이 고쳤다. 파트너 포탈 멤버 상세 화면이랑 충전 화면 JSP, 쿼리 매퍼까지 건드렸다. 이 상태값이 여러 화면에 흩어져 있어서 월렛 웹 컨트롤러 하나만 고치면 안 되고 표시하는 쪽도 같이 잡아야 한다.

소소해 보이지만 이런 상태 불일치가 쌓이면 서비스 신뢰도가 깎인다. "뭔가 처리 중이래서 기다렸는데 아무것도 안 됐어"를 한 번 경험한 사람은 다음번엔 믿고 쓰기가 어려워진다.

HOLDING 쿠폰 환불을 열었다, 그리고 오지급도 잡혔다

쿠폰 도메인으로 컨텍스트를 전환했다. 파트너 포탈에서 HOLDING 상태 쿠폰에 대한 환불을 개방했다.

HOLDING은 수신자가 아직 선택하지 않은 쿠폰 상태다. 선물을 받았는데 어떤 상품으로 교환할지 아직 결정하지 않은 것. 이 상태에서 환불이 필요한 케이스가 생기는데, 기존에는 파트너 포탈에서 처리할 방법이 없었다. 이번에 그 경로를 열었다.

개방하면서 버그가 하나 같이 잡혔다. 환불 대상자에게 재선물이 오지급되는 문제였다. 환불 처리 플로우 내에서 특정 조건 분기가 타면서 수신자에게 쿠폰을 다시 선물하는 로직이 실행됐다. 환불이 됐는데 쿠폰이 다시 발급되는 상황. 재선물 로직이 환불 플로우와 특정 경로를 공유하고 있었던 게 원인이었다. 조건 분기를 명확히 하고 오지급 경로를 차단했다.

그리고 이 환불 플로우에 당사자 알림을 붙였다. 구매자한테 환불 완료, 수신자한테 쿠폰 회수 안내. 인앱 + 푸시 둘 다.

알림 설계에서 중요했던 건 두 사람이 받아야 하는 메시지가 다르다는 점이다. 구매자는 "내가 산 게 환불됐다"는 맥락이고, 수신자는 "내가 가지고 있던 게 회수됐다"는 맥락이다. 같은 이벤트를 바라보는 방향이 다르다. 메시지 하나로 두 상황을 퉁치면 어느 한쪽은 어색한 문장을 받게 된다. 수신자가 아무 설명도 없이 쿠폰이 사라진 걸 경험하지 않도록 하는 게 최소한의 의무다.

이 알림이 지금까지 없었다는 게 솔직히 좀 놀랍긴 하다. 환불이 완료됐는데 구매자한테 아무 통보도 없이 그냥 처리만 되는 흐름이었던 거니까. 오늘 붙였다.

소리 없이 들어간 기준 통일

요구예치금 산정 기준에 충전권성 항목이 포함돼 있었다. 판매 지표에서는 이미 충전권성을 제외하고 계산하는데, 예치금 산정에서만 포함되고 있었다.

이게 "틀린 계산"이냐 하면 그렇게 단정하기 어렵다. 충전권 매출을 어떻게 보느냐에 따라 다를 수 있다. 근데 운영팀이 실제로 쓸 때는 두 숫자가 다르면 왜 다르냐는 질문이 반드시 나온다. 그때마다 "기준이 달라서요"를 설명하는 비용이 발생한다. 그 설명을 여러 번 반복하게 될 거라면 기준을 맞추는 게 맞다. SQL 매퍼 두 곳 수정하고 settlement-domain.md도 갱신했다.

민원 대응 내역 - 오늘 가장 많이 왔다갔다한 작업

오늘 커밋 숫자에서 가장 많은 비중을 차지한 게 민원 문서 쪽이었다. doc.jspadmin-complaint-doc.js가 여러 커밋에 걸쳐 반복 등장했다. 올렸다 수정했다 뒤집었다가 다시 정착한 흐름이다.

처음에 tb_complaint_doc 기반으로 사건별 문서 발급 이력 섹션을 만들었다. 민원 대응 과정에서 발급된 문서들을 사건 단위로 묶어서 이력으로 보여주는 것, 자동 채번 포함. 테이블 구조를 파악하고 어떻게 그룹핑할지 잡는 게 초기 작업이었다.

여기까지는 괜찮았다. 이걸 올리고 나서 전용 탭으로 분리했다. 대응 내역이 간단 화면이랑 수기 화면에 섞여있으면 세 모드의 맥락이 뒤엉킨다. 간단/수기/대응내역으로 탭을 명확히 나눠 각 화면의 역할을 분리했다. JSP와 CSS를 건드렸다.

분리하고 나서 사건 그룹별로 "ZIP 다시 만들기" 버튼을 넣었다. 대응 내역에서 특정 사건의 ZIP 패키지를 재생성할 수 있으면 유용하겠다는 생각이었다.

그리고 다음 커밋에서 그 버튼을 뺐다.

화면에 올려봤더니 맥락이 안 맞았다. 대응 내역 탭은 발급된 기록을 보는 장부여야 하는데, 거기서 새로 생성하는 버튼이 나오면 읽기와 쓰기가 같은 화면에 섞인다. 조회 화면인지 편집 화면인지가 모호해진다. 대응 내역은 읽기 전용 장부라는 개념을 명확히 하기로 했다. ZIP 재소환이 필요하다면 다른 컨텍스트에서 접근하는 게 맞다.

버튼을 달았다 뗀 게 낭비처럼 보일 수 있지만, 이 과정에서 "이 탭이 무엇이어야 하는가"가 명확해졌다. 화면에 올려보기 전에는 이론적으로만 판단해야 하는데, 실제로 보면 느낌이 다르다.

그룹 접기 fix도 들어갔다. 사건 그룹이 여러 개일 때 전부 펼쳐져 있으면 스크롤이 길어진다. 최신 발급분 그룹만 기본으로 펼치고 이전 것들은 <details> 태그로 접어두는 방식. 운영팀이 최근 발급 내역부터 확인하는 게 일반적인 패턴이라는 가정을 반영했다.

민원 문서 미리보기는 별개 커밋이었다. 회원 카드에서 문서를 클릭하면 새 탭에서 inline PDF로 바로 볼 수 있는 기능. 채번 없이 미리보기만 가능한 경로다. 백엔드에서 스트림으로 내려주는 방식이고 컨트롤러, JSP, JS 파일을 건드렸다.

민원 도메인은 이렇게 여러 커밋으로 흘러가는 게 사실 일반적이다. 완벽한 스펙을 미리 정의하고 한 번에 올리는 게 이 영역에선 거의 불가능하다. 운영팀이 실제로 써봐야 나오는 피드백이 있고, 그때마다 반영하는 루프가 현실적인 방법이다. 오늘 기준으로 민원 대응 내역 탭은 읽기 전용 장부로 정착했다. 이게 내일 또 바뀔 수도 있다는 건 안다.

오늘의 피로는 코드가 아니라 맥락 전환이었다

오전부터 저녁까지 도메인이 세 갈래였다. KP페이 가상계좌, 쿠폰 환불, 민원 문서. 각각 맥락이 완전히 다르기 때문에 전환할 때마다 이전 작업을 머릿속에서 지우고 새로 채워야 한다. 코딩 자체보다 이 전환 비용이 더 피곤할 때가 있다.

KP페이 가상계좌 작업은 "안 터지면 다행"이라는 심정으로 마무리했다. 논리적으로 네 케이스를 전부 잡았다고 생각하지만, 이런 외부 API 연동 쪽은 항상 예상하지 못한 시나리오가 나온다. 네 가지를 다 잡았다고 해서 다섯 번째가 없다는 보장이 없다. 그냥 지금 아는 케이스를 최대한 꼼꼼히 처리했다는 것뿐이다.

쿠폰 알림은 명확히 부재했던 걸 채운 작업이었다. 환불됐는데 당사자한테 아무 말이 없는 게 맞냐. 아니다. 오늘 붙였다.

민원 대응 내역은 ZIP 버튼을 달았다 뗀 과정을 포함해서, 결국 "이 화면이 무엇이어야 하는가"를 명확히 했다는 게 오늘 결과다. 장부는 장부여야 한다.

chore(psy) 배치가 오늘도 정상 작동했다. 에어컨 온도 전쟁, 에어컨 전쟁, 자정 지하도 발소리. 커버 이미지랑 JSON 셋이 자동으로 올라왔다. 특별히 손댄 게 없으니 더 할 말도 없다.

댓글 0

첫 댓글 달아줘.