작업으로 돌아가기 프로젝트 01 — 알림 인프라

LXP 알림

부산가톨릭대 LXP의 과제·공지·휴보강 변경을 감지해 Discord와 이메일로 알려주는 다중 사용자 서비스입니다. 과제 마감을 놓친 제 경험에서 시작해, 여러 사람이 계정을 맡겨도 안전하고 비용 없이 돌아가는 구조를 만드는 데 집중했습니다.

기간
2025약 2개월 + 운영 중
역할
1인 개발기획·백엔드·인프라·운영
핵심 영역
백엔드 · 인프라큐 · 워커 · 암호화
스택
TS · NodePrisma · MariaDB · Docker
GitHub 저장소 → 배포 사이트 ↗
01

배경

왜 만들었나

학교 LXP에는 과제, 공지, 그리고 주차별 휴강·보강 변경이 흩어져 올라옵니다. 알림이 약해서 한 번 놓치면 그대로 마감을 넘기기 일쑤였고, 실제로 그렇게 과제 하나를 못 낸 적이 있습니다.

매번 직접 들어가 확인하는 대신, 변경이 생기면 알아서 알려주는 도구를 만들기로 했습니다. 혼자 쓰려고 만들었지만 같은 불편을 겪던 친구들이 쓰기 시작하면서, 여러 사용자의 LXP 계정을 안전하게 보관하고 각자에게 알림을 보내는 다중 사용자 서비스로 발전시켰습니다.

운영 주체가 학생 한 명이다 보니 처음부터 기준이 명확했습니다 — 운영비는 0원에 가깝게, 손은 최대한 덜 가게.

02

주요 기능

무엇을 하는가
  • 전체 수강 과목 자동 감지수강 과목 목록을 기준으로 과제·강의실 공지·주차별 휴보강을 주기적으로 수집합니다.
  • Discord DM · 이메일 알림Discord 로그인만 하면 별도 설정 없이 DM으로 받고, SMTP/Resend로 이메일도 함께 보냅니다.
  • 사용자별 채널 · 설정Discord/email 채널을 각자 독립적으로 켜고, 최근 로그와 휴보강 종합을 화면에서 확인합니다.
  • 계정 암호화 보관LXP 자격증명과 webhook URL을 암호화해 저장하고, 어디에도 평문을 노출하지 않습니다.
03

기술 선택

왜 이 기술을 썼나
Discord DM기본 알림 채널
사용자가 webhook을 따로 만들 필요 없이 로그인만으로 받을 수 있고, 비용이 0원이라 MVP의 기본값으로 택했습니다. KakaoTalk·SMS 같은 유료 채널은 의도적으로 제외했습니다.
Prisma + MariaDB데이터 계층
사용자·구독·전송 큐·중복 방지 상태를 관계형으로 또렷하게 모델링하기 위해 선택했습니다. Prisma 마이그레이션으로 스키마 변경 이력도 관리합니다.
Docker Compose배포 단위
web·bot·worker·DB를 한 벌로 묶어 동일 환경에서 재현하고, 한 명이 운영해도 한 번에 띄우고 내릴 수 있게 했습니다.
Playwright데이터 수집
LXP에 공개 API가 없어 로그인 세션 기반으로 화면을 직접 읽어 과제·공지·휴보강을 가져옵니다.
04

트러블슈팅

부딪힌 문제와
해결 과정
중복 전송

같은 알림이 두 번, 과거 항목까지 한꺼번에

문제워커가 주기적으로 돌면서 이미 보낸 알림을 다시 보내거나, 처음 연결한 사용자에게 학기 내내 쌓인 과거 과제가 한꺼번에 쏟아졌습니다.
원인"무엇을 이미 알렸는가"에 대한 상태가 없어서, 매 실행이 전체 목록을 새 항목으로 취급했습니다.
해결최초 연결 시점에 현재 항목을 baseline으로 저장해 과거 항목은 알리지 않도록 하고, DB 기반 전송 큐 + 채널별 중복 전송 방지로 같은 항목이 두 번 큐에 들어가지 않게 했습니다.
결과신규 사용자 연결 시 과거 폭탄 알림이 사라지고, 재실행해도 같은 알림이 중복되지 않는 안정적인 전송이 됐습니다.
Rate limit

Discord 전송이 429로 막힘

문제사용자가 늘자 Discord DM 전송이 HTTP 429 Too Many Requests로 실패하기 시작했습니다.
원인짧은 시간에 다수 사용자에게 동시에 전송하면서 Discord rate limit에 걸렸고, 실패한 전송이 그대로 유실됐습니다.
해결429 응답의 retry_after 값을 읽어 DB 큐에서 해당 시간 뒤 재시도하도록 하고, 워커에 지터·제한 동시성을 두어 전송을 분산했습니다.
결과일시적 rate limit이 더 이상 알림 유실로 이어지지 않고, 지연되더라도 결국 전달되는 형태로 바뀌었습니다.
보안

남의 LXP 비밀번호를 어떻게 보관할 것인가

문제알림을 받으려면 사용자의 LXP 계정으로 로그인해야 하는데, 타인의 자격증명을 평문으로 두는 건 위험했습니다.
고민학생이 운영하는 서비스에 계정을 맡기는 만큼, 최소한 DB가 유출돼도 바로 악용되지 않아야 한다고 봤습니다.
해결LXP 자격증명과 Discord webhook URL을 AES-256-GCM으로 암호화 저장하고, 키는 32바이트 환경변수로 분리했습니다. 로그·UI·에러 메시지 어디에도 평문을 출력하지 않도록 했습니다.
결과DB만으로는 자격증명을 복원할 수 없고, 운영 중 실수로 민감정보가 로그에 남을 경로를 차단했습니다.
05

배운 점

회고

"한 번 보내고 끝"이 아니라 "두 번 안 보내기"가 더 어렵다. 전송 그 자체보다 멱등성·중복 방지·재시도 같은 보이지 않는 장치가 서비스의 신뢰를 만든다는 걸 체감했습니다.

남의 데이터를 다루는 책임감. 친구들의 계정을 맡으면서 암호화·시크릿 관리·로그 위생을 처음으로 진지하게 고민했고, 보안이 기능이 아니라 기본값이어야 한다는 걸 배웠습니다.

운영까지가 개발이다. Docker Compose로 web·bot·worker를 묶고 DRY_RUN으로 안전하게 점검하면서, 만드는 것과 계속 돌게 하는 것은 다른 일이라는 걸 알게 됐습니다.

다음 프로젝트
EJClaw — 멀티에이전트 런타임