사용자 학습 패턴 분석
사용자 학습 패턴 분석
사용자의 퀴즈 풀이 패턴을 더 깊이 있게 분석하고 맞춤형 피드백을 제공하기 위해 기능 업데이트를 진행했다.
단순히 “맞았다/틀렸다”를 넘어
사용자가 어떤 유형(문법/어휘)에서 약한지, 빈칸을 채우는 데 몇 초가 걸렸는지, 어떤 오답을 적었는지 파악할 수 있다.
1. DB 설계
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CREATE TABLE `quiz_history` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`quiz_id` BIGINT NOT NULL,
`user_id` BIGINT NOT NULL,
`quiz_type` VARCHAR(255) NOT NULL COMMENT '퀴즈 유형 (문법, 어휘 등)',
`word` VARCHAR(255) COMMENT '단어',
`quiz_sentence` VARCHAR(255) COMMENT '빈칸이 있는 퀴즈 문장',
`user_answer` VARCHAR(255) COMMENT '사용자가 제출한 정답/오답 텍스트',
`correct` TINYINT(1) NOT NULL COMMENT '그 당시에 맞았는지 틀렸는지 여부 (Boolean)',
`response_time` DOUBLE NOT NULL COMMENT '문제 풀이 시간',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
CONSTRAINT `fk_quiz_history_quiz` FOREIGN KEY (`quiz_id`) REFERENCES `quiz` (`id`) ON DELETE CASCADE,
CONSTRAINT `fk_quiz_history_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE
)
quiz_history를 추가한 이유
기존에는 quiz 테이블에서 정답 여부(correct)만 상태 값으로 업데이트하는 구조였다.
처음에는 상태 업데이트 방식을 활용하려 했으나 이 방식으로는 사용자의 성장 과정을 추적할 수 없어
매 퀴즈 시도마다 데이터를 쌓는 quiz_history 테이블을 새로 만들게 되었다.
1차 시도: 10초 소요, “apples” 작성 (오답) 👉
quiz_history에 기록,quiz는correct=false2차 시도: 5초 소요, “apple” 작성 (정답) 👉
quiz_history에 기록,quiz는correct=true
과거 오답 로그가 그대로 보존되므로
“처음엔 10초 걸려 틀렸지만, 다시 풀었을 땐 5초 만에 맞혔다”는 식의 학습 성장 그래프의 데이터를 만들 수 있다.
sentence 테이블과 quiz_history에 quiz_type(문법/어휘) 컬럼을 추가해
이후에 유저가 취약한 문제 유형을 통계 낼 수 있도록 구성할 수 있다.
2. 마이페이지 화면 구성: AI 분석 기반 3대 위젯
적재된 데이터를 바탕으로 마이페이지에 피드백을 제공하는 3가지 기능을 구현했다.
- 우선순위 복습 단어 Top 3:
- 단순히 틀린 횟수만 보지 않고 (오답 횟수 * 0.6) + (오답률 * 0.4) 의 가중치 공식을 적용하여 복습이 시급한 단어 3개를 추출한다.
- AI 학습 통계 보드:
- 전체 시도 대비 어휘/문법 퀴즈 정답률, 그리고 평균 문제 풀이 시간(초)을 계산하여 보여준다.
- 최근 오답 기록 로그:
- 최근 틀린 문장, 정답 단어, 사용자가 제출한 오답, 소요 시간, 퀴즈 유형(색상 구분)을 리스트업하여 보여준다.
3. 트러블슈팅: Spring Data JPA Native Query와 DTO 매핑 이슈
복습 우선순위 Top 3를 뽑기 위해 내부에서 가중치를 계산하여 LIMIT 3으로 가져오는 로직을 작성했다.
LIMIT 구문 사용을 위해 JPQL 대신 Native Query를 선택했는데 여기서 에러가 발생했다.
문제
1
No converter found capable of converting from type [org.springframework.data.jpa.repository.query.AbstractJpaQuery$TupleConverter$TupleBackedMap] to type [com.eng.dto.QuizHistoryResponseDto]
원인
네이티브 쿼리는 MySQL 엔진이 직접 실행한다.
SELECT 절에 new com.eng.dto.QuizHistoryResponseDto(...) 같은 패키지 경로를 포함한
객체 생성 표현식을 쓰면 DB가 이를 이해하지 못해 Syntax 에러를 뱉어낸다.
*해당 객체를 사용하지 않아도 오류가 떴다.
✅ 해결: Interface 기반 Projections 사용
DTO 대신 쿼리 결과의 별칭(AS)과 매칭되는 Getter 메서드를 가진 인터페이스(Interface)를 선언하여 매핑 문제를 해결했다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 1. Interface 정의
public interface RepeatWordMapping {
String getWord();
Long getWrongCount();
Double getCorrectRate();
}
// 2. Repository 쿼리 수정
@Query(nativeQuery = true, value =
"SELECT " +
" word AS word, " +
" SUM(CASE WHEN correct = false THEN 1 ELSE 0 END) AS wrongCount, " +
" (SUM(CASE WHEN correct = true THEN 1 ELSE 0 END) / COUNT(*)) * 100 AS correctRate " +
"FROM quiz_history " +
"WHERE user_id = :userId " +
"GROUP BY word " +
"ORDER BY (SUM(CASE WHEN correct = true THEN 1 ELSE 0 END) * 0.6) " +
" + ((100 - (SUM(CASE WHEN correct = true THEN 1 ELSE 0 END) / COUNT(*)) * 100) * 0.4) DESC " +
"LIMIT 3")
List<RepeatWordMapping> getReview(Long userId);
4. 로컬 테스트 환경 세팅: 데이터 초기화와 캐시 비우기
문제 10개, 학습 10개로 제한을 둔 상태였기 때문에 반복 테스트하기 위해 기존 데이터를 삭제해야했다.
4.1 RDBMS (MySQL) 초기화
테이블 간 외래키(Foreign Key)가 얽혀있어 TRUNCATE가 제대로 실행되지 않았다.
다음과 같이 제약 조건을 임시 해제하여 초기화했다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 1. 안전을 위해 외래키 제약 조건 잠시 끄기
SET FOREIGN_KEY_CHECKS = 0;
-- 2. 각 테이블 데이터 전면 초기화
TRUNCATE TABLE quiz_history;
TRUNCATE TABLE quiz;
TRUNCATE TABLE study_history;
TRUNCATE TABLE study;
TRUNCATE TABLE sentence;
TRUNCATE TABLE meaning;
TRUNCATE TABLE word;
-- 3. 외래키 제약 조건 다시 켜기 (필수)
SET FOREIGN_KEY_CHECKS = 1;
4.2 Redis 캐시 초기화
DB를 날렸음에도 퀴즈 문제를 풀 수가 없어 Redis의 캐시데이터도 제거했다.
1
2
3
4
5
# 캐시 완전히 비우기
docker exec -it <컨테이너_이름_또는_ID> redis-cli flushall
# 비워졌는지 확인하기 (0이 나오면 정상)
docker exec -it <컨테이너_이름_또는_ID> redis-cli dbsize