이번 프로젝트에서는 개인별 추천 도서 풀에서 매주 월요일마다 1권을 확정하여 노출하는 기능을 설계하고 구현했다.
주요 목표는 안정성 확보와 운영 편의성이며, 이를 위해 DB 구조를 개선하고 스케줄러 쿼리를 작성했으며, 필요 없는 컬럼(reason)을 제거했다.
user_recommended_book
: ML이 생성한 추천 도서를 저장하는 테이블.

변경사항: reason 컬럼 삭제
user_weekly_featured_book
: 월요일마다 확정된 1권의 추천 도서를 저장하는 테이블.

MySQL 8+ (윈도 함수 사용)
SET @week_start := DATE_SUB(CURDATE(), INTERVAL (WEEKDAY(CURDATE())) DAY);
INSERT INTO user_weekly_featured_book (user_id, book_id, week_start)
SELECT user_id, book_id, @week_start
FROM (
SELECT
ur.user_id,
ur.book_id,
ROW_NUMBER() OVER (
PARTITION BY ur.user_id
ORDER BY ur.recommended_at DESC, ur.recommend_id DESC
) AS rn
FROM user_recommended_book ur
WHERE NOT EXISTS (
SELECT 1
FROM user_book_read r
WHERE r.user_id = ur.user_id
AND r.book_id = ur.book_id
)
) t
WHERE t.rn = 1
ON DUPLICATE KEY UPDATE book_id = VALUES(book_id);
-- 추천 풀 정렬 가속
CREATE INDEX idx_urb_user_at_id
ON user_recommended_book (user_id, recommended_at DESC, recommend_id DESC);
-- 읽은 책 제외 조건 가속
CREATE INDEX idx_ubr_user_book
ON user_book_read (user_id, book_id);
idx_urb_user_at_id: 추천 풀에서 유저별 최신 도서를 뽑을 때 정렬 비용을 줄여 Top-1 조회 속도를 크게 향상.
idx_ubr_user_book: 이미 읽은 책 제외 조건을 인덱스로 처리해 존재 여부 확인 속도를 최적화.
ALTER TABLE user_recommended_book DROP COLUMN reason;
@Scheduled(cron = "0 0 9 ? * MON", zone = "Asia/Seoul")
@Transactional
public void publishWeeklyFeatured() {
LocalDate weekStart = thisMondayKST();
var userIds = recRepo.findDistinctUserIds();
var top1 = PageRequest.of(0, 1);
for (Long userId : userIds) {
var candidate = recRepo.findTopCandidateForWeekly(userId, top1)
.stream().findFirst();
if (candidate.isEmpty()) continue;
Integer bookId = candidate.get().getBook().getBookId();
jdbc.update("""
INSERT INTO user_weekly_featured_book (user_id, book_id, week_start)
VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE book_id = VALUES(book_id)
""", userId, bookId, weekStart);
}
}
ALTER TABLE user_recommended_book ADD COLUMN score DECIMAL(6,3) NULL;
ORDER BY score DESC, recommended_at DESC, recommend_id DESC
이 부분은 나중에 ML에서 점수까지 반환 가능하다고 하면 해볼 예정이다.
지금 만든 쿼리 자체는 DB에서 즉시 실행하는 배치 SQL이라, “월요일 몇 시”라는 시간은 아직 정해져 있지 않다. 그래서 코드를 통해서 월요일 아침 09:00에 되도록 스케줄링 확정 코드를 구현했다. 이제는 ML 연동을 해야한다. ML 연동은 다음 글에!