출석 모니터링 시스템 - LMS 웹훅

OpenEG LMS Webhook System v2.1 - LMS 이벤트 실시간 감지 및 학습 데이터 수집

📋 프로젝트 개요

LMS(Learning Management System)에서 발생하는 이벤트를 실시간으로 감지하고, 학습 데이터를 자동으로 수집하여 Google Chat으로 알림을 전송하는 비동기 백그라운드 서비스입니다.

시스템 역할

  • 실시간 알림: 문의사항 발생 시 Google Chat 웹훅 즉시 전송
  • 데이터 수집: VOD 로그, PBL, 과제(Task), 로드맵 자동 수집
  • 통계 연동: 수집 데이터를 통계 서버(testaten.yos.kr)로 자동 전송
  • 자동 복구: 토큰 만료 시 자동 재로그인, DB 커넥션 자동 갱신

다른 시스템과의 관계

┌─────────────────────────────────────────┐
│   출석 모니터링 시스템 (전체)            │
├─────────────────────────────────────────┤
│ 1. egatten (프론트엔드)                  │
│    └─ 시각화 및 사용자 인터페이스        │
│                                          │
│ 2. 출석 수집 백엔드                      │
│    └─ Google Sheets 출석 데이터 수집     │
│                                          │
│ 3. LMS 웹훅 (본 시스템) ⭐               │
│    ├─ LMS 이벤트 실시간 감지             │
│    ├─ 학습 데이터 수집                   │
│    └─ Google Chat 알림                  │
└─────────────────────────────────────────┘

🎯 해결한 문제

문제

  • 문의사항 누락: 학습자 문의사항을 실시간으로 파악하기 어려움
  • 학습 데이터 분산: VOD, PBL, 과제 데이터가 LMS에만 존재
  • 수동 모니터링: 운영자가 LMS를 직접 확인해야 함
  • 중복 알림: 같은 문의사항이 반복해서 알림될 수 있음

솔루션

  • 60초 폴링: LMS API를 주기적으로 조회하여 실시간 감지
  • 중복 방지: sk_id + entry_id 해시 기반 DB Unique Index 관리
  • 비동기 처리: asyncio로 수천 명의 데이터를 병렬 처리
  • 자동 복구: 토큰 만료, DB 커넥션 끊김 시 자동 복구

🛠 기술 스택

Backend

  • Language: Python 3.9+ (Type Hinting)
  • Concurrency: asyncio (이벤트 루프 기반 비동기)
  • HTTP: aiohttp 3.8+ (비동기 HTTP 클라이언트)

Database

  • Database: MySQL 5.7+ / MariaDB 10.3+
  • ORM: SQLAlchemy 1.4.x (Thread-safe 세션 관리)
  • Driver: PyMySQL 1.1+ (Pure Python)

Infrastructure

  • Container: Docker
  • Orchestration: Kubernetes
  • Validation: Pydantic 1.10+ (환경변수 검증)

External APIs

  • OpenEG LMS API: 문의사항, VOD, PBL, Task 데이터
  • Google Chat Webhook: 실시간 알림
  • 통계 서버: testaten.yos.kr (데이터 시각화)

💡 주요 구현 내용

1. 실시간 문의사항 모니터링

# 60초 간격 폴링
async def monitor_inquiries():
    while True:
        # LMS API 조회
        inquiries = await lms_service.get_inquiries()
        
        # 중복 체크 (DB Unique Index)
        new_inquiries = await filter_new_inquiries(inquiries)
        
        # Google Chat 알림
        for inquiry in new_inquiries:
            await send_webhook(inquiry)
        
        await asyncio.sleep(60)

특징:

  • 동시성 제어: asyncio.Semaphore로 최대 15개 동시 요청
  • 중복 방지: 3단계 중복 방지 시스템
    1. DB Unique Index (sk_id + entry_id 해시)
    2. 메모리 캐시 (빠른 조회)
    3. API 조회 전 사전 필터링

2. 학습 데이터 일괄 수집

매일 새벽 02:00 (KST) 실행:

async def collect_daily_data():
    # 1. VOD 시청 로그
    vod_logs = await collect_vod_logs()
    
    # 2. PBL 문제 및 제출
    pbl_data = await collect_pbl_data()
    
    # 3. 과제(Task) 정보 (v2.1 신규)
    tasks = await collect_tasks()
    
    # 4. 로드맵 (유닛/차시)
    roadmap = await collect_roadmap()
    
    # 5. 통계 서버 전송
    await send_to_statistics_server()

수집 데이터:

데이터설명주요 필드
VOD 로그동영상 시청 기록교시, 시청 시간, 8교시 출석 여부
PBL문제 목록 및 제출문제 ID, 제출 상태, 점수
Task과제 정보 (v2.1)과제 ID, 제출 유형, 제출 현황
로드맵유닛/차시 구조코스 구조, 진도율

3. 통계 웹훅 연동 (v2.1 신규)

class StatisticsWebhookService:
    async def send_vod_statistics(
        self, 
        sk_ids: List[str], 
        collection_date: str
    ) -> Dict:
        payload = {
            "sk_ids": sk_ids,
            "target_date": collection_date,
            "data_types": ["vod", "pbl", "task", "roadmap"]
        }
        
        # 재시도: 최대 3회 (Exponential Backoff)
        await self.post_with_retry(
            "https://testaten.yos.kr/api/webhook/statistics-trigger",
            payload
        )

특징:

  • VOD 로그 수집 완료 직후 자동 트리거
  • 지수 백오프 재시도 (3회)
  • 타임아웃: 30초

4. 자동 복구 시스템

자동 재로그인

async def api_call_with_auth(self, endpoint: str):
    try:
        response = await self.get(endpoint)
        return response
    except Unauthorized:  # 401 Error
        # 자동 재로그인
        await self.login()
        # 요청 재시도
        return await self.get(endpoint)

DB 커넥션 갱신

# 30분마다 커넥션 재생성
async def refresh_db_connection():
    while True:
        await asyncio.sleep(1800)  # 30분
        db_manager.recreate_session()

Graceful Shutdown

def handle_sigterm(signum, frame):
    logger.info("SIGTERM 수신, 안전 종료 시작...")
    # 진행 중인 작업 완료 대기
    # 리소스 정리
    sys.exit(0)

🏗 시스템 아키텍처

Layered Architecture

┌─────────────────────────────────────┐
│   Orchestration Layer               │
│   (WebhookProcessor)                │
│   - 메인 이벤트 루프                 │
│   - 태스크 조율                      │
└─────────────────────────────────────┘
            ↓
┌─────────────────────────────────────┐
│   Service Layer                     │
│   ├─ LMSApiService                  │
│   ├─ VodLogService                  │
│   ├─ StatisticsWebhookService       │
│   ├─ WebhookService                 │
│   └─ HealthChecker                  │
└─────────────────────────────────────┘
            ↓
┌─────────────────────────────────────┐
│   Data Access Layer                 │
│   (DatabaseManager)                 │
│   - Connection Pool                 │
│   - Transaction 관리                │
└─────────────────────────────────────┘

데이터 흐름

[LMS API]
    ↓ (60초 폴링)
[이벤트 감지]
    ↓
[중복 체크]
    ├─ (신규) → [Google Chat 알림]
    └─ (중복) → [Skip]

[매일 02:00]
    ↓
[일괄 데이터 수집]
    ↓
[MySQL 저장]
    ↓
[통계 서버 전송]

📊 데이터베이스 설계

주요 테이블 (10개 자동 생성)

테이블용도비고
openeg_entry_tracking문의사항 처리 이력중복 방지 핵심
openeg_vod_logsVOD 시청 기록교시별 데이터
openeg_tasks과제 정보v2.1 신규
openeg_task_submissions과제 제출 내역v2.1 신규
openeg_pbl_problemsPBL 문제
openeg_pbl_submissionsPBL 제출
openeg_gather_town_info기수 계정 정보로그인 정보

중복 방지 스키마

CREATE TABLE openeg_entry_tracking (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    sk_id VARCHAR(50) NOT NULL,
    entry_id VARCHAR(100) NOT NULL,
    entry_hash VARCHAR(64) NOT NULL,  -- sk_id + entry_id 해시
    processed_at DATETIME,
    UNIQUE KEY unique_entry (entry_hash),
    INDEX idx_sk_entry (sk_id, entry_id)
);

📈 성과

알림 효율성

  • 응답 시간: 문의사항 발생 후 평균 60초 이내 알림
  • 중복 방지율: 100% (DB Unique Index)
  • 알림 정확도: 허위 알림 0건

데이터 수집

  • 자동화율: 100% (매일 새벽 자동 실행)
  • 수집 완료 시간: 기수당 평균 5분
  • 데이터 정합성: 트랜잭션 배치 처리로 100% 보장

시스템 안정성

  • 가동률: 99.9% (Kubernetes 기반)
  • 자동 복구: 토큰 만료, DB 커넥션 끊김 자동 복구
  • 무인 운영: 1년간 수동 개입 3회 미만

성능

  • 동시 처리: 최대 15개 비동기 요청 (LMS 서버 부하 방지)
  • 메모리 사용량: 평균 200MB (단일 파일 아키텍처)
  • 처리 속도: 기수당 학생 100명 기준 2-3분

🔧 기술적 도전 과제

1. 대규모 비동기 처리

문제: 수천 명의 학생 데이터를 순차 처리 시 시간 소요 해결:

  • asyncio.gather로 병렬 처리
  • asyncio.Semaphore로 동시성 제어 (최대 15개)
  • 특정 계정 실패 시에도 다른 계정 영향 없도록 격리
async def process_all_accounts():
    sem = asyncio.Semaphore(15)
    
    async def process_with_limit(account):
        async with sem:
            try:
                await process_account(account)
            except Exception as e:
                logger.error(f"계정 {account} 실패: {e}")
                # 다른 계정은 계속 진행
    
    await asyncio.gather(*[
        process_with_limit(acc) for acc in accounts
    ])

2. 중복 알림 방지

문제: 같은 문의사항이 폴링 주기마다 반복 알림 해결:

  • 3단계 중복 방지 시스템
    1. DB Unique Index (영구적)
    2. 메모리 캐시 (빠른 조회)
    3. API 조회 전 필터링

3. 단일 파일 아키텍처의 유지보수

문제: 2,700줄 단일 파일로 코드 복잡도 증가 해결 방향:

  • 명확한 클래스 분리 (Service Layer 패턴)
  • Type Hinting으로 가독성 향상
  • 향후 모듈화 계획 (Refactoring 예정)

4. MySQL Gone Away 오류

문제: 장시간 유휴 상태 시 DB 커넥션 끊김 해결:

  • 30분마다 커넥션 자동 재생성
  • Connection Pool 설정 최적화
  • 재시도 로직 추가

🚀 배포 및 운영

Docker 컨테이너화

FROM python:3.9-slim
 
WORKDIR /app
COPY requirements_minimal.txt .
RUN pip install --no-cache-dir -r requirements_minimal.txt
 
COPY new_lms.py .
 
CMD ["python", "new_lms.py"]

Kubernetes 배포

apiVersion: apps/v1
kind: Deployment
metadata:
  name: lms-webhook
spec:
  replicas: 1
  template:
    spec:
      containers:
      - name: webhook
        image: openeg-webhook:v2.1
        env:
        - name: OPENEG_SLEEP_TIME
          value: "60"
        - name: OPENEG_MAX_CONCURRENT_REQUESTS
          value: "15"

환경 변수

# 데이터베이스
OPENEG_YOS_DB_HOST=***
OPENEG_YOS_DB_USER=***
OPENEG_YOS_DB_PASS=***
OPENEG_YOS_DB_NAME=openeg
 
# 튜닝
OPENEG_SLEEP_TIME=60
OPENEG_MAX_CONCURRENT_REQUESTS=15
OPENEG_VOD_LOG_ENABLED=True

🔗 관련 프로젝트

메인 시스템

데이터 흐름

[본 시스템: LMS 웹훅]
    ↓ (데이터 수집)
[MySQL DB]
    ↓ (데이터 제공)
[egatten 프론트엔드] → 사용자에게 표시

📝 배운 점

기술적 학습

  1. 비동기 프로그래밍 마스터

    • asyncio를 활용한 대규모 병렬 처리
    • aiohttp로 효율적인 HTTP 통신
    • 동시성 제어 및 리소스 관리
  2. 안정적인 백그라운드 서비스 설계

    • 자동 복구 시스템 구현
    • Graceful Shutdown 처리
    • DB 커넥션 관리 노하우
  3. 중복 방지 전략

    • DB Unique Index 활용
    • 메모리 캐시와 DB의 조합
    • 해시 기반 식별자 설계
  4. 단일 파일 vs 모듈화 트레이드오프

    • 배포 편의성 (단일 파일)
    • 유지보수성 (모듈화 필요성)
    • 적절한 균형점 찾기

시스템 운영 학습

  • 무인 운영: 1년간 안정적으로 운영되는 시스템 설계 경험
  • 모니터링: 로그 기반 문제 진단 및 대응
  • 점진적 개선: v2.0 → v2.1 버전업 (Task 수집 추가)
  • Kubernetes 운영: Pod 재시작, 리소스 관리 경험

🔮 향후 계획

단기 (완료됨)

  • Task(과제) 데이터 수집 (v2.1)
  • 통계 서버 연동 (v2.1)
  • 자동 복구 시스템 강화

중기 (검토 중)

  • 코드 모듈화 (services/, models/, utils/ 분리)
  • Prometheus 메트릭 엔드포인트 추가
  • 스트리밍 방식 데이터 처리 (메모리 최적화)

장기

  • 기수별 Worker 분산 (Kubernetes 스케일링)
  • 실시간 대시보드 구축
  • 머신러닝 기반 이탈 위험 예측

프로젝트 기간: 2024.01 - 2025.01 현재 상태: 완료됨 (운영 중) 최종 업데이트: 2025-01-21