사용자 행동 로그를 뽑아 달라는 요청이 있어 DLP 의 Moodle DB 에서 관련 테이블을 찾아봤다. 종류별로 테이블이 나뉘어 있을 것으로 생각했는데 mdl_logstore_standard_log 하나였고, 이벤트 종류는 eventname 컬럼 하나로 구분되고 있었다.
우리가 local/dlp 플러그인에서 남기는 이벤트도 같은 표로 들어간다. 그 구조가 주는 것과 잃는 것을 조회를 짜면서 하나씩 확인한 기록이다.
Table of contents
Open Table of contents
배경 — 하나뿐인 로그 테이블
이 구조에서는 새 종류의 행동을 기록하려고 해도 테이블을 새로 만들 필요가 없다. 이벤트 클래스 하나를 정의하면 그 클래스 이름이 eventname 값이 되어 새 이벤트가 된다.
기록하는 쪽 코드도 종류에 상관없이 $event->trigger() 한 자리로 들어가므로 단순하다. 새 기능을 붙일 때 스키마 변경이 없다는 것이 이 설계의 핵심적인 이점이었다.
이 설계가 주는 것
스키마가 안 바뀌면 배포할 때 DB 작업이 없고 되돌리기도 쉽다. 로그를 남기는 것 때문에 배포가 무거워지지 않는다.
조회하는 쪽에서도 여러 종류를 한 번에 가져올 때는 오히려 편하다. userid 하나로 걸고 timecreated 순서로 늘어놓으면 그 사용자의 행동이 조인 없이 시간순으로 나온다.
증상 — 조회가 문자열 매칭이 된다
대신 특정 종류만 뽑을 때는 eventname 값을 문자열로 맞춰야 한다. 값이 \core\event\course_viewed 처럼 백슬래시가 든 클래스 이름이라, PHP 문자열에서 이스케이프를 하나 빠뜨려도 아무도 안 알려 주고 결과만 0건이 된다.
두 가지 방법이 있었다. 문자열을 그대로 두고 조회마다 눈으로 확인하는 것과, 이벤트 이름을 코드에서 상수로 뽑아 쓰는 것이다. 뒤쪽을 골랐다. 이유는 문자열을 직접 적는 자리가 남아 있으면 언젠가 오타가 조용히 들어가서다. 이제 코어 이벤트는 \core\event\course_viewed::class 로, 우리 이벤트는 \local_dlp\event\lesson_completed::class 로 적는다.
이벤트 목록과 인덱스 편중
어떤 이벤트가 있는지를 스키마가 알려 주지 않으므로 그 목록은 사람이 문서로 만들어야 했다. SELECT eventname, COUNT(*) … GROUP BY eventname 으로 실제 값을 집계해 보니 문서에 없던 것이 몇 개 나왔는데, 전부 나중에 설치한 플러그인이 남기는 이벤트였다.
인덱스는 Moodle 로그 저장소의 스키마 명세(install.xml)에 셋뿐이다 — timecreated 단독, courseid, anonymous, timecreated, userid, contextlevel, contextinstanceid, crud, edulevel, timecreated. eventname 단독 인덱스는 없으므로 종류만으로 거르는 조회는 표 전체를 훑는다. 게다가 이 표는 모든 이벤트가 들어오니 가장 빨리 자란다. 그래서 조회에는 timecreated 기간 조건을 함께 걸도록 했다.
조치 — 고정 식별자 대신 역할로 거른다
기존 조회 중에 관리자·시험 계정을 제외하는 것이 있었는데, 그 계정을 userid NOT IN (2, 15, 33) 처럼 식별자로 박아 두고 있었다. 그런 계정이 하나 늘어나면 조건이 조용히 부족해진다.
그래서 mdl_role_assignments 의 roleid 를 기준으로 거르도록 조건을 손봤다. 근거는 계정은 늘지만 역할은 그대로라는 점이다. 새 시험 계정에 같은 역할만 주면 조회는 손대지 않아도 된다.
주의 — 다른 DB 와의 조인 전제
다른 DB 의 테이블과 조인하는 조회도 있었다. 사번으로 인사 DB 의 부서를 붙이는 것인데, SQL Server 에서는 같은 인스턴스 안의 다른 데이터베이스를 hrdb.dbo.employees 처럼 database.schema.object 세 마디 이름으로 바로 참조할 수 있어서 그렇게 되어 있었다.
T-SQL 구문 규칙 공식 문서는 이 형태를 로컬 인스턴스에 있는 객체에 한해 허용하고, 다른 서버면 앞에 링크된 서버 이름이 붙는다고 적고 있다. 즉 이 조회는 두 DB 가 같은 인스턴스에 있다는 전제에 기대므로 그 전제를 조회 문서에 명시했다. 인사 DB 가 다른 서버로 옮겨 가는 날 이 조회는 소리 없이 깨진다.
정리
- 이벤트 로그를 한 테이블에 담으면 스키마가 안 바뀐다
- 새 기능을 붙일 때 DB 작업이 없다
- 대신 조회가
eventname문자열 매칭이 된다 - 문자열 오타는 아무도 안 알려 주고 0건만 나온다
- 이벤트 이름은
::class상수로 뽑는다 - 어떤 이벤트가 있는지 스키마가 안 알려 주므로 집계해서 문서로 만든다
eventname단독 인덱스가 없고 그 표가 가장 빨리 자라므로timecreated조건을 함께 건다- 고정 식별자로 거르면 대상이 늘 때 조용히 부족해지므로 역할로 거른다
- 다른 DB 와의 조인은 같은 인스턴스 전제를 문서에 적는다