23개 쿼리를 한 번에 고치고, 화면을 네 번 뒤집은 날
목차
오늘 아침에 코드를 열었을 때, 처리할 일이 뭔지 대충은 알고 있었다. 파트너 포털 쪽 UI 정리가 남아있었고, 웹훅 기능도 마무리해야 했다. 근데 진짜 첫 번째 이슈는 예상 밖에서 왔다.
판매지표 숫자가 이상하다는 얘기였다.
판매지표의 오염, 그리고 23개 쿼리
파트너 포털에서 보이는 판매 실적 수치에 무상발급 건수가 섞여 있었다. DISPATCH 타입, ADMIN 타입, EVENT 타입 - 이 세 가지는 실제 결제가 발생하지 않는 무상 발급이다. 그런데 판매지표 쿼리들이 issue_type 구분 없이 전체를 집계하고 있었다. 파트너가 실제로 번 매출을 보는 게 아니라, 무상으로 나간 것까지 포함된 부풀려진 수치를 보고 있던 셈이다.
왜 이렇게 됐냐면, 처음에 지표 쿼리를 짤 때는 issue_type 구분이 지금처럼 세분화되어 있지 않았다. 이후에 DISPATCH, ADMIN, EVENT 타입이 추가되면서 무상발급 경로가 생겼는데, 그 시점에 기존 지표 쿼리들을 같이 안 건드렸다. 천천히 쌓인 부채였다.
수정 자체는 단순하다. issue_type IN ('PURCHASE', 'GIFT') 조건을 판매 집계가 들어가는 쿼리마다 박아주면 된다. 문제는 그 쿼리가 23개였다는 것.
매퍼 XML 파일들을 하나씩 열었다. 판매 집계, 일별 추이, 기간 합산, 파트너별 분석. 테이블 구조도 다르고, JOIN 방식도 다르고, 어떤 건 서브쿼리 안에 조건이 있었다. 기계적으로 일괄 치환하면 안 되는 구조가 분명히 있었다. 어떤 쿼리는 WHERE 절에 바로 박으면 됐고, 어떤 건 서브쿼리 안쪽 SELECT에 조건이 들어가야 했고, HAVING을 쓰는 쿼리는 또 다르게 처리해야 했다. 눈으로 23개를 직접 읽으면서 손으로 조건을 달았다.
그렇게 다 했다고 생각하고 커밋을 올렸는데, 일별 판매 추이 쿼리 2개가 빠진 걸 뒤늦게 발견했다. 별도 매퍼 파일에 있던 쿼리들이라 처음 검색 범위에서 누락됐던 것이다. 별도 커밋으로 뒤따라 수정했다. 그것도 끝나고 스타일 커밋이 하나 더 나왔다 - 쿼리 XML 주석에 em-dash가 들어간 것들을 제거하는 작업. 프로젝트 전역 표기 규칙인데 쿼리 파일에까지 슬금슬금 들어가 있었다. 네 개 매퍼 파일에서 일반 하이픈과 쉼표로 정리했다.
오전 시간의 상당 부분이 이 쿼리 작업으로 갔다. 23개라는 숫자보다, 각 쿼리를 직접 읽고 판단해야 하는 집중력 소모가 컸다. 기계적인 작업처럼 보이지만 눈을 뗄 수 없는 종류의 일이었다.
서류현황 화면 신설, 그리고 네 번의 수정
판매지표 작업이 마무리될 즈음 파트너 서류현황 화면을 새로 만드는 작업으로 넘어갔다.
/admin/partner/documents 경로에 JSP를 신설하는 것이었다. 각 파트너가 제출한 서류들의 현황을 한눈에 볼 수 있는 관리자 화면. 신설 커밋에서 기본 리스트 구조, 소속(상위 파트너) 컬럼, 서류 미리보기, 엑셀 다운로드 설정, 라우팅까지 한 번에 잡았다. 컨트롤러, ExcelConfigRegistry, 쿼리 매퍼, JSP, 파트너 목록 화면의 문서 버튼 링크까지 같이 들어갔다.
올리고 나서 요구사항이 바뀌었다.
신분증 관련 서류 컬럼을 화면에서 아예 빼달라는 얘기가 나왔다. 그리고 미제출 판정 로직에 문제도 있었다. 특정 서류 유형이 제출 대상이 아닌 파트너에게도 미제출로 표시되는 케이스였다. 파트너 유형별로 제출 의무가 있는 서류가 다른데, 그 조건이 쿼리에서 정확하게 반영이 안 됐던 것이다. 미리보기 방식도 inline으로 개선하는 것이 같이 들어갔다. 이 변경들이 두 번째 커밋이 됐다.
세 번째 커밋은 미리보기 이미지 lazy 로딩 문제였다. 서류 목록에서 이미지를 클릭하면 모달 오버레이에 미리보기가 뜨는 구조인데, 이미지가 아예 안 보이는 경우가 있었다.
원인은 loading="lazy" 속성이었다. lazy 로딩은 이미지가 뷰포트 근처에 왔을 때 불러오는 방식이다. 처음에 모달이 display:none 상태로 숨겨져 있으면, 브라우저는 그 안의 이미지가 뷰포트에 없다고 판단하고 로드를 건너뛴다. 모달이 열려도 이미 lazy로 스킵된 이미지는 자동으로 다시 로드하지 않는다. 알고 있던 함정인데 또 걸렸다. loading 속성을 제거하니 당연히 바로 됐다.
하루에 화면 하나가 커밋 네 번으로 나뉘었다. 신설 - 개선 - 판정·미리보기 fix - lazy 제거. 처음부터 요구사항을 다 잡고 시작했다면 하나나 둘로 끝낼 수 있었을 것이다. 신분증 제거 요청이나 미제출 판정 기준은 화면을 보여주기 전에 물어봤어야 할 부분이었다. lazy 로딩 문제는 명백히 내가 알고 있었어야 하는 것이기도 하고.
파트너 포털, 오늘 상당히 많이 손댔다
파트너 포털 쪽에서 오늘 나간 커밋이 8-9개다. 개별로 보면 각각 독립된 기능 같지만, 모두 같은 도메인 안에서 연결된 변경들이다.
가상계좌 마스킹과 관리자 토글
충전 관리 화면에서 가상계좌번호가 그대로 노출되고 있었다. 중간 자리를 마스킹 처리하고, 관리자 권한이 있을 때만 전체 노출 토글을 쓸 수 있게 했다. 유틸 클래스에 마스킹 처리 로직이 들어갔고, 컨트롤러 두 곳과 쿼리, JSP가 따라서 수정됐다.
이 마스킹 작업을 기반으로, 회원 상세 충전 내역에 가상계좌 컬럼을 추가했다. 동일하게 마스킹 적용. 수수료 내역 페이지에는 하위 파트너별로 수수료와 가상계좌 현황을 볼 수 있는 섹션을 새로 붙였다.
그런데 이 섹션을 refactor 커밋에서 다시 위치를 바꿨다. 처음엔 fee-revenue 페이지에 달았는데, 정산 페이지와 대시보드에 있는 게 더 자연스럽다는 결론이었다. 같은 날 만들고 같은 날 이동했다. 한 번에 결정을 못 한 케이스다.
예상 수수료 표시와 쉬운 말 개편
미소진 충전금에 대한 예상 수수료를 계산해서 보여주는 기능이 들어갔다. 충전 건당 수수료, 정률 수수료, 결제 수수료를 따로따로 보여주는 구조다. 쿼리가 새로 붙고, dashboard.jsp와 settlement.jsp 양쪽에 표시 영역이 생겼다.
이것과 같이 포털 전반의 섹션 문구를 쉬운 말로 바꿨다. 파트너 포털은 파트너 사업자가 보는 화면이다. 개발 용어나 시스템 용어가 그대로 노출되면 이해하기 어렵다. 수수료 구조 설명, 충전 안내, 정산 관련 설명들을 사업자가 읽기 쉬운 표현으로 전면 개편했다.
이런 문구 작업은 코드 수정보다 오래 걸릴 때가 있다. "이 표현이 맞는 말인가"를 계속 고민하게 되니까. 완성됐다는 확신도 없이 일단 올리는 경우가 많다.
예치담보 기준 전환 - 전일에서 역대 MAX로
예치담보 요구예치금 산정 기준을 오늘 바꿨다. 기존에는 전일 판매 금액을 기준으로 요구예치금을 계산했다. 이걸 역대 최고 일판매(MAX) 기준으로 전환하고, 10% 룰을 일괄 등록하는 배치도 같이 들어갔다.
전일 기준의 문제는 명확하다. 판매가 한동안 낮다가 갑자기 급증하는 날에 예치금이 부족해질 수 있다. 파트너가 담보를 충분히 쌓지 않은 상태에서 판매가 터지면 시스템 입장에서 리스크가 생긴다. MAX 기준이면 항상 파트너의 역대 최대 실적을 기반으로 예치금을 요구하게 되니, 이런 급증 상황에서 훨씬 안전하다.
파트너 입장에서는 더 많은 금액을 예치해야 하는 불편이 있다. 그래도 운영 안전 우선으로 판단했다. detail.jsp, deposit-list.jsp, 하위 파트너 화면도 따라서 수정됐다.
수수료 요율 상속 로직도 이 과정에서 함께 손봤다. 상위 파트너에서 요율을 상속받는 케이스에서 판정이 틀리는 경우가 있었고, 가용 lot 화이트리스트 처리와 요율 미설정 판정 기준도 같이 정확하게 다듬었다. codex 측 지적을 반영한 커밋이었다.
대시보드 레이아웃
대시보드 카드 배치를 CSS 그리드 auto-fit으로 바꿨다. 화면 너비에 따라 카드가 자동으로 재배치되는 방식이다. apple.css도 수정됐다. 기능보다 UX 쪽 개선이고, 변경 자체는 크지 않지만 다양한 해상도 환경에서 보는 파트너들에게는 차이가 난다.
웹훅 기능 올리고 바로 보안 감사를 맞았다
supply 웹훅 관련 커밋이 오늘 두 개 나왔다. 순서를 보면 기능 커밋이 먼저, 보안 감사 반영 커밋이 바로 뒤따랐다.
CHOICE(선택형) 타입 전용 공급 전환 기능과 파트너 웹훅 발송 기능을 새로 만들었다. 외부 쿠폰 연동 관련 클래스, 웹 컨트롤러, 개발 테스트용 엔드포인트, 파트너 컨트롤러, 배치까지 여러 파일이 동시에 바뀌었다. API 레퍼런스 문서도 이 시점에 업데이트됐다.
기능을 올리고 나서 보안 검토를 돌렸고, 나온 이슈들을 한 번에 처리했다.
페이로드에서 핀번호를 제거했다. 웹훅으로 외부에 나가는 데이터에 민감 정보가 포함되면 안 된다는 기본 원칙이다.
IPv6 ULA와 NAT64 차단이 눈에 띈다. 외부 웹훅을 발송할 때 대상 URL의 IP 주소를 검증해야 SSRF(Server-Side Request Forgery)를 막을 수 있다. IPv4 사설 대역만 막으면, IPv6 ULA 주소(fc00::/7 대역)나 NAT64 변환 주소(64:ff9b::/96)를 통해 내부망으로 요청이 들어갈 수 있다. 이걸 잡은 건 꼼꼼한 보안 검토였다는 얘기다.
테스트 쿨다운도 추가했다. 파트너가 웹훅 테스트를 무한정 날릴 수 없도록 요청 간격을 제한하는 것이고, self-only 방어는 자기 자신의 엔드포인트로 웹훅을 보내는 루프를 막는 것이다.
api-docs.jsp도 정리했다. 파트너가 보는 API 문서 페이지에서 조회 API 섹션을 숨겼다. 파트너에게 노출할 필요가 없는 내부 API 정보를 정리한 것.
기능 만들고 보안 감사하고 반영하는 사이클이 같은 날에 돌았다. 이게 이상적인 흐름이다. 기능 커밋이 나가고 며칠이 지나면 맥락이 흐려진다. 당일에 검토하고 당일에 닫은 게 맞는 방식이었다.
앱스토어 반려, 그리고 8개 언어
오늘 예상 못 한 인터럽트가 하나 있었다. Flutter 쪽 모바일 앱이 앱스토어 심사에서 반려됐다는 통보가 들어온 것이다.
5.1.1(iv) 반려다. 애플 심사 가이드라인에서 권한 요청과 관련된 규정인데, 마이크 권한 사전화면의 버튼 라벨이 문제가 됐다. 중립적이지 않은 표현이 들어가 있었던 것이다. 애플은 사전 권한 안내 화면에서 사용자를 압박하거나 오도하는 표현을 쓰지 말라고 요구한다.
수정은 버튼 텍스트를 Continue로 바꾸고, 화살표 아이콘을 추가하는 것이었다. 이 앱이 8개 언어를 지원하고 있어서 arb 파일을 언어별로 다 수정해야 했다. 영어, 아랍어, 스페인어, 힌디어, 인도네시아어, 일본어 arb가 변경됐다. 언어별로 해당 키를 찾아 값을 맞게 바꾸는 단순한 작업이지만 빠짐없이 챙겨야 한다.
재제출 방법도 오늘 문서화했다. 앱스토어 반려에 대응할 때 API로 PATCH 요청을 보내면 409 에러가 나온다는 트랩이 있다. 재제출은 웹 UI에서 2단계를 거쳐야만 가능하다. 처음 해보는 사람이면 API로 시도했다가 시간을 낭비하기 딱 좋은 구조다. 이걸 docs/mobile-app-launch.md와 CLAUDE.md에 지침으로 남겼다.
버튼 하나 바꾸는 게 별거 아닌 것 같지만, 반려 통보가 오면 빠르게 대응하는 게 중요하다. 재심사 사이클이 있어서, 늦으면 그만큼 출시가 밀린다. 빠르게 처리하고 올린 게 맞는 판단이었다.
운영 SQL 두 건 기록
오늘 운영에 실행 완료된 SQL 커밋이 두 개 쌓였다.
하나는 7월 23일 자 DISPATCH 충전권 이중부과 테스트 건 취소 보정이다. 테스트 과정에서 수수료가 2중으로 찍힌 건들을 수동으로 취소 처리한 내용이고, 이미 운영에 실행한 SQL을 기록으로 남긴 것이다. 다른 하나는 이커머스 PG 플랫폼 전 회원 충전한도를 1회 200만 원, 일 2,000만 원으로 통일한 것. 기존에 회원별로 한도가 들쑥날쑥했던 걸 정리한 내용이다.
운영 DB에 직접 돌리는 SQL은 항상 찝찝하다. 코드로 관리되는 게 아니라 사람이 판단해서 직접 실행하는 것이라, 실수가 나면 되돌리기 어려운 경우가 있다. 실행 완료 후 커밋으로 남겨두는 이 방식이 최소한의 추적 수단이 된다.
마지막에 붙은 것들
psy 프로젝트 쪽 자동 생성 콘텐츠가 오늘 커밋에 들어왔다. family-bathroom-ruler와 haunted-secondhand 두 개의 테스트 JSON, 커버 이미지. 자동화 배치로 생성되는 것이라 직접 손댄 건 없다.
발송포탈 발송내역 상세에서 발급건 수가 0으로 뜨는 버그 수정이 오늘 두 번 올라갔다. 내용이 같은 커밋이 두 개다. 실수다. 그냥 amend 했어야 했는데, 히스토리에 불필요한 중복이 생겼다.
오늘 커밋이 20개를 넘는다. 파트너 서류현황 신설, 23개 쿼리 필터 일괄 적용, 파트너 포털 전반 개편, 웹훅 기능과 보안 처리, 앱스토어 반려 대응, 운영 SQL 보정. 도메인도 여러 개였고, 프로젝트도 여러 개였다.
총괄 포지션이면 이런 날이 있다. 집중이 안 되는 게 아니라 집중해야 할 곳이 여러 개인 날. 그게 같은 말인지는 모르겠지만, 오늘은 그런 날이었다.
서류현황 화면이 네 번 고쳐진 건 아쉽고, 발송포탈 버그 커밋 중복은 실수다. 나머지는 다 의도한 대로 나갔다.
댓글 0
첫 댓글 달아줘.