[시리즈 2편] 그림으로 풀어낸 SaaS 알림 시스템

[시리즈 2편] 그림으로 풀어낸 SaaS 알림 시스템

이 글은 1편 - 그림으로 풀어낸 SaaS 알림 시스템의 후속편입니다.

들어가며

1편에서는 설비 연속 OFF 알림 기능의 핵심 로직과 어떤식으로 해결했는지 그림으로 알아봤습니다.

이번 글에서는 실무에서 마주한 진짜 고민들을 공유합니다:

  • 왜 3개의 새로운 테이블이 필요했나?
  • 어떻게 확장 가능한 구조를 만들었나?
  • SMS 14원짜리 알림이 왜 무서운가?
  • 운영 레벨로 나가기까지 무엇을 준비했나?

1. 시스템 구성과 테이블 설계

1.1 기존 시스템 파악하기

먼저 사용해야 되는 기존 테이블들을 살펴봤습니다:

기존 테이블
├── company
├── factory
├── machine
├── monitoring       # 설비 모니터링 데이터
├── user
└── notification

monitoring 테이블은 실시간으로 데이터를 받아 저장하는 핵심 테이블로, onoff_monitored(설비 ON/OFF), spm(분당 회전수), vibration(진동), current(전류) 등 다양한 데이터를 저장합니다.

이 테이블을 보면서 중요한 깨달음을 얻었습니다:

"연속 OFF 알림"은 시작일 뿐이다.
SPM 이상, 진동 임계치 초과, 전류 급등 등 다양한 알림이 필요할 것이다.
또는 다른 테이블에 있는 특정값을 사용할수도 있을것이다.

1.2 새로운 테이블 설계

회사별로 다양한 알람을 적용하고 관리할수있게 3개의 새로운 테이블을 설계했습니다:

1️⃣ alert_rule - 알림 규칙 정의

회사별로 어떤 알림을 어떤 조건으로 받을지 설정하는 테이블입니다.

CREATE TABLE alert_rule (
    ...
);

핵심 컬럼: id(PK), id_company, rule_type(확장 포인트), count, enabled, resend_interval_minutes

rule_type으로 확장 가능한 구조입니다. 현재는 CONSECUTIVE_OFF(연속 OFF)만 구현되어 있지만, 향후 SPM_ABNORMAL, VIBRATION_HIGH, CURRENT_SPIKE 등 다양한 알림 타입을 DB 데이터만 추가해서 지원할 수 있습니다.

2️⃣ alert_machine - 알림 이력 관리

어떤 설비에 언제 알림이 발생했는지, 언제 해소되었는지 추적하는 테이블입니다.

CREATE TABLE alert_machine (
    ...
);

핵심 컬럼: id(PK), id_machine, alert_type, resolved_at(NULL이면 미해결)

3️⃣ shedlock - 분산 스케줄러 동기화

CREATE TABLE shedlock (
    ...
);

다중 서버 환경에서 스케줄러가 중복 실행되지 않도록 락을 관리합니다.

결과: 해당 스케줄러가 돌때 필요한 테이블 기존 테이블 6개 + 신규 테이블 3개 = 총 9개 테이블 사용


2. 확장 가능한 스케줄러 아키텍처

2.1 왜 하나의 스케줄러에서 모든 알림을 처리하나?

초기에는 "알림 타입마다 별도 스케줄러를 만들면 되지 않나?"라고 생각했습니다:

// ❌ 알림 타입마다 스케줄러 생성?
@Scheduled(cron = "0 * * * * *")
public void checkConsecutiveOffAlerts() { ... }

@Scheduled(cron = "0 * * * * *")
public void checkSpmAbnormalAlerts() { ... }

@Scheduled(cron = "0 * * * * *")
public void checkVibrationHighAlerts() { ... }

하지만 이 방식은 리소스 낭비(각 스케줄러가 독립적으로 DB 조회), 확장성 제한(새 타입 추가 시 배포 필요), 유지보수 어려움(공통 로직 중복) 등의 문제가 있습니다.

2.2 단일 스케줄러 + 알림 타입 반복 구조

대신 하나의 스케줄러에서 활성화된 알림 규칙을 반복 처리하는 구조를 선택했습니다:

@Scheduled(cron = "0 * * * * *")
@SchedulerLock(name = "alertScheduler", lockAtMostFor = "50s", lockAtLeastFor = "10s")
@Transactional
public void checkAlerts() {
    // 1. 활성화된 알림 규칙 조회 (모든 타입)
    List<AlertRuleDto> activeRules = alertRuleRepository.findEnabledRulesAsDto();

    // 2. 각 규칙별로 처리
    for (AlertRuleDto rule : activeRules) {
        checkRuleForCompany(rule);  // 회사별 알림 체크
    }
}

하나의 실행 컨텍스트를 공유해 리소스 효율적이고, DB에 rule_type만 추가하면 새로운 알림 타입을 지원할 수 있습니다.


3. 운영을 고려한 방어적 설계와 테스트

3.1 개발 환경에서의 테스트

운영 규모는 회사 N개, 공장 M개, 설비 총 OO대 수준. 장애가 났을 때를 가정한 네거티브 테스트를 빡세게 진행했습니다.

3.2 비용과 안정성에 대한 고민

실제로는 SMS를 사용하진 않지만, 만약 SMS 문자 기능이 붙는다면? 이런 시나리오까지 고려했습니다.

건당 비용이 발생하는 기능에서 재전송 로직이나 해제 조건에 버그가 있으면 같은 알림이 반복 발송됩니다. 설비 몇 대 × 관리자 여러 명. 매 분마다 잘못 나가면 비용이 기하급수적으로 증가합니다.

그만큼 안정적으로 고민했습니다:

  • 재전송 주기 엄격한 체크
  • 하루 최대 발송 제한 (per 설비)
  • 중복 전송 방지 로직
  • 알림 발송 전 최종 검증

4. 확장성을 고려한 아키텍처

4.1 알림 타입별 검증 로직 분리

현재는 연속 OFF만 구현되어 있지만, 향후 확장을 위해 검증 로직을 분리했습니다:

@Component
public class AlertRuleChecker {

    // 연속 OFF 검증
    public boolean checkConsecutiveOff(
        List<MonitoringAlertDto> data,
        int requiredCount
    ) {
        // 1. 개수 체크
        // 2. 모두 OFF 상태 확인
        // 3. 시간 간격 검증 (3분 이내)
    }

    // 향후 추가 가능
    public boolean checkSpmAbnormal(
        List<MonitoringAlertDto> data,
        int threshold
    ) {
        // SPM 이상 검증 로직
    }

    public boolean checkVibrationHigh(
        List<MonitoringAlertDto> data,
        float threshold
    ) {
        // 진동 임계치 초과 검증 로직
    }
}

Strategy Pattern 적용 고려:

public interface AlertCheckStrategy {
    boolean check(List<MonitoringAlertDto> data, AlertRuleDto rule);
}

@Component
public class ConsecutiveOffStrategy implements AlertCheckStrategy {
    @Override
    public boolean check(List<MonitoringAlertDto> data, AlertRuleDto rule) {
        // 연속 OFF 검증
    }
}


5. 마치며

항상 요구사항은 쉬워 보입니다.

"설비 이상 OFF 시 알람 보내는 기능 필요합니다."

단순해 보이지만, 실제로는:

  • 멀티테넌시 환경에서 회사별로 다른 규칙 적용
  • 확장성 고려 (연속 OFF뿐 아니라 SPM, 진동, 전류 등 다양한 알림 타입)
  • 성능 최적화 (수천 대 설비를 1분마다 실시간 체크)
  • 비용 관리 (만약을 대비한 방어적 설계)
  • 안정성 고려 (알림 하나도 신중하게)

까놓고 보니 하나의 프로젝트급이었습니다.

어떤 서비스에서 어떤 레벨로 구현하느냐에 따라 난이도는 천차만별입니다. 단순 기능 구현을 넘어 확장 가능한 시스템 설계운영을 고려한 방어적 코딩까지 고민한 값진 경험이었습니다.

감사합니다.


Read more

쉬어가며

쉬어가며

개발 일만 하다 보면 때로는 잠시 멈춰서 주변을 둘러보는 것도 중요하다. 오늘은 오래전에 다녔던 병원에서의 일을 가볍게 풀어볼까 한다. 고객이 병원에 방문하고, 그것이 수익으로 이어지기까지. 그 안에는 생각보다 훨씬 많은 전략과 기술, 그리고 사람들의 노력이 숨어있다. 1. 들어가며 오래전 다녔던 한 병원에서의 일이다. 당시 나는 "의료 IT"라는 낯선 도메인에

By Jeonggil
Building AI Sales Pipeline That Actually Researches: Multi-Agent Orchestration with tool-use

Building AI Sales Pipeline That Actually Researches: Multi-Agent Orchestration with tool-use

계속 우리를 괴롭혔던 문제 세일즈 파이프라인이 작동하고 있었습니다. 여섯 개의 Claude 에이전트가 각자 역할을 수행했습니다: 회사를 조사하고, 솔루션을 매핑하고, 제안서를 작성하고, 딜 규모를 추정하고, 이메일을 작성합니다. CLI 명령어 하나면 몇 분 안에 개인화된 세일즈 제안서가 완성되었습니다. 하지만 거기에는 거짓말이 내재되어 있었습니다. "리서처" 에이전트는 실제로 아무것도 조사하지 않았습니다. "Koelle GmbH, Germany"

By Sardor Madaminov