출결 계산을 반복할 때 같은 데이터를 다시 읽고 있었다
LearningX 종합성적부는 학생별 출결 요약을 계산한다. 학생 반복문 안에서 사용자 정보와 해당 과목의 출결을 각각 읽고, 과제 묶음별 계산에서는 같은 Lesson을 다시 찾을 수 있었다. 계산식이 같아도 반복되는 조회가 학생과 묶음 수에 따라 늘어나는 구조다.
학기 출결 다운로드에 일괄 조회와 캐시를 도입한 뒤, 종합성적부 요약 경로로 확장했다. 종합성적부 경로는 고유 로그인 ID를 모아 사용자를 한 번 읽고, 과목과 로그인 목록으로 출결을 읽어 사용자·section·component별로 묶는다. Lesson은 과목 단위로 읽어 course·week·lesson 위치로 찾는 메모리 캐시를 채운다.
반복 조회를 메모리 조회로 바꾸는 대신 과목 전체 데이터와 스냅샷을 보관하는 비용이 생긴다. 학기 다운로드의 다른 일괄 도우미에는 나누어 읽는 처리가 있지만, 이 종합성적부 경로는 사용자와 출결 각각의 단일 whereIn을 사용한다. 조회 횟수 감소를 그대로 DB 계산 비용이나 응답 시간 감소 수치로 해석할 수는 없다.
내부 함수가 외부에서 채운 캐시까지 비우면 안 된다
AttendanceHelper는 기본 singleton과 학교별 커스텀 도우미를 함께 사용할 수 있다. 외부 배치가 이미 캐시를 채운 상태로 요약 함수를 호출하면 그 캐시를 이어 써야 한다. 내부 계산이 끝났다고 무조건 비우면 외부의 다음 계산이 준비된 데이터를 잃는다. 반대로 내부에서 채운 캐시를 남겨두면 이후 계산이 오래된 스냅샷을 읽을 수 있다.
변경은 두 도우미 모두 캐시가 없는 경우에만 새로 채우고 primedHere를 기록한다. 계산의 finally는 이 호출이 채웠을 때만 정리한다. 다음은 객체별 주입·정리 부분을 생략한 흐름 요약이다.
$primedHere = false;
if (!$base->hasLessonCache() && !$custom->hasLessonCache()) {
// 두 도우미에 과목의 Lesson 캐시를 주입한다.
$primedHere = true;
}
try {
// 학생별 출결 요약 계산
} finally {
if ($primedHere) {
// 이 호출이 주입한 캐시만 비운다.
}
} 이 규칙에는 범위 전제가 있다. 어느 한 도우미에만 캐시가 있어도 새 주입을 건너뛰므로 두 도우미가 항상 같은 범위를 갖는다는 보장은 아니다. 또 캐시가 존재하는데 Lesson 키가 없으면 단건 DB 조회로 돌아가지 않고 없는 항목으로 처리한다. “캐시 없음”과 “채워진 캐시에 항목 없음”을 다르게 다루므로, 주입한 캐시가 계산 범위를 충분히 덮어야 한다.
캐시를 읽는 객체까지 맞췄다
캐시를 올바르게 채워도 다음 조회가 다른 객체를 쓰면 효과가 없다. 학기 다운로드 경로에서 커스텀 도우미를 얻는 accessor는 파일이 있으면 호출할 때마다 새 객체를 만들었다. 캐시를 주입받은 객체와 실제 Lesson을 조회하는 객체가 달라질 수 있었다.
후속 변경은 static customAttendanceHelperInstance에 커스텀 도우미 또는 기본 singleton을 보관하도록 바꿨다. 이후 accessor 호출이 같은 객체를 돌려줘 주입·조회·정리가 하나의 캐시를 대상으로 이루어진다. 객체 수명 문제는 일괄 조회를 도입한 뒤 별도로 보강한 부분이다.
호출부마다 같은 객체를 인자로 전달하는 방법도 가능하지만 학교별 경로를 함께 고쳐야 한다. accessor에서 동일성을 유지하면 변경 위치가 작아지는 대신 공유 객체의 정리가 더 중요해진다. 종합성적부의 finally와 달리 학기 다운로드의 정리 호출은 정상 종료 뒤에 있다. 이 패턴 전체에 예외 정리가 보장된다고 일반화할 수는 없다.
일괄 조회가 줄어든 것과 같은 행을 선택하는 것은 별개다
사용자 일괄 조회는 orderBy('_id') → get() → keyBy('user_login')을 사용한다. 같은 로그인 문서가 여러 개라면 Collection의 keyBy는 뒤의 값으로 앞의 값을 덮는다. 오름차순 결과에서는 마지막 문서가 남는다. 기존의 명시적 정렬 없는 first()가 선택한 문서와 같다고 보장할 수 없다. 결과 일치를 판단하려면 로그인의 유일성이나 중복 때 선택 규칙부터 확인해야 한다.
관련 변경에는 이 캐시 소유권·객체 동일성의 전용 회귀 테스트가 추가되지 않았다. 캐시를 채운 주체만 정리하는 규칙은 코드에서 확인되지만, 모든 학교별 커스텀 경로의 결과 동등성과 장기 실행에서의 수명까지 입증하지는 않는다. 반복 조회를 줄이는 설계에는 캐시의 범위, 객체의 동일성, 정리 책임이 함께 필요하다.