이메일 크론 오류 노출 및 VS API에 실시간 관측 가능성 추가
요약:
저는 이메일 크론을 리팩토링하여 SMTP/Brevo 실패를 더 이상 숨기지 않고, 이를 관측 가능성 계층(observability layer)을 통해 노출하도록 했습니다. 이번 변경으로 스코프가 지정된 오류 보고기가 추가되고, 실패가 크론 트리거 컨트롤러로 전파되며, 오류가 다시 숨겨지지 않도록 보장하는 새로운 테스트 스위트가 배포되었습니다.
문제점
저희의 야간 runEmailCron() 작업(apps/api/src/email/email.cron.ts)은 일괄 알림 이메일을 보내도록 설계되었습니다. 근본적인 SMTP 제공업체(Brevo)가 메시지를 거부할 때, sendMail() 헬퍼는 예외를 포착하고 알림을 조용히 전송된 것으로 표시했습니다. 그 증상은 로그에 흔적 없이 누락되는 미전달 이메일의 증가하는 백로그였습니다:
ERROR: Cron job "email" completed successfully
더 심각한 문제는, cron-trigger.controller(apps/api/src/cron/cron-trigger.controller.ts)가 포착된 오류가 절대 전파되지 않았기 때문에 이 작업을 성공적으로 간주했다는 것입니다. 이로 인해 디버깅이 불가능해졌고 이해관계자들은 배달 문제에 대해 알지 못했습니다.
제가 처음 시도한 것
초기 구현(수정 전)은 다음과 같았습니다:
// email.service.ts (원래)
export async function sendMail(to: string, html: string) {
try {
...
저는 크론이 중단되어 다른 관련 없는 작업들을 막을까 봐 try/catch를 유지했습니다. 아이디어는
AsyncLocalStorage가 필요한 이유? 이는 요청 레벨 식별자(cronRunId)를 수동으로 모든 함수를 통해 전달할 필요 없이 비동기 경계(async boundaries) 전반에 걸쳐 전파합니다.
2. 스코프를 크론 작업에 연결하기 (Wire the Scope into the Cron)
// apps/api/src/email/email.cron.ts
import {
sendCondoFeeReminder,
...
이제 sendMail 내부에서 발생하는 모든 실패는 자동으로 runId를 가지고 전달되며, 크론 작업 자체는 보고 후 오류를 다시 발생(re-throws)시킵니다.
3. 크론 트리거 컨트롤러가 실패를 존중하도록 만들기 (Make the Cron-Trigger Controller Respect Failures)
// apps/api/src/cron/cron-trigger.controller.ts
async function runJob(
job: string,
...
컨트롤러는 이제 어떤 작업이 오류를 발생시키더라도 status: "error"를 반환하며, 이는 GitHub Actions 워크플로우로 전파됩니다.
4. 무음 실패(Silent Failures)에 대비한 테스트 스위트 추가하기 (Add a Test Suite to Guard Against Silent Failures)
// apps/api/src/__tests__/email.cron.errores.test.ts
import { runEmailCron } from "../../email/email.cron.js";
import { sendMail } from "../../email/email.service.js";
...
이 테스트는 sendMail이 오류를 발생시킬 경우, 크론 작업이 이를 무시(swallow)하지 않음을 보장합니다.
5. GitHub Actions 워크플로우 업데이트하기 (Update the GitHub Actions Workflow)
# .github/workflows/cron-diario.yml
name: ⏰ Cron diario — API PlayaMXCRM
...
GitHub Actions를 통해 작업을 실행하면, API 컨테이너(Render 무료 티어)가 깨어나 크론을 실행하고, 어떤 작업이 실패할 경우 비영(non-zero) 코드로 종료됩니다. 이는 액션 UI에서 즉각적인 피드백을 제공합니다.
핵심 요약 (Key Takeaway)
배경 작업(background jobs)에서 오류를 절대 무시하지 마십시오. 스코프 기반의 관찰 가능성 헬퍼(AsyncLocalStorage + reportError)를 사용하여 실패를 표면화하고, 오케스트레이터(cron-trigger controller)가 발생된 오류를 작업 실패로 처리하도록 하세요. 이 패턴은 실시간 알림, 신뢰할 수 있는 테스트 커버리지, 그리고 "작업 실행됨"과 "작업 성공함" 사이의 깔끔한 분리를 제공합니다.
다음 단계 (What's Next)
- 경고(Alerting):
reportError에 후크를 연결하여 실패한 이메일 배치 작업이 온콜 인시던트(on-call incident)를 트리거하도록 합니다.
- 재시도 로직(Retry Logic): 일시적인 SMTP 오류에 대비하여
sendMail 내부에 지수 백오프 재시도(exponential back-off retries)를 구현합니다.
- 메트릭스(Metrics): 대시보드를 위해 Prometheus 카운터(
email_cron_success_total, email_cron_failure_total)를 발행합니다.
- 기능 플래그(Feature Flag): 새로운 오류 노출 동작을 플래그로 감싸서 프로덕션 환경에 점진적으로 배포할 수 있도록 합니다.
Roberto Luna Osorio – 풀스택 개발자 및 프로젝트 리드
Playa del Carmen, México
#vibecoding #buildinpublic #nodejs #typescript #cron #observability #github-actions
저의 Build in Public 시리즈의 일부입니다 — Playa del Carmen, México에서 Building PlayaMXCRM을 구축하는 실제 과정을 공유합니다.
레포: zaerohell/VS · 2026-10-09
#playadev #buildinpublic