출석 모니터링 시스템 - 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단계 중복 방지 시스템
- DB Unique Index (
sk_id+entry_id해시) - 메모리 캐시 (빠른 조회)
- API 조회 전 사전 필터링
- DB Unique Index (
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_logs | VOD 시청 기록 | 교시별 데이터 |
openeg_tasks | 과제 정보 | v2.1 신규 |
openeg_task_submissions | 과제 제출 내역 | v2.1 신규 |
openeg_pbl_problems | PBL 문제 | |
openeg_pbl_submissions | PBL 제출 | |
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단계 중복 방지 시스템
- DB Unique Index (영구적)
- 메모리 캐시 (빠른 조회)
- 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🔗 관련 프로젝트
메인 시스템
- 출석 모니터링 시스템 - 전체 아키텍처
- 출석 수집 백엔드 - Google Sheets 수집
데이터 흐름
[본 시스템: LMS 웹훅]
↓ (데이터 수집)
[MySQL DB]
↓ (데이터 제공)
[egatten 프론트엔드] → 사용자에게 표시
📝 배운 점
기술적 학습
-
비동기 프로그래밍 마스터
asyncio를 활용한 대규모 병렬 처리aiohttp로 효율적인 HTTP 통신- 동시성 제어 및 리소스 관리
-
안정적인 백그라운드 서비스 설계
- 자동 복구 시스템 구현
- Graceful Shutdown 처리
- DB 커넥션 관리 노하우
-
중복 방지 전략
- DB Unique Index 활용
- 메모리 캐시와 DB의 조합
- 해시 기반 식별자 설계
-
단일 파일 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