Files
reloop-v2/ALGORITHM.md
reloop 420610a41d docs: ALGORITHM/README/report 최신 상태 반영
- 가격 2,900원 통일, 기능 게이트 문서화
- 코드 규모 45,641줄/71 API/15 모델/26 페이지
- 실현가능성/시장성 섹션 전면 재작성
- 2022 교육과정 프리셋, 학교 관리 4대 기능 반영

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 19:39:35 +09:00

16 KiB
Raw Permalink Blame History

ReLoop v2 복습 알고리즘 상세 설명

이 문서는 ReLoop v2의 복습 스케줄링이 어떻게 작동하는지를 코드 레벨에서 설명합니다.

요약: 두 가지 경로

ReLoop은 두 가지 복습 스케줄링 경로를 가지고 있고, 사용자의 행동에 따라 자동으로 갈림:

경로 언제 사용? 핵심 아이디어
SM-2 하이브리드 사용자가 "어려웠어/괜찮았어/쉬웠어"를 선택했을 때 체감 난이도로 다음 복습 간격 결정
에빙하우스 망각곡선 난이도 선택 없이 결과만 기록했을 때 (legacy) 기억 감쇄 시뮬레이션으로 복습 시점 계산

현재 앱의 주요 UI(복습 세션, e-book 뷰어, 처음 풀기)는 모두 selfDifficulty를 보내므로 SM-2 경로가 기본이다.


경로 1: SM-2 하이브리드 (메인)

1.1 개념

SM-2는 SuperMemo 2의 약자로, 1987년에 만들어진 간격 반복 알고리즘이다. 핵심 아이디어:

  • 각 문제(태그)마다 **EF (Ease Factor, 쉬움 계수)**를 관리한다 (기본값 2.5)
  • 쉽다고 느끼면 EF가 올라가고, 어렵다고 느끼면 내려간다
  • 다음 복습 간격 = 이전 간격 × EF
  • EF가 높을수록 간격이 빠르게 벌어진다

ReLoop은 여기에 페르소나 배율을 곱해서 학습자 유형별로 조정한다.

1.2 EF (Ease Factor) 업데이트

사용자가 "어려웠어/괜찮았어/쉬웠어"를 선택할 때마다:

어려웠어 → EF = max(1.3, 현재EF - 0.3)     // 급격히 하락
괜찮았어 → EF = 현재EF                       // 변동 없음
쉬웠어   → EF = max(1.3, 현재EF + 0.15)     // 소폭 상승
  • 최소 EF는 1.3 (이 이하로 내려가지 않음)
  • 초기 EF는 2.5
  • EF는 SkillSnapshot.easeFactor 필드에 저장됨 (태그별로 하나)
  • 기억 강도 S₀는 별도로 SkillSnapshot.s0 (0~1)에 저장됨

예시:

시작:     EF = 2.5
어려웠어: EF = 2.5 - 0.3 = 2.2
괜찮았어: EF = 2.2 (그대로)
쉬웠어:  EF = 2.2 + 0.15 = 2.35
어려웠어: EF = 2.35 - 0.3 = 2.05
어려웠어: EF = 2.05 - 0.3 = 1.75
어려웠어: EF = 1.75 - 0.3 = 1.45
어려웠어: EF = max(1.3, 1.45 - 0.3) = 1.3  ← 바닥

1.3 복습 간격 계산

페르소나 배율 (pf):
  상위권 (senior)  = 1.3  → 간격이 30% 더 넓어짐
  중위권 (mid)     = 1.0  → 기본
  하위권 (junior)  = 0.8  → 간격이 20% 좁아짐
  벼락치기 (crammer)= 0.6  → 간격이 40% 좁아짐

학습 강도 배율 (if):
  엄격 (strict)   = 0.7  → 간격이 30% 좁아짐 (더 자주 복습)
  균형 (moderate)  = 1.0  → 기본
  여유 (relaxed)   = 1.4  → 간격이 40% 넓어짐 (덜 자주 복습)

어려웠어 → 무조건 1일 후 (어떤 상황이든):

간격 = 1일

처음 푸는 문제 (iteration = 0):

괜찮았어 → 간격 = round(3 × pf × if)일
쉬웠어   → 간격 = round(7 × pf × if)일
체감 난이도 senior (×1.3) mid (×1.0) junior (×0.8) crammer (×0.6)
어려웠어 1일 1일 1일 1일
괜찮았어 4일 3일 2일 2일
쉬웠어 9일 7일 6일 4일

첫 번째 복습 (iteration = 1):

어려웠어 → 1일
그 외    → 간격 = round(6 × pf)일

두 번째 이후 복습 (iteration >= 2):

어려웠어 → 1일
그 외    → 간격 = round(6 × EF^(iteration-1) × pf × if)일
iteration EF=2.5, mid(×1.0) EF=2.5, senior(×1.3) EF=1.5, mid(×1.0)
0 (처음) 3일 (괜찮음) 4일 3일
1 6일 8일 6일
2 15일 20일 9일
3 38일 49일 14일
4 60일 (cap) 60일 (cap) 20일

모든 간격은 최소 1일, 최대 60일로 제한된다.

1.4 전체 흐름 (SM-2 경로)

[사용자가 문제를 풀고 "어려웠어" 클릭]
  ↓
1. SkillSnapshot에서 현재 EF 로드 (없으면 2.5)
  ↓
2. EF 업데이트: EF = max(1.3, EF - 0.3)
  ↓
3. 간격 계산: 1일 (어려웠어는 항상 1일)
  ↓
4. 간격 × 페르소나 배율 (어려웠어는 1일 고정이라 배율 적용 안 됨)
  ↓
5. ReviewSchedule 생성 (scheduledAt = 지금 + 1일)
  ↓
6. SkillSnapshot.easeFactor에 새 EF, s0에 기억 강도 저장
  ↓
[1일 후 복습 큐에 등장]
  ↓
[사용자가 "쉬웠어" 클릭]
  ↓
1. EF = max(1.3, 이전EF + 0.15)
  ↓
2. 간격 = round(6 × EF^(1-1) × pf × if) = 6일 (mid+moderate 기준)
  ↓
3. 망각곡선 보정: 6일 후 예상 P 시뮬레이션
  ↓
4. P >= 30%이면 6일 유지, P < 30%이면 안전 간격으로 축소
  ↓
5. 새 ReviewSchedule (scheduledAt = 보정된 간격)
  ↓
[n일 후 다시 복습 큐에 등장]
  ↓
... 반복 (iteration 증가 → EF 지수승 → 간격 기하급수적 확대)

1.5 망각곡선 안전장치

SM-2가 계산한 간격이 너무 길어서 그 시점에 기억이 거의 사라졌을 가능성을 방지한다:

1. SM-2가 간격 N일을 계산
2. 망각곡선으로 N일 후 기억 강도 시뮬레이션:
   futureS = s0 × e^(-λ × N)
   futureP = sigmoid(k × (futureS - D))
3. futureP < 0.3 (30%)이면:
   - 정답 확률 50%가 되는 시점을 역산
   - 그 시점으로 간격 축소
4. futureP >= 0.3이면: SM-2 간격 그대로 사용

이 보정은 벼락치기형(λ=0.6)처럼 빠르게 잊는 사용자에게 특히 효과적. SM-2가 6일을 계산해도 실제로 3일 만에 기억이 30% 이하로 떨어진다면 3일로 줄여준다.


경로 2: 에빙하우스 망각곡선 (Legacy)

이 경로는 selfDifficulty가 없을 때만 사용됨. 현재 UI에서는 거의 발생하지 않는다.

2.1 개념

에빙하우스의 망각곡선은 "시간이 지남에 따라 기억이 지수적으로 감쇄한다"는 모델:

S(t) = S₀ × e^(-λ × t)
  • S₀: 현재 기억 강도 (0~1). 문제를 맞히면 올라가고, 틀리면 내려감
  • λ (람다): 망각 속도. 페르소나별로 다름
  • t: 마지막 학습으로부터 경과 일수

여기에 "정답 확률"을 시그모이드 함수로 변환:

P(정답) = sigmoid(k × (S(t) - D))
        = 1 / (1 + e^(-k × (S(t) - D)))
  • k: 시그모이드 기울기 (고정값 4.0). 클수록 S(t)의 작은 변화에도 P가 급변
  • D: 문제 난이도 (0~1). 높을수록 어려운 문제

2.2 S₀ (기억 강도) 업데이트

문제를 풀 때마다 결과에 따라 S₀가 변경:

맞음 (correct)   → 새S₀ = clamp(이전S₀ × 0.5 + 0.6, 0, 1)
부분 (partial)   → 새S₀ = clamp(이전S₀ × 0.7 + 0.3, 0, 1)
틀림 (incorrect) → 새S₀ = clamp(이전S₀ × 0.5, 0, 1)
이전 S₀ 맞음 부분 틀림
0.3 (초기) 0.75 0.51 0.15
0.5 0.85 0.65 0.25
0.8 1.0 0.86 0.40

2.3 다음 복습 시점 계산

"정답 확률이 기준선 아래로 떨어지는 시점"을 구한다:

기준선 (P_threshold) — 학습 강도 설정:
  엄격 (strict)   = 0.70  → 아직 70%일 때 미리 복습 (자주)
  균형 (moderate)  = 0.50  → 반반일 때 복습 (기본)
  여유 (relaxed)   = 0.35  → 많이 까먹은 후 복습 (드물게)

수학적으로 풀면:

P_threshold = sigmoid(k × (S₀ × e^(-λ × t) - D))

이것을 t에 대해 풀면:

t = -(1/λ) × ln((D + logit(P_threshold)/k) / S₀)

여기서 logit(p) = ln(p / (1-p))

2.4 예시 시나리오

설정: 중위권(mid, λ=0.2), 균형(moderate, threshold=0.5), S₀=0.75, D=0.5

logit(0.5) = ln(0.5/0.5) = 0
target = 0.5 + 0/4 = 0.5

t = -(1/0.2) × ln(0.5 / 0.75)
  = -5 × ln(0.667)
  = -5 × (-0.405)
  = 2.03일 → 약 2일 후 복습

같은 조건, 상위권(senior, λ=0.1):

t = -(1/0.1) × ln(0.5 / 0.75)
  = -10 × (-0.405)
  = 4.05일 → 약 4일 후 복습 (느리게 잊으니까 더 늦게)

같은 조건, 벼락치기(crammer, λ=0.6):

t = -(1/0.6) × ln(0.5 / 0.75)
  = -1.67 × (-0.405)
  = 0.68일 → 약 16시간 후 복습 (빨리 잊으니까 빨리)

페르소나별 효과 비교

SM-2 경로에서 (현재 메인)

페르소나는 PERSONA_FACTOR 배율로만 영향:

상위권 ×1.3 → 모든 간격이 30% 넓어짐
중위권 ×1.0 → 기본
하위권 ×0.8 → 모든 간격이 20% 좁아짐
벼락치기 ×0.6 → 모든 간격이 40% 좁아짐

학습 강도(reviewIntensity)는 INTENSITY_SM2_FACTOR 배율로 영향:

엄격 ×0.7 → 모든 간격이 30% 좁아짐 (더 자주 복습)
균형 ×1.0 → 기본
여유 ×1.4 → 모든 간격이 40% 넓어짐 (덜 자주 복습)

추가로 망각곡선 안전장치가 작동하여 SM-2 간격 후 예상 P가 30% 이하면 자동 축소.

망각곡선 경로에서 (legacy)

페르소나는 **PERSONA_LAMBDA (망각 속도)**로 영향:

상위권: λ=0.1 → 하루에 10%씩 감쇄 → 천천히 잊음 → 넓은 간격
중위권: λ=0.2 → 하루에 20%씩 감쇄 → 보통
하위권: λ=0.4 → 하루에 40%씩 감쇄 → 빨리 잊음 → 좁은 간격
벼락치기: λ=0.6 → 하루에 60%씩 감쇄 → 매우 빨리 잊음 → 매우 좁은 간격

학습 강도는 P_threshold로 영향:

엄격: P가 70% 아래로 떨어지기 전에 복습 → 자주 복습
균형: P가 50% 아래로 떨어지기 전에 복습 → 보통
여유: P가 35% 아래로 떨어지기 전에 복습 → 드물게 복습

SkillSnapshot 필드 구조

SkillSnapshot은 태그별로 두 개의 독립적인 값을 저장한다:

필드 범위 의미 사용처
s0 0 ~ 1.0 기억 강도 망각곡선 보정, 예상 P 계산
easeFactor 1.3 ~ 4.0+ (nullable) SM-2 Ease Factor SM-2 간격 계산

두 값은 항상 동시에 업데이트된다. SM-2 경로에서도 s0(기억 강도)를 업데이트하고, 망각곡선 보정에 사용한다. easeFactor가 null이면 해당 태그에서 SM-2가 아직 사용되지 않았음을 의미한다.


밀린 복습 자동 이월

복습 예정일에 안 풀면 다음날 아침 9시(KST)로 자동 이월:

복습 큐 조회 시 (GET /reviews/queue):
  1. 오늘 00:00 KST 이전의 pending 상태 복습을 찾음
  2. 해당 복습의 scheduledAt을 "오늘 09:00 KST"로 변경
  3. 변경된 수를 rescheduledCount로 반환

단, 수학 과목 + 사용자 직접 등록 문제만 이월됨 (기출 문제집 문제는 제외).


페르소나 변경 시 재계산

프로필에서 페르소나나 학습 강도를 바꾸면 모든 pending 복습 일정이 재계산된다:

  1. 해당 유저의 pending 상태 ReviewSchedule 전부 로드
  2. 각 리뷰에 대해:
    • SM-2 경로: 새 페르소나의 PERSONA_FACTOR로 간격 재계산 (selfDifficulty='medium' 가정)
    • 망각곡선 경로: 새 λ, 새 threshold로 다음 복습 시점 재계산
  3. scheduledAt 업데이트

상수 요약 (빠른 참조)

┌─────────────────────────────────────────────┐
│ SM-2 상수                                    │
├─────────────────────────────────────────────┤
│ DEFAULT_EF       = 2.5  (초기 쉬움 계수)      │
│ MIN_EF           = 1.3  (최소 EF)             │
│ MAX_INTERVAL     = 60일 (최대 복습 간격)       │
│ Hard EF 패널티    = -0.3                      │
│ Easy EF 보상      = +0.15                     │
│                                              │
│ 초기 간격 (iteration=0):                       │
│   어려웠어 = 1일                               │
│   괜찮았어 = 3 × pf × if 일                   │
│   쉬웠어   = 7 × pf × if 일                   │
│                                              │
│ 이후 간격:                                     │
│   어려웠어 = 1일                               │
│   그 외    = 6 × EF^(iter-1) × pf × if 일     │
│                                              │
│ 망각곡선 보정:                                  │
│   간격 후 예상 P < 30% → 안전 간격으로 축소     │
├─────────────────────────────────────────────┤
│ PERSONA_FACTOR (SM-2 간격 배율)               │
│   상위권  = 1.3                               │
│   중위권  = 1.0                               │
│   하위권  = 0.8                               │
│   벼락치기 = 0.6                              │
│                                              │
│ INTENSITY_SM2_FACTOR (학습 강도 배율)          │
│   엄격   = 0.7                                │
│   균형   = 1.0                                │
│   여유   = 1.4                                │
├─────────────────────────────────────────────┤
│ 망각곡선 상수                                  │
├─────────────────────────────────────────────┤
│ PERSONA_LAMBDA (망각 속도, day⁻¹)             │
│   상위권  = 0.1                               │
│   중위권  = 0.2                               │
│   하위권  = 0.4                               │
│   벼락치기 = 0.6                              │
│                                              │
│ INTENSITY_THRESHOLD (복습 기준선)              │
│   엄격   = 0.70                               │
│   균형   = 0.50                               │
│   여유   = 0.35                               │
│                                              │
│ DEFAULT_K          = 4.0  (시그모이드 기울기)   │
│ DEFAULT_INITIAL_S0 = 0.3  (초기 기억 강도)     │
└─────────────────────────────────────────────┘

코드 위치

파일 역할
backend/src/forget/persona-forget.service.ts 알고리즘 엔진 (모든 수식)
backend/src/study-logs/study-logs.service.ts 문제 풀 때 → 첫 복습 스케줄 생성
backend/src/reviews/reviews.service.ts 복습 제출 → 다음 복습 스케줄 생성
backend/src/me/me.service.ts 페르소나 변경 → 기존 스케줄 재계산

Free/Pro 기능 게이트

기능 Free Pro / School
과목 등록 3개까지 무제한
일일 복습 큐 10개 무제한
마스터리 경로 차단 (403) 전체 접근
PDF 문제집 업로드 월 1회 무제한
기출 문제집 샘플만 전체

게이트 구현 위치:

파일 게이트 내용
backend/src/subjects/subjects.service.ts Free 과목 3개 제한
backend/src/reviews/reviews.service.ts Free 복습 큐 10개 슬라이스
backend/src/stats/stats.service.ts Free 마스터리 경로 차단

2022 개정 교육과정 프리셋

과목명(예: "중1수학", "고2수학")을 입력하면 해당 학년의 단원이 태그로 자동 생성된다.

과목 단원 수 예시
중1수학 9개 소인수분해, 일차방정식, 좌표평면...
중2수학 8개 일차부등식, 연립일차방정식, 피타고라스...
중3수학 7개 이차방정식, 이차함수, 삼각비...
고1수학 18개 다항식, 복소수, 이차방정식, 집합, 함수...
고2수학 17개 지수/로그, 삼각함수, 미적분, 확률통계...
고3수학 16개 급수, 삼각함수 미분, 벡터, 이차곡선...

구현: backend/src/subjects/curriculum-presets.ts