개발현황

크레플의 R&D를 소개합니다.

R&D

개발현황

PC 기반 자동화 시스템의 알람 및 이벤트 로그 설계: 신우주 연구원

  • 관리자
  • 2025.07.14

PC 기반 자동화 시스템의 알람 및 이벤트 로그 설계

작성자: 신우주

1. 서론

자동화 설비를 운용하다 보면, 예상하지 못한 상황이 종종 발생합니다. 예를 들어 센서 신호가 안 들어오거나, 실린더가 지정된 시간 내에 올라가지 않거나, 진공이 제대로 잡히지 않는 경우처럼 말이죠. 이런 상황에서 가장 먼저 도움이 되는 것이 바로 알람 시스템입니다.
"어떤 문제가, 언제, 어디에서 발생했는지" 즉시 알려주는 것이 핵심입니다. 알람 코드가 뒤죽박죽이거나, 같은 문제도 서로 다른 방식으로 처리된다면, 실무에서 큰 혼란이 생기기 쉽습니다.

이 글에서는 제가 현장에서 겪고, 적용해 본 알람 로그 설계 경험을 바탕으로, PC 기반 자동화 시스템에서 실용적인 알람 시스템을 어떻게 구성할 수 있는지 소개 드리려고 합니다.

 

2. 알람 시스템의 핵심 구성 요소

자동화 시스템에서 알람은 단순한 메시지가 아니라, 문제 발생의 증거이자 원인 추적의 단서입니다. 그래서 알람 시스템을 설계할 때는 단순히 "문제가 발생했다"는 수준을 넘어서, 그 내용을 정확하고 일관된 형식으로 기록하는 것이 중요합니다. 이를 위해 알람 메시지를 구성하는 요소들을 체계적으로 정의해 둘 필요가 있습니다.

제가 실제로 사용한 알람 로그 구조는 다음과 같습니다:

항목 설명
타임스탬프 알람이 발생한 정확한 시각 (예: 2025-05-30 08:15:23)
디바이스 이름 어떤 장치 또는 부위에서 알람이 발생했는지 (예: Robot1, Lift3)
디바이스 ID 같은 종류의 장치가 여러 개 있을 경우 구분을 위한 고유 번호 (예: 1, 2)
알람 코드 정수값 또는 Enum 값으로 정의된 고유 코드 (예: 0x0102)
알람 레벨 알람의 심각도에 따라 INFO, WARNING, ERROR로 구분
메시지 사람이 이해하기 쉬운 설명 (예: "진공 신호가 시간 내에 감지되지 않았습니다.")

알람 코드 체계는 통일감이 중요.

설비가 복잡 해질수록 알람 코드도 많아집니다. 이때 각 코드가 어떤 의미인지 개발자나 유지보수 인력이 한눈에 파악할 수 있어야 합니다. 그래서 저는 다음과 같이 정수 기반 코드 체계를 사용했습니다:

  • 앞 1~2바이트: 장치 유형 (예: 로봇 = 0x01, 실린더 = 0x02 등)
  • 뒤 1~2바이트: 상세 알람 번호 (예: 진공 미감지 = 0x01)

예를 들어 0x0101이라면, 이는 로봇에서 진공 미감지 알람을 의미하게 됩니다.
이렇게 정형화된 코드는 Enum 으로 관리하면 가독성과 유지보수가 훨씬 편해집니다. 코드와 메시지를 매핑해두면 UI에도 자동으로 설명을 띄울 수 있고, 중복 정의나 오타도 줄일 수 있습니다.

알람 레벨 분류는 대응 우선순위를 나누기 위해 필요

알람의 종류에 따라 대응 방법이 다릅니다. 예를 들어 단순 정보 메시지(INFO)는 알림만 해도 되지만, 설비 정지가 필요한 오류(ERROR)는 작업을 멈추고 사용자의 확인이 필요합니다. 보통 다음과 같이 분류합니다:

INFO: 일반 정보, 통계용 이벤트 (예: 작업 시작)

WARNING: 주의가 필요한 상황이지만 설비는 계속 동작 가능 (예: 진공 강도 낮음)

ERROR: 설비 동작을 중단해야 하는 심각한 오류 (예: 실린더 미복귀, 안전센서 이상)

이렇게 레벨에 따라 UI 표현 방식(색상, 팝업 여부, 로그 저장 방식 등)을 다르게 처리하면 작업자나 관리자도 쉽게 판단할 수 있습니다.

  1. 코드 설계 전략 – Enum을 활용한 알람 코드 관리

알람 시스템에서 중요한 것 중 하나는 코드 관리의 일관성과 가독성입니다. 처음에는 "진공 미감지" 같은 문자열을 직접 넣고 처리할 수 있지만, 알람 종류가 늘어나고 UI, 로그, 데이터 분석 등에 재활용되기 시작하면 점점 한계가 생깁니다. 그래서 Python의 IntEnum 을 활용해서 알람 코드를 정의하고, 설명 메시지를 함께 관리하는 구조로 설계했습니다.

Enum을 사용하면 다음과 같은 장점이 있습니다:

  • 코드 값과 이름을 매핑해서 가독성이 좋아짐
  • IDE 자동완성 덕분에 오타 가능성 줄어듦
  • 코드 하나만 고치면 전체 UI, 로그 시스템에 반영 가능
  • 코드 설명을 딕셔너리로 묶으면 다국어 대응도 쉬움

예제: 알람 코드 정의

from enum import IntEnum

class AlarmCode(IntEnum):
    VACUUM_TIMEOUT = 0x0101
    CYLINDER_NOT_UP = 0x0102
    SENSOR_FAULT = 0x0103

여기서 0x0101은 '진공 신호가 일정 시간 내에 감지되지 않았음'을 의미합니다. 코드 상에서는 이렇게 쓸 수 있습니다:

current_alarm = AlarmCode.VACUUM_TIMEOUT

설명 메시지 매핑

알람 코드와 설명을 따로 매핑해서 관리하면 UI나 로그 출력에도 쉽게 쓸 수 있습니다:

ALARM_DESCRIPTIONS = {
    AlarmCode.VACUUM_TIMEOUT: "진공 신호가 시간 내에 감지되지 않았습니다.",
    AlarmCode.CYLINDER_NOT_UP: "실린더가 지정된 위치까지 올라가지 않았습니다.",
    AlarmCode.SENSOR_FAULT: "센서 입력이 비정상입니다."
}

def get_alarm_description(code: AlarmCode) -> str:
    return ALARM_DESCRIPTIONS.get(code, "알 수 없는 알람입니다.")

이렇게 하면 알람이 발생했을 때 UI에서 다음과 같이 표현할 수 있습니다:

popup.show(
    title="알람 발생",
    message=get_alarm_description(current_alarm)
)

다국어 대응

추후 다국어(예: 한국어/영어/베트남어) 지원이 필요하다면, ALARM_DESCRIPTIONS를 언어별로 분리하거나 JSON/YAML 파일로 저장할 수 있습니다:

ALARM_DESCRIPTIONS_KO = { ... }
ALARM_DESCRIPTIONS_EN = { ... }

Enum을 활용한 알람 코드 설계는 단순하지만 강력합니다. 특히 UI와 로그 시스템이 같이 있는 PC 기반 자동화에서는 유지보수가 쉬워지고, 오류 가능성도 줄어듭니다.

4. 알람 로그 저장

알람 시스템에서 알람을 띄우는 것만큼이나 중요한 게 있습니다. 바로 발생한 알람을 기록으로 남기는 일입니다.
기록이 남아야 나중에 장애를 분석하거나, 반복되는 문제를 추적할 수 있습니다.

설비 데이터를 장기적으로 쌓거나, 검색/필터/통계를 자주 한다면 SQLite 같은 경량 DB를 사용하는 것이 좋습니다.

장점

  • 수천만 건 이상의 데이터도 구조적으로 관리 가능합니다.
  • 시간 필터, 레벨 필터 등 빠른 조건 검색이 가능합니다.
  • 테이블 스키마가 있으므로 데이터의 일관성이 보장됩니다.

단점

  • DB 파일에 동시에 쓰기 위해선 큐나 트랜잭션 처리가 필요합니다.

알람 발생 이벤트를 qt의 signal를 통해 DB 쓰기 전용 스레드로 전달해서, 메인 스레드에서는 GUI 반응성과 로직 안정성을 유지하도록 구성했습니다.

5. 알람 처리 로직

알람 시스템이 아무리 잘 설계돼 있어도, 사용자가 알람을 제때 알아차리지 못하면 의미가 없습니다. 알람이 발생했을 때 바로 눈에 띌 수 있도록 알람의 수준 별로 색상을 구분하여 화면에 표시하도록 하였습니다.

중복 알람 억제

예를 들어 진공이 안 잡혔다고 해서 1초에 수십 개의 팝업이 뜬다면 오히려 더 혼란스러울 수 있습니다. 저는 아래와 같은 방식으로 동일한 알람은 무시하는 구조를 넣었습니다.

def add_alarm(self, alarm_info: AlarmInfo):
# 이미 존재하는 알람인지 확인.
    if self.alarm_model.is_alarm_info_exist(alarm_info):
        return

    self.alarm_log_repository.add_alarm_log(alarm_info)
    self.alarm_view.append_alarm(alarm_info)
alarm_num = self.alarm_controller.get_alarm_count()

    if alarm_num > 0:
        notification = f"{alarm_num if alarm_num < 100 else '100+'}"
        self.main_view.set_alarm_notification(notification)
    else:
        self.main_view.hide_alarm_notification()

알람 이력창도 함께 제공

팝업은 일시적인 표시이기 때문에, 알람을 한 번 놓치면 다시 확인하기 어렵습니다. 그래서 저는 알람 이력 UI를 따로 구성해서 언제든지 발생했던 알람들을 조회할 수 있도록 했습니다.

알람 이력창의 특징:

  • 발생 시각 기준 정렬 (최신 순)
  • 알람 레벨 필터: INFO / WARNING / ERROR
  • 장치별 검색 또는 코드 기반 검색
  • 주로 관리자나 개발자가 이력을 분석하거나, 유지보수 목적으로 활용

이 이력창은 발생한 모든 알람의 기록을 보여주는 역할만 하고, 알람 상태를 제어하거나 리셋 하지는 않습니다.

현재 알람 목록창

이와 별도로, 저는 현재 발생 중인 알람만 관리하는 전용 UI도 구성했습니다. 이곳에서는 작업자가 다음과 같은 조치를 취할 수 있습니다:

  1. 현재 발생 중인 알람들이 표 형태로 표시됩니다.
  2. 알람 목록에서 한 행을 선택하면, 해당 알람에 대한 조치를 수행할 수 있는 버튼(예: "Reset")이 활성화됩니다.
  3. 사용자가 Reset 버튼을 누르면:
  4. 내부적으로 해당 알람에 맞는 오류 해결 로직이 동작합니다. (예: 센서 리셋, 실린더 재구동 등)
  5. 해당 장치가 정상 상태로 복귀했는지 확인합니다.
  6. 이상이 없다면 장비 상태를 "정상"으로 전환하고, 작업을 이어서 진행할 수 있도록 상태를 복원합니다.

이 Reset 과정은 단순히 UI에서 알람을 지우는 게 아니라, 현실에서 실제 문제가 해결됐는지 확인하고 정상화를 유도하는 제어 흐름의 일부입니다.

알람 시스템을 UI 단에서 구분한 이유

알람을 확인하는 용도(이력)와 실제로 장비 제어와 연관된 처리(Reset)는 성격이 다릅니다. 그래서 두 기능을 각각 다른 UI로 분리한 이유는 다음과 같습니다:

  1. 이력창: 확인 / 추적용
  2. 목록창: 조치 / 제어용

이렇게 팝업 → 목록 → 이력으로 이어지는 구조는 실무에서도 알람에 대한 인지 → 대응 → 분석의 전 과정을 커버할 수 있도록 도와줍니다.
그리고 각 UI가 명확한 역할을 갖고 분리되어 있기 때문에, 유지보수나 기능 확장도 유리합니다.

6. 알람과 장비 상태(State) 전이 로직

자동화 시스템에서 알람은 단순한 경고가 아니라, 장비의 상태를 실시간으로 제어하는 신호로 작동해야 합니다. 사용자가 알람을 확인하지 못했을 때도, 시스템이 스스로 상태를 판단하고 멈추거나 경고를 유지하는 구조가 되어야 장비의 신뢰성을 보장할 수 있습니다.

알람 수준에 따른 상태 전이 규칙

알람이 발생하면, 알람의 레벨(WARNING, ERROR 등) 에 따라 장비 상태를 자동으로 전이 시키는 구조가 필요합니다.

알람 레벨 장비 상태 동작 설명
INFO RUNNING 유지 단순 정보 알림 (작업 시작, 설정 완료)
WARNING PAUSED 경고 메시지 출력, 작업 일시 정지
ERROR ERROR 즉시 작업 중단, 사용자 확인 후 복구 필요

이런 방식으로 동작하면, 작업자는 장비가 왜 멈췄는지 명확하게 인지할 수 있고,알람 수준이 높을수록 더 확실하게 대응할 수 있게 됩니다.

우선순위가 다른 알람이 동시에 발생할 경우

실제 현장에서는 INFO, WARNING, ERROR 수준의 알람이 동시에 여러 개 발생하는 경우도 흔합니다. 이때는 항상 가장 높은 수준의 알람을 기준으로 장비 상태를 설정하는 방식이 안전합니다.

예를 들어 다음과 같은 알람이 동시에 존재할 경우:

  • Lift1 → WARNING: 리프트 업 상승 타임 아웃.
  • Robot2 → ERROR: 로봇 통신 연결 끊김.

이 경우 장비 상태는 ERROR 상태를 유지해야 하며,
모든 ERROR 알람이 해결된 후에야 WARNING으로 내려갈 수 있습니다.

상태 갱신 예시 코드:

def update_machine_state_from_alarms(active_alarms: list[AlarmDTO]):
    if any(a.level == AlarmLevel.ERROR for a in active_alarms):
        machine.set_state(MachineState.ERROR)
    elif any(a.level == AlarmLevel.WARNING for a in active_alarms):
        machine.set_state(MachineState.PAUSED)
    else:
        machine.set_state(MachineState.RUNNING)

알람 Reset 시 상태 복원 처리

사용자가 알람 목록에서 특정 알람을 Reset할 때도, 장비 상태는 전체 알람 목록을 기준으로 다시 평가해야 합니다. 단 한 개의 알람이 해결됐다고 해서 즉시 RUNNING 상태로 돌아가는 건 위험할 수 있습니다.

상태 복원 절차:

  1. 사용자가 알람을 선택해 Reset 수행
  2. 내부적으로 알람 해결 시도 → 성공 여부 확인
  3. 해결된 알람은 목록에서 제거
  4. 남아 있는 알람의 가장 높은 수준을 기준으로 장비 상태 재설정

예시 구현:

def update_machine_state_from_alarms(active_alarms: listdef reset_alarm_and_update_state(alarm_to_reset: AlarmDTO):
    if resolve_alarm_logic(alarm_to_reset):  # 실제로 문제 해결됨
        active_alarms.remove(alarm_to_reset)
        update_machine_state_from_alarms(active_alarms)

이런 구조는 사용자 조작 실수나 경고 무시 같은 위험을 줄이고, 
알람과 장비 동작 사이의 연계 신뢰성을 확보하는 데 중요한 역할을 합니다.

장비 제어의 안전성과 알람의 연결

결국 알람 시스템의 목적은 작업자에게 문제를 알려주는 것뿐 아니라, 장비를 안전하게 멈추고, 올바른 상태로만 동작하게 만들기 위한 제어 장치입니다.

이 챕터에서 소개한 로직은 단순한 알람 팝업을 넘어서, 알람 → 장비 정지 → 조치 후 복귀 라는 자연스러운 제어 흐름을 구현하는 데 꼭 필요한 핵심 요소입니다.

마무리하며

알람 시스템은 설비가 커질수록, 그리고 운영이 길어질수록 신뢰성과 구조적인 관리가 점점 더 중요해지는 영역입니다.
단순히 메시지를 띄우는 수준에서 시작하더라도, 이런 확장 포인트들을 염두에 두고 구조를 설계하면 나중에 복잡한 상황에도 유연하게 대응할 수 있는 알람 시스템을 만들 수 있습니다.