Skip to content

개발팀 운영 노트 ​

대부분 한 번 데였던 것입니다. 같은 자리에 다시 빠지지 않으려고 적어 둡니다.

이 문서는 공개되어 있습니다

키·접속 정보·개인정보·서버 주소는 여기에 적지 않습니다. 그런 것은 사내 비공개 채널에 둡니다.

시간대 — DB는 UTC, PHP는 KST ​

운영 서버의 데이터베이스는 UTC로, PHP는 Asia/Seoul로 돕니다. 한 쿼리 안에서 둘을 섞으면 9시간이 어긋납니다.

php
// ✗ MySQL NOW() / CURDATE() 는 UTC
"WHERE expires_at > NOW()"
"SET created_at = NOW()"

// ✓ PHP 상수를 쓴다
"WHERE expires_at > '".G5_TIME_YMDHIS."'"
"SET created_at = '".G5_TIME_YMDHIS."'"

실제로 있었던 일:

  • 문자 인증번호가 5분이 아니라 9시간 5분 동안 유효했습니다. 만료 시각은 PHP(KST)로 넣고 NOW()(UTC)로 비교해, 만료 판정이 늘 9시간 밀렸습니다
  • 예약 데이터의 생성·수정 시각이 9시간 과거로 기록됐습니다

날짜 비교도 같습니다. DATE(issued_at) = CURDATE() 처럼 저장은 KST, 비교는 UTC면 오전 9시 이전 실행에서 하루가 통째로 어긋납니다.

발송 실패 목록은 배열이 아니라 객체다 ​

문자·알림톡 SDK는 실패한 건들을 객체로 돌려줍니다. 배열처럼 읽으면 PHP 8에서 치명적 오류가 나고 스크립트 전체가 그 자리에서 멈춥니다.

php
// ✗ isset() 이라도 예외 없이 죽는다
if (isset($failed['to'])) ...

// ✓ 공용 헬퍼로 정규화해서 쓴다
$failedPhones = uri_stamp_failed_phone_map($e->getFailedMessageList());

실제로 있었던 일: 알림톡 대상 40명 중 첫 10건만 접수 시도하고 중단됐습니다. 발송 이력조차 남지 않아 다음 날 자동 재시도도 되지 않았습니다.

부분 실패는 예외로 오지 않는다 ​

send() 는 전량 실패일 때만 예외를 던집니다. 일부만 실패하면 정상 반환되므로, 응답의 실패 목록도 함께 확인해야 합니다. 그러지 않으면 실패한 건이 「성공」으로 기록됩니다.

스탬프는 저장하지 않는다 ​

스탬프 개수를 컬럼에 쌓지 않고, 예약 데이터에서 매번 다시 계산합니다.

  • (연락처 × 날짜)로 이용 분을 합산 → 시간 단위로 나눠 내림
  • 발급 장수 = 전체 스탬프 ÷ 필요 개수 − 이미 발급한 장수

이 구조 덕분에 과거 데이터를 다시 수집하거나 고쳐도 결과가 스스로 맞춰지고, 같은 작업을 몇 번 돌려도 중복 발급이 생기지 않습니다. 차감·초기화 로직을 두지 않는 이유입니다.

대신 발급 근거는 발급 시점에 따로 남깁니다. 나중에 되짚을 방법이 없기 때문입니다.

상태 컬럼을 판정 근거로 쓰지 않는다 ​

이용권의 상태(사용 가능 / 만료)는 하루 한 번 도는 크론이 뒤늦게 맞추는 값입니다. 그 사이에는 기간이 지난 이용권이 「사용 가능」으로 남아 있습니다.

실제로 있었던 일: 유효기간은 그날 23:59:59 까지인데 만료 크론이 23:59:00 에 돌아 만료 다음 날 하루 종일 「사용 가능」인 채로 남았고, 그대로 사용 처리까지 됐습니다. 게다가 유효기간 검사는 「되돌리기」 경로에만 있었고 정작 「사용」 경로에는 없었습니다.

  • 판정은 항상 만료일을 직접 본다 — 사용 처리·화면 표시 모두
  • 크론은 표시를 맞추는 보조 수단이지 차단 수단이 아니다
  • 같은 판정이 두 군데 이상 필요하면 공용 함수로 두고, 화면과 서버가 같은 함수를 쓴다

버튼을 숨기는 것은 안내일 뿐입니다. 막는 것은 서버이고, 사용 처리 경로가 둘 이상이면(매장 앱·관리자 화면) 양쪽 모두에 있어야 합니다.

금액·비율은 스냅샷으로 남긴다 ​

  • 쿠폰 금액은 발급 시점 값을 복사
  • 본사 지원 비율은 사용 처리 시점 값을 복사

이벤트 설정을 나중에 바꿔도 과거 정산액이 소급 변경되지 않습니다. 정산은 사용 완료 기준이므로 비율은 사용 시점이 맞습니다.

자동화 작업을 새로 붙일 때 ​

정확히 그날만 보는 조건은 위험합니다.

sql
-- ✗ 하루라도 걸러지면 그 배치는 영영 누락된다
WHERE DATE(expires_at) = 오늘 + 30일

-- ✓ 범위로 잡고, 이미 보낸 건만 제외한다
WHERE DATE(expires_at) <= 오늘 + 30일
  AND DATE(expires_at) >= 오늘
  AND NOT EXISTS (성공 이력)

같은 이유로, 발송 대상을 「오늘 발급분」으로만 좁히면 실패한 건이 다음 날 대상에서 빠져 영영 재시도되지 않습니다. 재시도 창을 며칠 두고, 실패 상태도 대상에 포함시킵니다.

원칙: 「보낸 것을 제외」하는 방식으로 짜면 스스로 복구되고, 「오늘 것만 선택」하는 방식으로 짜면 한 번 놓친 것이 영구히 남습니다.

자동 발송 경로는 반드시 잠근다 ​

로그인 없이 도는 경로는 외부에서 호출하면 임의 발송이 가능합니다. 내부 요청만 허용하고, 테스트 이벤트는 삼중으로 차단합니다 (이벤트 진입 · 대상 조회 · 단건 지정 경로 각각).

스키마 변경은 멱등하게 ​

컬럼·테이블 추가는 한 곳에 모아 두고 매 실행마다 안전하게 다시 돌 수 있게 만듭니다 (존재 확인 후 추가, CREATE TABLE IF NOT EXISTS). 배포 순서에 의존하는 수동 마이그레이션을 두지 않습니다.

로그를 늘렸으면 회전도 늘린다 ​

작업을 추가하면서 로그 파일만 늘고 회전 설정에 빠져 있던 적이 있습니다. 새 로그를 만들면 같은 커밋에서 회전 대상에 넣습니다. 날짜별로 파일이 쌓이는 상세 로그는 별도로 오래된 것을 지웁니다.

Released under the MIT License.