notify.rinda.ai 트랙을 노드 단위로 분해했습니다.resolveGovernedSender('marketing') 풀 라운드로빈 (ses-domain-governor.service.ts:156-178). 마케팅 넛지를 10개 도메인에 분산 → 어느 도메인도 강한 발신자 평판을 못 쌓음.is_default 1행. "고정 primary + 일일캡 스필오버"로 전환하면 전 마케팅 볼륨이 1개 도메인에 집중 → 평판 최속 축적 + 모든 수신자가 동일 From.notify.rinda.ai 신규 도메인은 근본 해결이 아니며 critical path에서 분리 — 원하면 병렬 워밍업 트랙(4~6주)으로.마케팅 넛지 발신 주소는 env 고정이 아니라 발송 시점마다 governor가 10개 도메인 풀에서 회전 선택합니다. 수신자별 시작점을 잡은 뒤 rate 경합(1건/10초) 또는 일일캡에서 다음 도메인으로 회전 — 같은 유저가 매번 다른 발신자를 보게 되고 도메인별 평판이 축적되지 않습니다.
// 현행 marketing 분기 (ses-domain-governor.service.ts:156-178) const pool = await getSystemMarketingPool() // system_marketing 10개 const start = stableIndex(routingKey, pool.length) // 수신자별 시작점(회전) do { for (let i = 0; i < pool.length; i++) { const d = pool[(start + i) % pool.length] const r = await tryAcquireDomain(d) if (r === 1) return { fromEmail: `hello@${d.domain}` } // r===0(rate) OR -1(cap) → 다음 도메인으로 회전 ← 근본 원인 } await sleep(MARKETING_RETRY_SLEEP_MS) } while (Date.now() < deadline)
| 항목 | 실제 값 |
|---|---|
| marketing From | hello@{10개 중 회전 도메인} (governor off일 때만 env info@notify.grinda.ai fallback) |
| system_marketing 풀 (10) | channel·dispatch·dusk·entry·family·field·lane·line·run·web.rinda.ai — 전부 day38·cap 300 |
| 재사용 가능한 기존 인프라 | is_default 컬럼(현재 전부 false)·daily_cap 스필오버·rate 대기 루프(MARKETING_MAX_WAIT_MS) |
Critical Path CP ═ · 병렬 가능 = 동일 layer. 비용 [S]≤50 [M]≤150 [L]≤300 LOC.
Layer0 (조사·게이트) Layer1 (구현) Layer2 (검증) Layer3
┌───────────────────────┐
│N0.1* S SES identity │═══╗
│ primary 검증 확정 GATE│ ║
├───────────────────────┤ ║ ┌────────────────────┐
│N0.2 S 일일 발송량 │ ╠═▶│N1.1* M governor 분기│═╗
│ cap 300 충분성 │ ║ │ 고정primary+스필오버│ ║ ┌──────────────┐
└───────────────────────┘ ║ ├────────────────────┤ ╠═▶│N2.1* S 단위TC │═╗ ┌───────────────┐
╠═▶│N1.3* S is_default=1 │═╝ │ +send-ci │ ╠▶│N3.1* S 배포후 │
║ ├────────────────────┤ └──────────────┘ ║ │ 스팸률 모니터│
║ │N1.2 S pool ORDER BY│────────────────────┘ └───────────────┘
║ └────────────────────┘
(선택) ╚═▶ N1.4 M notify.rinda.ai 병렬 워밍업 트랙 ───▶ (4~6주)
CP: N0.1 → N1.1/N1.3 → N2.1 → N2.2(PR) → N3.1
✓게이트: N0.1 미확정 시 N1.3(primary 지정) 진행 금지 — 미검증 도메인 발송 = 바운스
모순: alpha DB는 220개 rinda 서브도메인을 워밍업·발송 중이라는데, SES 조사에는 25개만 잡히고 system_marketing 10개(entry·channel 포함)가 하나도 안 보임. 실제 발송되려면 SES 검증 identity가 필수이므로 SES 조사가 페이지네이션 미조회로 잘렸을 가능성이 큼.
왜 게이트인가: primary로 지정한 도메인이 SES 미검증이면 DKIM 서명 실패 → 스팸/바운스 직행. primary 확정 전 반드시 검증 여부를 확인.
aws-ses 스킬로 identity 목록을 페이지네이션 끝까지 재조회 — entry.rinda.ai(primary 후보)의 VerificationStatus=SUCCESS + Easy DKIM SUCCESS 확인.system_marketing 도메인 중 워밍 성숙도 높은 것으로 primary 후보 교체.resolveGovernedSender marketing 분기를 primary-first + 일일캡에서만 스필오버로 교체. rate 경합은 회전이 아니라 같은 primary에서 대기.
// after — marketing 분기 const pool = await getSystemMarketingPool() // is_default DESC 정렬 (N1.2) if (pool.length === 0) return null const deadline = Date.now() + MARKETING_MAX_WAIT_MS let idx = 0 // primary = pool[0] 고정 시작 do { const d = pool[idx] if (await withinGlobalRate()) { const r = await tryAcquireDomain(d) if (r === 1) return { fromEmail: `${MARKETING_LOCAL_PART}@${d.domain}` } if (r === -1 && idx < pool.length - 1) { idx++; continue } // 일일캡 소진 → 다음 도메인만 스필오버 // r === 0 (rate) → 회전하지 않고 같은 primary에서 대기 } await sleep(MARKETING_RETRY_SLEEP_MS) } while (Date.now() < deadline) return { fromEmail: `${MARKETING_LOCAL_PART}@${pool[0].domain}` } // fail-open: primary
stableIndex()와 routingKey 사용처가 marketing 분기뿐 → 이 변경으로 orphan이 되면 함수·createHash import까지 제거. transactional(OTP) 분기는 변경 없음.
풀 쿼리에 ORDER BY is_default DESC, domain ASC 추가로 primary가 항상 pool[0]. (구현 시 현행 ORDER BY 확인 후 조정)
워밍 성숙한 system_marketing 도메인 1개를 primary로. GATE(N0.1)에서 SES 검증 확인된 도메인 사용. 예시는 entry.rinda.ai.
-- manual-migration 013 (idempotent). 단일 primary 보장. UPDATE rinda_managed_sending_domains SET is_default = false WHERE send_role = 'system_marketing' AND is_default = true; UPDATE rinda_managed_sending_domains SET is_default = true WHERE send_role = 'system_marketing' AND domain = 'entry.rinda.ai'; -- (권장) 역할별 단일 primary 강제 — 부분 유니크 인덱스 CREATE UNIQUE INDEX IF NOT EXISTS uq_managed_default_per_role ON rinda_managed_sending_domains (send_role) WHERE is_default;
__setGovernorRedisForTest로 Redis mock. ①캡 여유 시 항상 primary 반환 ②rate 경합 시 회전 안 함 ③primary 캡 소진 후에만 2번째 도메인.sh send-ci.sh (lint+type+build) 통과.hello@entry.rinda.ai 일관 확인.warmup_metric_daily/SES 이벤트로 primary 스팸률 추이 모니터(목요일 배포~).is_default 되돌리면 정렬만 바뀌고 회전 로직 복귀 전까진 여전히 primary-first. 완전 원복은 코드 revert.SES_GOVERNOR_ENABLED=false → env fallback(notify.grinda.ai) 단일 발송(회전 자체가 사라짐).worktree-pr 스킬로 alpha PR 생성 → squash merge.is_default 지정.alpha-beta-sync).notify.rinda.ai 병렬 트랙 critical path 제외근본 fix와 독립. 평판 격리를 위해 전용 도메인을 원할 때만. 전 단계 스킬 자동화 가능(조사 완료). day0 → 4~6주 워밍업 필요하므로 지금 스팸 문제는 ①이 먼저 해결.
| # | 단계 | 스킬 | 내용 |
|---|---|---|---|
| 1 | SES identity + Easy DKIM | aws-ses | notify.rinda.ai 도메인 생성 → DKIM 토큰 3개 발급 |
| 2 | DNS 8레코드 (entry 패턴 미러링) | cloudflare | DKIM CNAME×3 + MAILFROM(bounces.notify MX10+SPF TXT) + DMARC TXT, 전부 proxied=false |
| 3 | config set 연결 | aws-ses | rinda-marketing 연결(SNS→SQS 이미 가동) |
| 4 | 워밍업 등록 | alpha-db | rinda_managed_sending_domains(send_role=system_marketing, cap 50 램프) + warmup_account |
| 5 | primary 승격 | alpha-db | 워밍 성숙 후 is_default를 notify로 이전 (①과 동일 메커니즘) |
notify.grinda.ai, 요청은 notify.rinda.ai, governor 풀은 .rinda.ai — 세 갈래. 트랙 착수 전 어느 도메인으로 통일할지 확정 필요.
| 리스크 | 영향 | 대응 |
|---|---|---|
| primary 도메인 SES 미검증 | 높음 DKIM 실패·바운스 | N0.1 게이트에서 선확정, 미검증 시 검증된 도메인으로 교체 |
| 단일 도메인 볼륨 초과(cap 300) | 중 스필오버 잦음 | N0.2 발송량 확인 → cap 상향 또는 primary 그룹화 |
| 평판 집중의 양날 | 중 primary 평판 악화 시 전체 영향 | 스필오버 도메인 유지(청정 풀) + 모니터링 |
| notify.grinda vs rinda 혼선 | 낮음 정합성 | 도메인 표기 통일 결정 |