[시리즈 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분마다 실시간 체크)
- 비용 관리 (만약을 대비한 방어적 설계)
- 안정성 고려 (알림 하나도 신중하게)
까놓고 보니 하나의 프로젝트급이었습니다.
어떤 서비스에서 어떤 레벨로 구현하느냐에 따라 난이도는 천차만별입니다. 단순 기능 구현을 넘어 확장 가능한 시스템 설계와 운영을 고려한 방어적 코딩까지 고민한 값진 경험이었습니다.
감사합니다.