발화 시점 바꾸다 설계 절반을 뒤집은 날
목차
아침을 balanceDashboard.jsp 748행으로 시작했다는 게 좀 상징적이다. EL 오타 하나 때문에 운영 잔액 대시보드 JSP 컴파일이 죽어 있었다. name이랑 parentLevelCode 병합 표현식에서 오타가 끼어 있었고, 그 탓에 화면이 그냥 뻗어 있는 상태였다. 빠르게 들어가서 수정하고 나오긴 했는데, 아침 첫 커밋이 오타 픽스라는 건 기분 좋은 시작이 아니다. 그냥 쿨하게 넘기고 다음 걸 봤다.
발화 시점 하나만 바꾸면 된다고 생각했다
GIFT_RECEIVED 웹훅 작업은 일찍 시작했다. 요청 자체는 단순했다. 파트너 쪽에서는 수신자가 선물을 수령하는 시점이 아니라, 발송이 완료된 시점에 웹훅을 받고 싶다는 거였다. 파트너 입장에서 생각하면 당연한 말이다. 쿠폰이 나갔다는 사실이 중요하지, 수신자가 언제 열어봤는지는 파트너의 관심사가 아니니까. 수령을 기다렸다가 발화하면 웹훅이 며칠씩 밀릴 수도 있다.
그런데 발화 시점을 옮기는 게 단순하지 않았다. 선물이 나가는 경로가 세 가지였다.
- 기본 전환 플로우 (gift-package 쪽)
- 파트너 포탈에서 재선물
- 관리자 콘솔에서 재선물
세 경로가 각자 다른 클래스, 다른 서비스 레이어에 걸쳐 있었다. 공통 훅을 만들어서 집어넣을 수도 있었는데, 각 플로우의 완료 시점이 미묘하게 달라서 공통화가 애매했다. 결국 세 지점에 각각 직접 심었다. 중복처럼 보이지만, 지금 단계에서 억지로 추상화하는 것보다 명시적으로 세 곳에 있는 게 나중에 추적하기 편하다고 판단했다. 물론 세 곳을 다 찾아가서 넣어야 한다는 피곤함은 있었다.
기존의 수령 대기 발화 지점은 제거했다. 이건 남겨두면 안 된다. 발송 때 한 번, 수령 대기 때 또 한 번 가면 파트너 쪽이 중복 처리를 해야 하니까.
dedup이 맞지 않았다
발화 시점을 옮기면서 기존 dedup 구조가 맞지 않는다는 걸 발견했다. 기존엔 "건당 1회" 게이트를 걸고 있었다. 쿠폰 건 기준으로 이미 웹훅이 나간 적 있으면 안 내보내는 방식이었다.
재선물이 들어오면 이게 깨진다. 파트너가 A한테 보낸 선물을 B한테 재선물하면, 수신자가 바뀌었으니 웹훅이 새로 나가야 한다. 근데 기존 건당 게이트는 이미 같은 건이라고 보고 막아버린다.
해결 방향은 dedup 키를 giftSeq 기준으로 올리는 것이었다. 선물 회차가 새로 생기면 새 키, 같은 회차 안에서 중복만 막는다. 이렇게 하면 재선물 때 giftSeq가 새로 발급되니까 자연스럽게 웹훅이 나간다. 쿼리 매퍼를 건드려서 dedup 체크와 삽입 쪽 둘 다 giftSeq를 키로 쓰도록 수정했다.
이 작업은 발화 시점 변경 커밋과 별도로 쪼갰다. 이유가 있다. 발화 시점 변경은 "어디서 쏠 것인가"고, dedup 변경은 "언제 막을 것인가"다. 두 개가 섞이면 리뷰할 때 논리를 추적하기 어렵다. 커밋 단위를 나누는 게 귀찮을 때도 있지만, 나중에 어떤 변경이 어떤 버그를 만들었는지 역추적할 때 이 습관이 큰 차이를 만든다.
API 문서도 업데이트했다. .claude/docs/api-reference.md랑 포탈 쪽 api-docs.jsp 둘 다. 발송 완료 시점 발화, giftSeq당 1회 제한, giftSeq 필드 스펙. 파트너 개발자가 이걸 보고 연동하는 거니까, 여기가 틀리면 파트너 쪽에서 엉뚱한 타이밍에 맞춰 코드 짜놓고 나중에 클레임이 온다. 문서가 귀찮은 건 사실인데, 연동 API 문서는 코드랑 같이 가야 한다.
11.5초를 0.17초로
파트너 포탈 주문 목록 얘기를 안 할 수가 없다.
11.5초. 이건 느린 게 아니라 죽어 있는 거다. 10초가 넘어가면 타임아웃 설정에 따라 에러로 떨어질 수도 있고, 그 이전에 사용자가 이미 페이지를 닫는다. 파트너 매니저가 주문 들어왔나 확인하려고 들어갔다가 11초 넘게 기다려야 한다면 그냥 안 쓰게 된다.
원인을 파보니 pm 폰매칭 상관 서브쿼리 문제였다. 주문 목록 쿼리에서 파트너 매니저 정보를 붙이기 위해 상관 서브쿼리를 쓰고 있었는데, 이게 주문 행마다 실행됐다. 주문 건수가 어느 정도 쌓이면 이 패턴은 선형이 아니라 거의 제곱으로 불어난다. 처음에 주문이 몇 십 건일 땐 괜찮았겠지만, 데이터가 쌓이면서 서서히 느려지다가 어느 순간 박살이 났을 거다.
사전집계 조인으로 바꿨다. 폰매칭 결과를 인라인 뷰에서 먼저 집계한 다음 한 번의 조인으로 붙이는 방식. 행마다 서브쿼리가 실행되는 게 아니라, 집계가 한 번에 끝나고 결과를 가져다 쓰는 구조다.
0.17초. 수치가 너무 확연해서 처음 보고 잠깐 다시 실행해봤다. 11.5초에서 0.17초면 거의 68배 차이다. 이런 게 개발의 재미라면 재미다. 쿼리 구조 하나 바꿨는데 사용자 경험이 완전히 달라지는 순간. 그리고 이 성능이 확보됐으니까 바로 12초 폴링 실시간 갱신을 붙일 수 있었다. 0.17초 쿼리면 12초마다 때려도 서버에 부담이 없다. 만약 아직도 11.5초짜리였으면 폴링은 꿈도 못 꿨을 거다. 성능 개선을 먼저 하고 기능을 얹은 순서가 맞았다.
주문 목록이 이제 새로고침 없이도 갱신된다. 사소해 보이지만 포탈 사용성에서 체감이 크다.
요율 스냅샷은 나중을 위한 작업
kp-pay 가상계좌 입금 웹훅에 요율 스냅샷을 추가했다. 오늘 당장 문제가 되는 버그가 아니라, 나중에 생길 게 뻔한 버그를 미리 막는 작업이었다.
기존 구조에서는 입금 웹훅이 들어오면 그 시점의 현재 요율을 읽어서 정산 계산에 쓴다. 이러면 요율이 변경되는 순간, 과거 입금 건에도 새 요율이 소급 적용된다. 정산이 흔들린다. 이런 종류의 데이터 정합성 문제는 당장은 드러나지 않다가 요율 변경 시점에 터진다. 그때 가서 수습하려면 입금 내역과 요율 이력을 대조해가며 재계산해야 하는데, 그게 훨씬 힘들다.
입금 시점 요율을 스냅샷해서 저장하도록 바꿨다. 이제 나중에 요율이 바뀌어도 과거 정산에는 영향 없다. 커밋 자체는 짧지만 이런 작업이 쌓여야 정산이 믿을 만한 시스템이 된다. 당장 눈에 보이는 성과가 없는 작업이라 우선순위에서 밀리기 쉬운데, 오늘 짬을 내서 넣었다. 나중에 고마울 것 같다.
CNC 계산기, 하루에 스캐폴드부터 코어까지
오후 중반부터 맥락이 완전히 바뀌었다. CNC 가공 계산기 앱, cncfeeds.
bendwise에서 포크해서 시작했다. bendwise는 판금 계산기로 같은 계산기 앱 계열이라 기반 구조가 비슷하다. sed 스윕으로 도메인 식별자를 전부 교체하고, 앱 서명 설정이랑 스토어 아티팩트를 걷어냈다. CLAUDE.md랑 README는 CNC 도메인에 맞게 처음부터 다시 썼다. 이 스캐폴딩 작업은 기계적이지만 실수가 잦다. 남은 bendwise 문자열 하나가 앱 이름이나 스토어 설명에 섞여 올라가면 심사에서 걸리거나 출시 후에 어색하게 남는다. 꼼꼼하게 확인했다.
디자인은 machine-teal 팔레트로 잡았다. 공업 느낌이 있으면서 대비가 충분한 색이다. WCAG AA 검증도 했다. 나중에 접근성 리뷰에서 걸리면 더 번거로우니까 처음부터 잡아두는 게 맞다. 앱 아이콘은 엔드밀로 했다. CNC 도메인이니까 당연한 선택이고, 런처 아이콘은 hdpi, mdpi, xhdpi, xxhdpi, xxxhdpi 다섯 해상도를 전부 새로 생성했다.
핵심은 cutting math engine이었다. MIT 재료 데이터 19개 그룹을 JSON으로 넣었다. 재료마다 절삭 속도 범위와 이송속도 계수 등이 다르고, 이걸 기반으로 엔드밀 지름과 회전수, 이송속도, 절삭 깊이 등을 계산한다. 직접 손 계산으로 검증하면서 단위 테스트를 짰고 10개 전부 통과했다. 단위 테스트가 있어야 나중에 수식을 수정할 때 자신 있게 건드릴 수 있다. 없으면 계산기 앱인데 계산이 맞는지 확신을 못 하게 된다.
UI는 speeds/feeds, tap drill, bolt circle, power, reference 다섯 섹션으로 재작성했다. 각 섹션이 CNC 가공에서 자주 쓰이는 계산 유형이다. tap drill은 나사 탭 작업 전에 드릴 구멍 크기를 계산하는 거고, bolt circle은 원형 배열 볼트 구멍 좌표 계산, power는 절삭 동력 추정이다. 분석 버그 0개. 테스트 10/10.
하루에 스캐폴드에서 코어 로직과 UI까지 간 건 속도가 빠른 편이다. bendwise 구조를 재활용한 덕분이지만, 계산 도메인이 완전히 달라서 수식과 재료 데이터는 처음부터 짰다. 계산기 앱을 몇 개 해보면서 패턴이 어느 정도 잡힌 것 같다. 스캐폴딩 할 때 뭘 남기고 뭘 교체해야 하는지 감이 생겼다.
CAS AdMob ID, 세 번 반복
CAS에서 각 앱 AdMob ID를 발급받았고, 이걸 실제 앱들에 반영하는 작업이 세 앱에 걸쳐 있었다. pilmori, gwangclock, noiseproof.
각 앱마다 iOS Info.plist의 GADApplicationIdentifier를 CAS 지정 ID로 교체하고, pubspec.yaml 버전을 올리고, 제출 스크립트에 재제출 라인을 추가했다. 이게 세 번 반복됐다. 기계적인 작업이지만 ID를 잘못 넣으면 광고가 아예 안 나온다. pilmori 커밋 메시지에 "광고 미노출 픽스"라고 되어 있는 걸 보면, 이미 잘못된 ID로 올라가 있었던 상태였던 것 같다.
CAS 미디에이션 연동의 함정이 이 부분이다. AdMob 콘솔에서 직접 발급받은 앱 ID와 CAS가 지정해주는 AdMob ID가 다르다. CAS가 중간에서 미디에이션 레이어가 되면, SDK에 넣을 AdMob ID는 내 AdMob 콘솔 ID가 아니라 CAS가 별도로 발급한 값이어야 한다. 이걸 놓치면 광고 요청이 엉뚱한 앱 ID로 가서 노출이 안 되거나 수익이 집계가 안 된다. 처음 CAS 연동할 때 한 번은 이 함정에 빠지게 되어 있는 것 같다.
Podfile.lock도 두 건이 있었다. 앱 레포 하나, iOS 서브 디렉토리 하나. CAS Optimal 실링크가 반영됐는데 lock 파일만 업데이트가 안 된 잔여분이었다. 그냥 chore지만 안 넣으면 다른 환경에서 빌드할 때 의존성이 어긋난다.
ASC 50앱 실측, 자동 생성은 알아서
ASC 대시보드도 정리했다. 라이브 40, 심사 중 10. 50앱이면 어딘가 상태가 스테일해지기 마련인데, 오늘 직접 확인해서 카드 24개 라벨을 정정하고 신규 8종을 추가했다. 실측이라고 명시한 이유가 있다. 문서에 남은 값은 언제든 스테일해질 수 있고, 실제 상태와 달라지면 가장 문제가 생기는 게 제출 스케줄이나 버전 트래킹이다. 주기적으로 실측 맞춰두는 게 답이다.
심리테스트 자동 생성은 오늘도 파이프라인이 돌아서 세 개 나왔다. 다앙 거래 유형, 친구-연애-경쟁, 자정의 박물관 시선. 커버 이미지 셋, 콘텐츠 JSON 셋. 이건 시스템이 자동으로 하는 거라 신경 덜 쓰이는데, 정상적으로 올라왔다는 건 파이프라인이 살아있다는 뜻이니까 그걸로 됐다.
커밋이 스물네다섯 개인 날이지만 정신없이 찍은 날이라는 느낌은 아니다. 크게 보면 백엔드 웹훅 개편과 쿼리 성능, 신규 CNC 앱 스캐폴딩, 광고 ID 교체, 대시보드 정리가 병렬로 돌아갔고, 각 맥락 안에서는 어느 정도 깊이 있게 들어간 것 같다.
오늘 제일 뿌듯한 건 11.5초 쿼리를 0.17초로 만든 것이다. 제일 복잡했던 건 GIFT_RECEIVED dedup 재설계였고. CAS AdMob ID 교체는 세 번 반복이었지만 반드시 필요한 반복이었다.
요율 스냅샷은 당장 눈에 보이는 성과가 없는 작업인데, 이런 게 쌓여야 나중에 정산 데이터를 믿을 수 있게 된다. 오늘 넣어둔 게 잘한 것 같다.
댓글 0
첫 댓글 달아줘.