열아홉 번 도는 쿼리를 접고 리스팅 수십 장 올린 날
목차
오늘 커밋 목록을 보면 뭔가 엄청 많이 한 것 같다. 실제로도 많이 했다. 그런데 "오늘 뭐 했어요?"라고 누가 물으면 잠깐 막힌다. 파트너 대시보드 성능 개선, 딥링크 폴백 버그 세 번, 통화매니저 기능, 선물 PIN 만료일 버그, Play 스토어 리스팅 수십 장. 이게 다 하루다.
그중 기술적으로 가장 오래 붙들었던 건 파트너 수익 요약 쿼리 재작성이었다. 나머지는 그 사이에 끼워 넣듯 처리했고, Play 리스팅은 후반에 폭발했다.
재귀 CTE 열아홉 번, 단일 패스로 접다
selectPartnerEarningsSummary를 처음 뜯었을 때 숫자가 19였다. 재귀 CTE 호출이 쿼리 하나 실행에 19회 반복.
파트너 계층을 DB에서 다룰 때 재귀 CTE가 나오는 건 자연스러운 선택이다. 문제는 어떻게 쓰느냐인데, 기존 코드는 집계 항목마다 재귀 CTE를 별도로 호출하는 구조였다. 월별 수익, 충전 합계, 결제 합계, 각 항목마다 계층을 따로 타니까 파트너 하나 조회에 DB가 계층을 열아홉 번 훑는다. 파트너 수가 적을 때는 버텼겠지만 누적되면서 슬로우 쿼리가 보이기 시작한 거다.
재작성 원칙은 단순하게 잡았다. 재귀는 앞에서 딱 한 번만 타고 계층 전체를 CTE로 끌어올린다. 이후 모든 집계는 조인과 그룹바이로만 처리한다. 재귀 CTE 하나를 맨 앞에 두고 집계 CTE들을 순서대로 쌓아 올리는 구조인데, 쿼리가 길어지는 단점이 생기지만 DB가 실제로 계층을 타는 건 딱 한 번이다.
짜는 데 예상보다 시간이 걸린 건 값 검증 때문이었다. 같은 파라미터로 기존 쿼리와 신규 쿼리를 병렬로 돌리면서 결과를 비교하는데, 거의 다 맞는데 일부 파트너에서 monthEarningsAll 값이 달리 나왔다. 처음엔 재귀 조인 순서 문제인가 싶었다. 들여다보니 월 기준 필터 조건이 미묘하게 달랐다. 기존 쿼리가 월 경계를 특정 조건으로 처리하고 있었는데, 재작성 과정에서 그 조건이 더 단순하게 표현되어 있었고 의미상 동치처럼 보였지만 실제로는 edge case에서 결과가 달랐다. codex 검증 단계에서 잡혀서 다행이었다. PR C-1에 fix 커밋이 붙은 게 그것 때문이다. 이게 리팩터링의 진짜 위험이다. 쿼리를 예쁘게 바꿨다고 생각했는데 의미가 조금 달라져 있는 것.
PR을 A, B, C 세 단으로 나눈 것도 오늘 판단 중 하나였다. A는 tb_partner_metric_daily 테이블 생성과 리빌드 배치 신설. B는 대시보드 V2 쿼리 도입과 PartnerMetricBucketToggle 토글, 기본값 OFF. C는 개별 쿼리 최적화. 한 번에 올리면 롤백 단위가 너무 커지고, 데이터가 안 쌓인 상태에서 V2를 켜면 대시보드에 빈 차트가 뜬다. 배치가 충분히 돌고 버킷에 데이터가 쌓이면 그때 토글 올리는 식으로 가는 거다. 그래서 지금 운영에 배포해도 실제 사용자가 보는 화면은 여전히 기존 쿼리로 서빙된다.
selectPartnerBalanceHistoryDailyV2는 버킷+라이브 전환 구조다. 날짜별 집계는 버킷에서 가져오고, 당일 실시간 부분은 라이브 테이블로 보완하는 방식. 값 동일 검증을 두 번 돌렸고, 약 10배 성능 차이가 나왔다. 이 수치가 운영 트래픽에서도 그대로 나오는지는 토글 올려봐야 안다.
충전/결제 현황 selectChargePaymentSummaryForHierarchyV2는 요율 사전해결이 핵심이었다. 기존에 행별로 요율을 따로 계산하는 구조가 있었는데, 그걸 조인으로 앞에서 한 번 처리하고 이후 집계에서 그냥 참조하게 바꿨다. 이쪽은 비교적 깔끔하게 끝났다.
운영 DDL은 오늘 사용자 승인을 받고 바로 실행했다. DDL_20260811_partner_metric_daily.sql과 전체 테이블 목록 업데이트. 로컬, 스테이징 다 통과했어도 운영 DDL은 실행할 때 항상 긴장이 따른다. 아무것도 안 일어났고, 이번엔 그냥 잘 됐다.
blur 오탐이 폴백을 세 번이나 먹어치웠다
딥링크 랜딩 페이지 버그가 오늘 fix 커밋 세 개를 남겼다. 같은 파일이다. 솔직하게 쓰자면 두 번은 잘못 고쳤다.
시작은 앱 미설치 상태에서 스토어 폴백이 동작하지 않는다는 이슈였다. 딥링크 URL에 진입하면 앱 스킴을 시도하고, 앱이 설치되어 있으면 앱이 열리고, 없으면 브라우저에 남아서 스토어 링크를 보여줘야 한다. 그런데 앱이 없는데도 폴백이 취소되는 케이스가 있었다.
원인은 blur 이벤트였다. 앱 스킴을 시도할 때 브라우저가 잠깐 포커스를 잃었다가 복귀하는데, 이 blur를 앱 전환 성공으로 오탐하는 리스너가 붙어 있었다. 앱이 설치된 경우엔 실제로 앱이 열리면서 브라우저가 배경으로 넘어가니까 blur 감지가 맞는 신호다. 문제는 앱이 없는 경우에도 스킴 시도 과정에서 blur가 짧게 발생하고, 그걸 구분하지 못했던 거다.
첫 번째 수정에서 지연 처리를 넣었다. blur가 일정 시간 이상 지속되면 앱 전환 성공으로 보고, 짧게 끝나면 실패로 처리하는 방식이었다. 타이밍으로 구분하자는 아이디어인데, 브라우저마다 타이밍이 달랐다. 어떤 브라우저는 스킴 실패해도 blur가 길게 유지되고, 어떤 건 성공해도 빠르게 돌아왔다. 지연 값을 어떻게 잡아도 모든 환경에서 맞지 않았다.
두 번째 수정에서는 알럿 지연이 생기거나 설치 버튼이 조건 없이 상시 노출되는 부작용이 추가로 생겼다. 첫 번째 수정의 side effect였다.
세 번째에서 리스너 등록 순서를 바꾸고 더 이상 참조되지 않는 죽은 변수들을 제거했다. 이벤트 핸들러가 스킴 시도보다 늦게 등록되면 일부 환경에서 이벤트 자체를 못 잡는 문제가 있었다. 순서를 바로잡는 것으로 마무리했다.
딥링크 폴백은 브라우저마다 동작이 달라서 로컬에서 재현이 구조적으로 어렵다. 그게 변명이 되지는 않는다. 다음에 이 부분 건드릴 때는 환경별 케이스를 먼저 정리하고 들어가야 한다. 그러지 않으면 또 같은 패턴이 나온다.
통화매니저 완성, 선물 PIN은 반쪽
KT 통화매니저 주소록 내보내기가 오늘 기능으로 완성됐다. 회원목록에 엑셀 메뉴를 추가하고, 그룹명이 단순 이름이 아니라 시스템 > 파트너 전체 계층 경로로 출력되게 쿼리를 짰다. KT 통화매니저에서 분류가 제대로 동작하려면 경로 형태가 있어야 한다.
매핑 커밋이 두 개로 나뉜 건 요건이 뒤늦게 확정됐기 때문이다. 회사명 필드에 파트너명이 아니라 대리점명이 들어가야 하고, 메모 필드에는 파트너코드가 들어가야 한다는 게 초기 구현 이후에 정해졌다. 이런 매핑은 실제로 파일을 뽑아서 KT 시스템에 올려봐야 맞는 값을 알 수 있어서, 한 번에 완성되기 어려운 성격이 있다.
선물 PIN 만료일 버그는 찝찝하게 끝났다. 쿠폰 만료일을 물려받아야 하는데 7일 폴백으로 고정되던 문제였는데, 조건 분기에서 쿠폰 만료일이 null이 아닌데도 폴백으로 빠지는 경우가 있었다. 로직 자체는 수정했지만, 이미 발급된 PIN 중에 만료일이 잘못 박혀 있는 게 있는지 확인이 아직 남아 있다. 데이터를 직접 조회해봐야 알 수 있는 건데 오늘은 시간이 없어서 내일로 넘겼다. 이게 좀 걸린다. 로직 고쳤다고 끝이 아닌 상황.
Play 리스팅 수십 장의 재정비
오늘 커밋에서 가장 많은 분량을 차지하는 건 단연 Play 스토어 리스팅이다. docs 커밋만 세어보면 수십 개다. 앱 하나가 아니라 여러 앱에, 로케일도 8개에서 많은 건 12개까지.
이 작업의 배경이 있다. Play 스토어 반려. 원인이 두 가지였는데, 의료 선언과 타 앱 이미지 잔재. 앱 설명 문구에 의료 관련 표현이 정책에 걸렸고, 스크린샷이나 프로모션 그래픽에 다른 앱 화면이 섞여 있던 것도 문제가 됐다. 이게 CLAUDE.md에 "반려 원인 정정 재제출 기록"으로 남아 있다.
반려 이후 각 앱의 리스팅을 처음부터 다시 쓰는 작업이 시작됐다. 기준은 짧은 설명 70자 이상, 자세한 설명 3300~3500자 이상, 로케일은 기본 8개. 짧은 설명이 70자 이상이라는 게 생각보다 까다롭다. Play 콘솔에서 짧은 설명 상한이 80자이니까, 70자 이상 80자 이하라는 좁은 범위를 매번 맞춰야 한다. 로케일별로 같은 의미의 문장도 글자 수가 달리 나오고, 특히 독일어는 합성어 문화라 길어지고 일본어는 한자 밀도에 따라 들쭉날쭉하다.
자세한 설명 3500자 이상도 단순히 글자를 채우는 게 아니다. 기능 설명이 충분해야 하고, 정책 위반 표현이 없어야 하고, 각 로케일에서 문장이 자연스러워야 한다. 로케일이 8개면 같은 내용을 8가지 언어로 검토한다는 뜻이다.
오늘 처리한 앱 수가 많다 보니 실수도 났다. ASC Keywords 필드가 누락된 커밋이 있었고 그다음 커밋에 복원했다. iOS Name/Subtitle/Keywords/Promo 필드는 연속 2개 커밋에서 날아가서 또 복원 커밋이 생겼다. listing.md 파일이 길어서 편집하다 보면 나올 수 있는 실수인데, 같은 패턴이 두 번 연속으로 나온 건 좀 허탈했다.
em-dash 제거 커밋도 있었다. 프로젝트 내부 규칙에 em-dash 금지가 있는데 리스팅 문구에 들어가 있던 걸 하이픈과 문장 부호로 바꿨다. 작은 거지만 이런 게 쌓이면 전체 텍스트 일관성이 달라진다.
앱별로 listing.md가 있고, 로케일이 많은 경우엔 listing_de.md, listing_en.md, listing_es.md, listing_fr.md, listing_it.md, listing_ja.md 식으로 파일이 분리되어 있다. 오늘 커밋에 이 파일들이 묶음으로 나오는 앱들이 여럿 있다. 앱마다 각 로케일 파일을 열고 내용을 채우고 자수 확인하고 닫고를 반복했다.
Slang Decoder(global SKU), 요즘말 사전(kr SKU) 같은 SKU별 분리 앱도 있었다. 앱 이름이 다르고 설명 기조가 달라야 하는데, 같은 카테고리 앱이다 보니 구분이 흐려지지 않게 유지하는 게 신경 쓰이는 부분이다.
play_console_apps.json에 신규 앱 세 개가 누락된 것도 오늘 잡았다. 8월 9일에 만든 traytime, camberpro, arrowwise가 빠져 있었다. 이 파일이 배치 처리 대상 앱 목록이라면 누락되면 해당 앱들이 자동화 흐름에서 조용히 빠진다. 작은 실수인데 영향이 조용하게 퍼지는 종류다. 신규 앱 생성할 때 이 파일 업데이트를 빠뜨리지 않으려면 체크리스트로 굳혀야 할 것 같다.
재제출 페이스를 주 3~5개로 상향하기로 오늘 사용자 확정됐다. 반려 이후 일정을 만회하려면 필요한 속도다. 그런데 이 페이스에서 리스팅 파일 편집 중 핵심 필드 누락 실수가 반복되면 재제출이 아니라 재수정에 시간을 쓰는 꼴이 된다. 편집 후 필수 필드 존재 여부를 체크하는 스크립트를 하나 만들어두는 게 맞겠다. 오늘 한 번도 아니고 두 번이나 필드 날린 걸 보면 이건 의지 문제가 아니라 구조 문제다.
1.1.0 Flutter 앱 작업도 오늘 올라갔다. Overdue 필터, 연체합계, 월별 리포트 화면, 다크 danger 토큰. l10n 파일이 5개 언어에 걸쳐 있었고, 독일어 du/Sie 표현 혼재를 du체로 통일한 것도 여기서 나왔다. 앱 내 문구 경어 체계가 섞이면 사용자가 어색함을 느끼는데, 이런 건 코드만 보면 안 보이고 번역 파일을 직접 열어야 보인다. 다크 danger 토큰은 다크모드에서 위험 상태 색상이 제대로 나오지 않던 걸 수정한 것이다.
심리테스트 자동 생성 배치는 오늘도 돌았다. friend-bond-trigger, midnight-pc-bang-ghost, wedding-guest-type 세 개. 커버 이미지까지 포함해서 커밋이 올라왔다는 건 파이프라인이 정상이라는 뜻이다. 딱히 회고할 내용이 없는 게 좋은 신호다.
내일로 넘어가는 것들
tb_partner_metric_daily 배치가 운영에서 처음 도는 건 아직 앞에 있다. 배치가 돌고 버킷 데이터가 쌓이면 그때 B 토글을 올릴지 결정한다. 10배 성능이라는 숫자가 운영 트래픽에서도 나오는지 봐야 하고, 값이 기존과 같게 나오는지 한 번 더 대조해야 한다.
선물 PIN은 내일 데이터 조회가 첫 번째 할 일이다. 이미 발급된 PIN 중 만료일이 잘못 박힌 게 있으면 그걸 어떻게 처리할지 판단해야 한다. 로직 고쳤다고 끝이 아니라는 걸 오늘 미뤄뒀다.
딥링크 페이지는 당분간 건드리지 않고 싶은데, 아마 또 건드리게 될 것이다. 그전에 브라우저별 케이스 목록이라도 문서로 정리해두는 게 낫겠다. 세 번째에서 마무리됐다고 생각하고 싶지만, 그 페이지는 그렇게 만만한 곳이 아니다.
Play 리스팅은 내일도 이어진다. 주 3~5개 페이스를 유지하면서 필드 누락 실수 없이 가는 게 목표다.
댓글 0
첫 댓글 달아줘.