출시하고 나니 에러가 달라졌습니다
지난 글에서는 광고 구독 기능을 처음 백엔드까지 짜면서 만난 에러들을 정리했습니다. 404는 옆집 서버가 줬고, 결제 결과는 조용히 사라졌고, 400을 만든 범인은 제 화면이었습니다.
이번엔 출시하고 난 뒤 이야기입니다. 출시하고 나니 에러의 종류가 바뀌었습니다. 개발할 땐 "요청이 안 된다"였다면, 출시 뒤엔 "데이터가 이상하다"였습니다. 그리고 이런 건 코드를 고쳐서 배포해도 고쳐지지 않았습니다.
1. 갱신했더니 쿠폰이 사라졌습니다
광고는 한 달 단위 구독이라, 매달 갱신될 때 새 회차를 만들고 쿠폰도 새로 만듭니다. 그런데 기존 시스템에서 넘어온(이관) 매장들이 처음 갱신되던 날, 갱신된 매장 5곳 모두 새 회차 쿠폰이 0장이었습니다. 갱신 슬랙 알림에도 "쿠폰 : 없음"이 찍혔습니다.
원인은 갱신 로직이 이관 쿠폰을 복제 대상에서 일부러 뺀 데 있었습니다. 이관 쿠폰은 발급 상한이 "평생 누적" 기준이라, 그대로 복제하면 한 달 치 예산이 평생 치로 잡힙니다. 예산이 터지는 건 막았지만, 대신 이관 쿠폰만 있던 매장은 갱신 후 쿠폰이 하나도 없게 됐습니다.
코드는 이관 쿠폰을 복제하는 대신 발급 종료일을 새 회차 끝까지 늘리고, 새 회차에 다시 연결하도록 고쳤습니다. 그런데 코드를 고쳐도 이미 갱신이 지나간 5곳은 돌아오지 않습니다. 코드는 앞으로 일어날 일만 바꿉니다. 이미 끊긴 매장은 DB에서 직접 발급 종료일을 늘리고 회차 연결을 다시 만들어야 했습니다.
프론트에선 배포하면 끝이었습니다. 새로고침하면 다 새 코드로 도니까요. 서버는 아니었습니다. 이미 저장된 데이터는 배포해도 그대로입니다.
새로고침으로 달라지지 않는다는 게 좀 무서웠습니다. 심지어 데이터만 고친다고 끝나는 것도 아니었습니다. 고친 데이터가 다른 곳에서 또 어떻게 꼬일지 모른다는 생각이 드니... 더 무서웠습니다.
2. 바꾸려면 갱신일에만
살려 놓은 이관 쿠폰은 또 문제를 만들었습니다. 사용량도 평생 누적이라 달이 바뀌어도 리셋이 안 되니까, 누적이 쌓여서 한도를 다 쓴 걸로 잡히는 매장이 나왔습니다. 손님 앱은 "소진", 사장님 광고센터는 "잔여 있음". 같은 쿠폰이 화면마다 다르게 보였습니다.
그럼 새 쿠폰으로 바꾸면 되지 않을까? 바로 바꾸려다 멈췄습니다. 한 매장은 60장 중 48장이 쓰인 상태였는데, 지금 새 쿠폰으로 바꾸면 사용 카운트가 0부터 다시 시작합니다. 이미 쓴 48장은 없던 일이 되고, 그 회차 예산이 다시 열립니다. 한 달 예산을 두 번 쓰게 되는 거죠.
| 갱신일까지 기다리면 | 지금 바로 바꾸면 | |
|---|---|---|
| 이번 회차 사용 카운트 | 48 / 60 | 0 / 60 (새 쿠폰) |
| 이번 회차에 더 쓸 수 있는 쿠폰 | 12장 | 60장 |
| 이번 회차에 쓰이는 쿠폰 합계 | 최대 60장 | 최대 108장 |
그래서 원칙을 하나 세웠습니다.
쿠폰 교체는 갱신일(회차가 시작해서 사용이 0인 날)에만.
그 매장은 실제로 갱신일을 기다렸다가 바꿨습니다. 사고가 나기 전에 멈춘, 몇 안 되는 순간이었습니다 😅
3. 에러 없이 옛 시스템에서 돌고 있던 매장
배포하고 나서 불안했습니다. 그래서 구독 갱신과 결제는 슬랙으로 알림이 오게 해 두고, 매일 슬랙 로그를 보면서 쿠폰 발급과 결제 내역을 DB에서 직접 확인했습니다.
그러다 갱신이 돌았는데 쿠폰이나 구독 이력이 보이지 않는 매장을 발견했습니다. 조회해 보니 새 구독이 아니라 옛 시스템에서 돌고 있었습니다. 결제는 멀쩡히 되고 있었고, 에러도 없었습니다.
기존 매장을 새 구독으로 옮기는 이관은 딱 한 번 돌린 작업이었습니다. 돌리는 순간의 데이터를 보고 옮기고 끝입니다. 그런데 그때 이 매장은 결제 실패로 재시도 중이었습니다. 결제가 실패하면 종료일이 하루씩 밀리니까, 이관이 돌던 날엔 그날 끝나는 구독처럼 보였습니다.
// 결제일이 7일인 매장
8/7 결제 실패 → 종료일 8/8
8/8 결제 실패 → 종료일 8/9
8/9 결제 실패 → 종료일 8/10 // 8/10 이관이 본 모습
이관에는 곧 끝나는 구독을 빼는 조건이 있었습니다. 당일에 끝나는 구독을 옮기면 회차가 바로 닫혀 버리기 때문입니다. 재시도도 맞고 이 조건도 맞는데, 둘이 만나면서 이 매장이 빠진 겁니다.
그 뒤에 결제가 성공했습니다. 결제 배치는 옛 구독과 새 구독을 가리지 않고, 그날이 결제일인 대상을 전부 결제합니다. 이관된 매장은 옛 결제 대상을 꺼 뒀지만, 빠진 매장은 그대로 켜져 있었습니다. 그래서 결제는 계속 되는데 새 구독에는 없는 상태가 됐습니다.
같은 매장이 더 있는지, 옛 시스템에서 결제가 돌고 있는데 새 구독이 없는 곳을 전부 조회했습니다. 48건이 나왔는데 대부분은 결제 없이 노출만 하는 행이라 이관 대상이 아니었고, 실제 후보는 7곳이었습니다.
이관 기능은 다른 개발자분이 만든 거라 여쭤보고, 실행 전에 결과를 미리 보는 기능으로 판정부터 확인한 뒤 제가 직접 처리했습니다. 7곳은 세 갈래로 나뉘었습니다.
- 2곳은 판정을 통과해서 바로 옮겼습니다.
- 3곳은 말일에 결제하는 매장이라, 결제가 성공한 뒤에 옮기기로 했습니다.
- 2곳은 이용권으로 결제하는 매장이었습니다. 새 구독은 카드 결제만 지원해서 옮기지 못했습니다.
에러가 없으니 알람도 없었습니다. 에러가 나지 않는 문제는 직접 들여다봐야 보였습니다.
4. "100장인데 왜 400장이지?"
선착순 쿠폰은 받은 사람 수로 마감됐습니다. 그런데 손님들이 받기만 하고 안 쓰는 경우가 많았습니다. 쿠폰은 금방 "마감"이 뜨는데, 실제로 쓰인 수는 그만큼 되지 않았습니다. 안 쓴 쿠폰은 유효기간이 끝나면 수량이 돌아오긴 합니다. 하지만 그날 바로 쓰려는 손님은 이미 쓸 수가 없었고, 매장 문의가 꾸준히 들어왔습니다.
그래서 기준을 "받은 사람 수"에서 "실제로 쓴 사람 수"로 바꾸자는 얘기가 나왔습니다. 저는 바로 바꾸지 않고 고민부터 했습니다. 두 기준의 장단점이 갈렸기 때문입니다.
| 받은 사람 수 기준 | 실제로 쓴 사람 수 기준 | |
|---|---|---|
| 좋은 점 | 받아 둔 쿠폰은 무조건 쓸 수 있음 | 받기만 한 손님 때문에 마감되지 않음 |
| 아쉬운 점 | 다 받아 가면 그날 쓰려는 손님이 못 씀 | 받아 둔 쿠폰이 결제 순간에 막힐 수 있음 |
결국 실제로 쓴 사람 수로 바꾸고, 받기는 넉넉하게 열어 두기로 했습니다. 받은 쿠폰이 전부 사용으로 이어지지 않으니, 받는 시점에 수량을 끊으면 실제 사용이 목표에 못 미치기 때문입니다.
사장님이 쿠폰을 20장으로 정하면, 실제로 쓸 수 있는 건 20장이고 받을 수 있는 건 그 4배인 80장입니다. 배수는 영업과 협의한 3~4배 중 4배로 정했습니다. 20장이 다 쓰이면 쿠폰은 마감되고, 더 받을 수도 없습니다.
// 사장님이 20장으로 정했을 때
실제로 쓸 수 있는 수량 20 // 다 쓰이면 마감
받을 수 있는 수량 80 // 20 × 4
사장님 화면엔 정한 수량만 보이게 했습니다. 4배는 내부 사정이니까요.
나중에 출시 뒤 데이터를 집계해 보니, 받은 쿠폰 중 실제로 쓰인 건 32.8%였습니다. 세 장을 받으면 한 장 정도만 쓰이는 셈입니다.
그런데 갱신 슬랙 알림을 보다가 깜짝 놀랐습니다. "쿠폰 400장". 100장으로 계약한 매장인데 400장이라니? 알고 보니 알림이 그 내부 숫자(받기 한도)를 그대로 찍고 있었습니다.
화면에서는 열심히 숨겼는데, 알림이라는 다른 출구로 새어 나간 겁니다. 숫자를 보여 주는 곳은 화면만이 아니었습니다.
5. "두 달 무상"을 0원 결제로 처리한 방법
영업에서 어느 매장과 "두 달 무상, 쿠폰은 계속"으로 합의했습니다. 그런데 코드에는 "무상"이라는 개념이 없습니다. 결제 배치는 매일 정해진 시간에 그날이 결제일인 구독을 골라 결제하고, 결제가 성공해야 다음 회차와 쿠폰이 만들어집니다.
처음 떠오르는 방법은 "결제를 멈추자"입니다. 그런데 이게 함정이었습니다. 결제 대상에서 빼면 배치가 그 구독을 건너뜁니다. 갱신이 안 되고, 쿠폰 발급 기간이 끝나고, 자정 배치가 쿠폰을 꺼 버립니다. 실제로 환불 처리하면서 결제 대상에서 빠진 매장이 이렇게 됐는데, 구독 상태는 여전히 ACTIVE라 화면과 실제가 어긋나 있었습니다.
그래서 반대로 갔습니다. 결제를 멈추지 말고, 결제 금액을 0원으로. 결제 코드를 따라가 보니 0원이면 PG를 부르지 않고 성공으로 처리되고, 성공 콜백은 평소와 똑같이 갑니다. 그러면 갱신도 쿠폰도 평소대로 돕니다.
실행할 땐 이렇게 했습니다.
- 결제 대상 행의 금액만 0으로
- WHERE에 "지금 값" 조건을 넣어서, 두 번 실행되거나 엉뚱한 행을 건드리지 않게
- 되돌리는 SQL을 미리 써 두기
- 배치가 돈 뒤 하나씩 확인: 결제 로그 0원 성공, 주문 완료(PG 호출 없음), 다음 결제일 +1달, 새 회차, 새 쿠폰, 슬랙 알림까지
이 배치는 dev에서 재현하기가 어려워서, 상용에 처음 적용한 날 배치가 돈 직후 위 항목을 전부 조회해서 확인했습니다.
DB를 직접 고치면서 생긴 습관
처음엔 DB 툴에서 여러 문장을 한꺼번에 실행했다가, 일부만 실행되거나 두 번 실행되는 일을 겪었습니다. 그 뒤로 습관이 생겼습니다.
- 한 번에 한 문장만
- 실행 전에 같은 WHERE로 SELECT 해서 건수부터 확인
- WHERE에 "지금 값" 조건 넣기 (두 번 돌려도 두 번째는 0행)
- 실행 뒤 affected rows가 예상과 같은지 확인
- 되돌리는 SQL을 먼저 써 두기
예를 들어 쿠폰 발급 종료일을 늘릴 때는 이런 모양이 됩니다. (테이블·컬럼 이름과 값은 예시입니다)
-- 1. 같은 WHERE로 먼저 조회해서 건수 확인 (예상: 1행)
SELECT id, end_date FROM coupon_type
WHERE id = 42 AND end_date = '2026-08-31';
-- 2. "지금 값" 조건을 건 UPDATE (두 번 돌려도 두 번째는 0행)
UPDATE coupon_type SET end_date = '2026-09-30'
WHERE id = 42 AND end_date = '2026-08-31';
-- 3. 되돌리는 SQL은 실행 전에 미리 써 두기
UPDATE coupon_type SET end_date = '2026-08-31'
WHERE id = 42 AND end_date = '2026-09-30';
프론트에선 실수해도 새로고침하면 됐습니다. DB는 새로고침이 없습니다.
AI 답도 그대로 믿지 않게 됐습니다
이 작업은 AI와 같이 했습니다. 로그를 읽게 하고, 코드 흐름을 따라가게 하고, 쿼리를 짜게 했습니다. 다만 운영 DB를 고치는 쿼리는 제가 직접 실행했습니다. AI 답을 그대로 믿으면 안 되는 순간이 있었기 때문입니다.
이관에서 빠진 매장을 처리할 때, AI는 4곳을 바로 옮길 수 있어 보인다고 정리했습니다. 실행 전에 미리 보기를 돌려 보니 통과한 건 2곳이었습니다. 나머지 2곳은 이용권으로 결제하는 매장이었습니다.
쿠폰이 언제 어떻게 만들어졌는지 추적할 때도 그랬습니다. 사내 AI 봇은 데이터만 보고 쿠폰이 자동으로 만들어졌다고 추정했습니다. 회차별로 다시 따라가 보니 사람이 만든 쿠폰이 섞여 있었고, 자동으로 만들어진 건 일부였습니다.
그래서 AI 답은 출발점으로만 씁니다. 미리 보기나 실제 데이터 한 건으로 확인하고 나서 움직입니다.
마무리
출시하고 보니 서버에는 코드만 있는 게 아니었습니다. 이미 쌓인 데이터, 매일 도는 배치, 영업과 한 약속까지 같이 얽혀 있었습니다. 코드는 배포하면 바뀌지만, 이미 쌓인 데이터와 이미 한 약속은 배포로 바뀌지 않습니다. 그래서 뭔가 고칠 때 "앞으로 생길 것"과 "이미 생긴 것"을 따로 생각하게 됐습니다.
설계도 마찬가지였습니다. 처음엔 나름 신중하게 설계했다고 생각했습니다. 그런데 선착순 기준이 바뀌고, 없던 "무상" 약속이 생기고, 요구사항이 하나씩 붙을수록 처음에 더 깊이 생각하지 못한 부분이 보였습니다. 이관 쿠폰만 있던 매장처럼 아예 고려하지 못한 케이스도 있었고요. 백엔드 개발자분들이 왜 설계에 가장 많은 시간을 쓰는지, 이번에 정말 이해가 됐습니다.
프론트로 돌아와서 남은 버릇도 하나 있습니다. 숫자 하나를 숨길 때도, 그 숫자가 화면 말고 어디로 또 나가는지(알림, 어드민, 엑셀)를 같이 봅니다. 400장 사건 이후로 생긴 버릇입니다.
돌아보면 프론트는 눈에 보이는 이슈를 바로바로 고칠 수 있었습니다. 백엔드는 달랐습니다. 데이터가 쌓이고 고쳐지면서 또 다른 문제가 생기거나, 생각하지 못했던 데서 이슈가 터졌습니다. 그런 걸 모르고 있다가 사용자분들께 더 큰 문제를 만든 적도 있어서, 죄송한 마음이 컸습니다.
그래서인지 그 뒤로는 데이터와 로그를 주기적으로 확인하고 있습니다. 처음부터 끝까지 제가 만든 거라 그런지 계속 신경이 쓰여서, 오늘도 로그를 들여다보고 있습니다.
프론트 개발자가 백엔드까지 꼭 개발해야 하는 건 아닙니다. 그래도 기회가 온다면, 흐름을 한 번 파악해 보는 셈 치고 도전해 보시길 무조건 추천합니다. 요청 하나가 서버 안에서 어떻게 흘러가는지 알고 나면, 에러를 전하는 방식도, 스피너 하나를 그리는 방식도 달라집니다.