[시리즈 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

A2A 메모리 레이어: AI 에이전트를 위한 공유 두뇌 구축하기

A2A 메모리 레이어: AI 에이전트를 위한 공유 두뇌 구축하기

모든 것의 시작이 된 문제 우리를 계속 괴롭히던 것이 있었습니다: 왜 AI 에이전트는 대화가 끝나는 순간 모든 것을 잊어버릴까요? 한번 생각해보세요. 챗봇에게 전화보다 이메일을 선호한다고 말합니다. 다음 세션에서는요? 마치 처음 만난 것처럼 행동합니다. 한 상담원에게 주문 문제를 설명하고, 다른 상담원에게 연결되면, 갑자기 처음부터 다시 시작해야 합니다. 우리 모두 겪어본 일입니다.

By Sardor Madaminov
[시리즈 2편] 실무로 배우는 메시지 큐 - RabbitMQ

[시리즈 2편] 실무로 배우는 메시지 큐 - RabbitMQ

들어가며 [시리즈1]에서는 프로세스 내부 메시지 큐를 다뤘습니다. 이번엔 네트워크 메시지 큐인 RabbitMQ를 다룹니다. RabbitMQ 공식 문서나 기술 블로그는 많지만, 실무에서 어떻게 사용하는지에 대한 글은 의외로 적습니다. "Producer가 뭐고 Consumer가 뭔지는 알겠는데, 그래서 실제로는 어떻게 쓰는데?" 이번 글에서는 우리 MES 시스템에서 RabbitMQ를 어떻게 활용하고 있는지 실제 코드와 함께 공유합니다. 우리

By Jeonggil
[시리즈 1편] 실무로 배우는 메시지 큐 - Windows Message Loop

[시리즈 1편] 실무로 배우는 메시지 큐 - Windows Message Loop

들어가며 이 글은 "실무로 배우는 메시지 큐" 시리즈의 첫 번째 글입니다. 실무에서 발견한 문제를 해결하는 과정에서, IME 입력 문제와 해결 과정을 공유합니다. 메시지 큐는 RabbitMQ, Kafka 같은 네트워크 레벨만 있는 게 아닙니다. 우리가 매일 쓰는 Windows 애플리케이션도 메시지 큐 기반으로 동작합니다. * 시리즈1 (이 글): 프로세스 내부의 메시지 큐 - Windows

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

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

들어가며 제조업 IoT 플랫폼에서 N대 이상의 설비를 실시간으로 모니터링하고, 설비가 연속으로 꺼졌을 때 담당자에게 즉시 알림을 보내는 기능을 개발하게 되었습니다. 데이터는 실시간으로 쌓이지만, 설비이상을 체크하는 스케줄러 주기는 1분으로 설정하였습니다. 시스템 아키텍처 기존 인프라와 Push 기능은 이미 구축되어 있었습니다. 저는 중간에 들어가는 Alert Scheduler만 구현하면 되는 상황이었습니다. ┌──────────────────────────────────────────────────────────┐ │ 설비 IoT 센서 (실시간)

By Jeonggil