결함 잡고 SEO 뿌리고, 하루 다 갔다
목차
오전 첫 이슈 리포트에 "충전 페이지에서 계속 뺑뺑이 돈다"는 표현이 있었다. 전형적인 UX 버그 냄새다. 그런데 결제 흐름을 조금만 파보면 이런 무한 루프는 거의 항상 UX가 아니라 데이터 문제다. 게이트에서 조건 분기를 타다가 필요한 행이 없으면 어디로도 못 가고 같은 체크를 반복하는 구조가 되는 것이다. 이번도 그랬다. 1원 인증을 완료한 회원인데 pay_member 행 자체가 없었다. 가입 흐름 어딘가에서 insert가 누락된 케이스들이 운영 DB에 쌓여 있었던 것이다.
이런 버그가 발견하기 어려운 이유는 정상 흐름에서 절대 재현이 안 된다는 점이다. QA 계정은 가입 절차를 처음부터 끝까지 밟았으니 당연히 행이 있다. 문제는 중간에 예외 경로를 탄 실 사용자 계정들인데, 이게 운영 환경에서만 터진다. 결국 가입 흐름의 어느 분기에서 롤백 없이 부분 커밋이 일어났을 가능성이 높다. 소셜 로그인 콜백처럼 외부 서비스가 끼는 경로는 특히 의심해볼 만하다.
결제 결함: 원자성이 깨지면 정합성도 깨진다
pay_member 케이스를 정리한 다음 가상계좌 충전 결함 4건, 정산 결함 5건을 이어서 잡았다. 이 중 P1이었던 건 운영자 커미션 확정 로직이다. 커미션 확정이 원자적으로 처리되지 않으면 - 커미션 상태는 업데이트됐는데 정산 원장 기록이 빠진다거나 - 나중에 숫자를 맞추려 해도 어느 시점이 기준인지 알 수 없게 된다. 재무 감사 추적도 의미를 잃는다.
정산 로직에서 트랜잭션 경계를 설계할 때 기준으로 삼는 패턴은 이렇다:
BEGIN TRANSACTION;
-- 1. 커미션 상태 확정 (pending 체크로 이중 확정 방지)
UPDATE operator_commission
SET status = 'confirmed', confirmed_at = NOW()
WHERE id = :id AND status = 'pending';
-- 2. 정산 원장 기록
INSERT INTO settlement_ledger (operator_id, amount, type, ref_id)
VALUES (:operator_id, :amount, 'commission', :commission_id);
-- 3. 잔액 반영
UPDATE operator_balance
SET balance = balance + :amount
WHERE operator_id = :operator_id;
COMMIT;
단순해 보이지만 ORM 서비스 레이어를 여러 메서드로 쪼갰을 때 트랜잭션 컨텍스트가 어느 메서드까지 살아있는지 흐릿해지기 쉽다. 커미션 확정 서비스와 원장 기록 서비스가 분리되어 있고, 두 번째 호출 직전에 예외가 나면 첫 번째만 커밋된 채로 끝난다. 이번 수정에서 경계를 명확하게 묶으면서 그 부분이 해소됐다. 운영자 커미션 같은 금전 관련 연산은 애초에 한 트랜잭션 안에 다 들어가야 한다는 걸 다시 확인했다. 오전 내내 결제 코드만 봤더니 점심 먹으러 나갈 때 머리가 꽤 빠근했다.
SEO 롤아웃과 소셜 봇 개선
오후는 SEO 작업이었다. 운영자 표기를 푸터에 넣고, Organization JSON-LD를 달고, 운영자 링크 도착지를 /en/about/으로 통일하는 걸 9개 사이트에 반복했다. 솔직히 지루하다. 사이트마다 템플릿 구조가 조금씩 달라서 복붙이 안 되고 일일이 손을 대야 한다.
그래도 E-E-A-T 관점에서 피할 수 없는 작업이다. 구글이 Experience, Expertise, Authoritativeness, Trustworthiness를 평가할 때 법인·운영자 정보가 일관되게 노출되는지를 본다. JSON-LD로 Organization 스키마를 심어두면 크롤러가 해석하기 쉬워지고 Knowledge Panel 노출 가능성도 올라간다. 신뢰 시그널을 쌓는 작업은 결과가 바로 안 보이니까 미루기 쉬운데, 미루면 미룰수록 경쟁 사이트와 격차가 난다.
오늘 9개 사이트 공통으로 반영한 항목:
- 푸터 운영자 표기 (법인명, 대표자명)
<script type="application/ld+json">Organization 스키마 삽입- 운영자 링크 href
/en/about/통일 - 언어별 hreflang 누락 여부 점검
한 사이트에서 패턴을 잡아두면 다음 사이트는 빨라진다. 9개 쯤 되면 거의 체화가 된다. 반복이 지루한 건 사실인데, 이걸 자동화하려면 사이트 구조를 충분히 표준화해야 한다. 지금은 그 표준화가 덜 된 상태라 손 작업이 남는 것이다.
소셜 봇은 오늘 가장 만족스러운 작업이었다. AI가 생성한 카피가 쌓이면 계정 전체가 광고 봇처럼 보이기 시작한다는 걸 오래 전부터 알고 있었는데 정리를 미뤄왔다. 이모지 남발, "지금 바로 만나보세요" 식 문장, "추천"·"핫픽" 같은 라벨 접두어 - 이게 누적되면 자연스러운 계정처럼 안 읽힌다. 플랫폼 알고리즘이 어떻게 판단하는지는 알 수 없지만, 사람이 봐도 티가 나면 어딘가에는 영향이 있을 것이다. 전부 걷어내고 실데이터 기반 팩트 문장으로만 구성하도록 프롬프트를 다시 짰다.
발행 리듬도 손봤다. 매시 cron이 뜨면 포스팅이 3~4개 연속으로 나가는 배치 느낌이 생겼는데, 랜덤 딜레이를 얹어서 그 패턴을 분산시켰다. LLM이 외부 지식을 끌어와서 옛 그룹명이나 불확실한 디테일을 지어내는 문제는 프롬프트에 명시적으로 차단했다. "제공된 데이터 필드 외의 외부 지식을 사용하지 마라"는 지시를 명확하게 박아두는 것이다. 이게 없으면 LLM이 그럴듯하지만 틀린 사실을 채워 넣는데, 아이돌 관련 콘텐츠에서 잘못된 정보가 나가면 팬덤 반응이 빠르다. 진작 했어야 했는데 오늘에야 손댔다.
안드로이드 크래시: 첫 실행에서만 터지는 것들
AGP9에서 R8 minify가 기본 ON으로 바뀐 게 원인이었다. WorkDatabase_Impl 생성자가 스트립되면서 첫 실행 시 크래시가 나는데, 두 번째 실행부터는 정상이라 재현이 까다롭다. "내 기기에서는 안 터진다"가 나오기 딱 좋은 유형이다. 신규 설치 사용자에게만 영향이 가고, 기존 설치 기기나 QA 환경에서는 잡히지 않는다.
flaky 크래시 중에서도 첫 실행에서만 터지는 건 특히 무섭다. Crashlytics에 찍히는 스택 트레이스가 minify 이후라 심볼이 깨져 있으면 초반에 원인 특정도 어렵다. 오늘은 일단 minify/shrink를 명시 OFF로 처리하고 빌드 번호를 올려서 긴급 배포했다.
임시방편인 걸 알면서 릴리즈해야 하는 상황은 찝찝하다. 근본 해결은 ProGuard 룰에 WorkDatabase_Impl을 keep 처리하거나 AGP9 마이그레이션 가이드를 제대로 따라가는 것인데, 오늘 당장 그 여유는 없었다. minify를 끄면 APK 사이즈와 빌드 시간 모두 영향을 받으니 오래 둘 수 있는 상태가 아니다. 이슈 티켓에 재현 조건과 임시 처리 내용을 남기고 다음 스프린트로 넘겼다.
데모 4개 등록도 오늘 끝냈다 - camera-filter, franchise-intake, coupon-i18n, webinar-platform. 명암비 보정 같은 잔손질이 생각보다 많이 붙었고, 썸네일 작업이랑 뱃지 수치 맞추는 게 코딩보다 시간 대비 피로감이 높다. 이런 작업은 한 타임에 몰아서 하는 게 맞는데, 오늘은 결함 잡는 사이사이에 끼워서 컨텍스트 전환 비용이 더 들었다.
하루에 커밋이 너무 많았다. 집중이 분산된 날이었는데 P1 결함 하나는 막았으니 그걸로 됐다. 내일은 인덱싱 상태 확인이랑 소셜 봇 실발행 모니터링부터 시작한다.
댓글 0
첫 댓글 달아줘.