이전 게시물에서는 개발이유, 기능 정의, 디자인, DB 스키마에 대해 정리했다. 이제는 백엔드에서는 어떻게 데이터를 관리, 저장, 생성하는지 알아보겠다.


1. DB Backfill

스트릭 기능을 만들면서 지금까지 마주치지 못한 문제에 직면했다.

스트릭 관련 테이블을 보면 알 수 있듯이, 게시물의 상황을 주차별로 정리하여 최신화 하는걸 의도하고 있기 때문에 지금까지 작성한 게시물에 대해서도 weekly_streak, streak_status 데이터를 채워줘야하는 문제였다.

이렇게 누락된 데이터를 채워넣는 작업을 Data Backfill 작업이라고 부른다.

백필 작업을 어떻게 할지 고민하다가, Mysql에 WITH RECURSIVE CTE 문법이 있어서 이를 이용해서 데이터를 채울 수 있었다. 이때 CTE는 Common Table Expression으로 하나의 쿼리문 범위 내에서만 존재하며 여러 번 참조될 수 있는 이름이 지정된 일회성 결과 데이터이다. <br><br>

주간 통계 백필(backfill) SQL

우선 API에서는 최근 20주의 weekly_streak이 필요하다. 이때 블로그를 개설한지 20주가 되지 않았기 때문에, 오늘 날짜로부터 20주 전까지 생성하면 API조건도 만족하고 블로그 글의 모든 게시물을 테이블에 반영할 수 있다.

그리고 weekly_streak은 매주 글을 얼마나 썼는지 기록하는 역할이므로, Post테이블의 published_at 속성을 참고해서 해당 주에 몇 개의 게시물이 작성 되었는지 카운트하여 저장하도록 만들었다.

INSERT INTO weekly_streak (
    year, week_number, start_date_time, end_date_time, post_count, created_at, updated_at
)
WITH RECURSIVE weeks AS (
    SELECT 0 AS n,
           DATE_ADD(
                   DATE_SUB(DATE(NOW() - INTERVAL 6 HOUR), INTERVAL WEEKDAY(NOW() - INTERVAL 6 HOUR) DAY),
                   INTERVAL 6 HOUR
           ) AS start_dt
    UNION ALL
    SELECT n + 1,
           DATE_ADD(start_dt, INTERVAL -1 WEEK)
    FROM weeks
    WHERE n < 19
)
SELECT
    YEAR(start_dt) AS year,
    WEEK(start_dt, 1) AS week_number,
    start_dt,
    start_dt + INTERVAL 1 WEEK - INTERVAL 1 SECOND AS end_date_time,
    COUNT(p.id) AS post_count,
    NOW() - INTERVAL 6 HOUR,
    NOW() - INTERVAL 6 HOUR
FROM weeks w
         LEFT JOIN post p
                   ON p.published_at >= w.start_dt
                       AND p.published_at <  w.start_dt + INTERVAL 1 WEEK
GROUP BY year, week_number, start_dt
ORDER BY start_dt;

<br><br>

주간 통계 백필 SQL

주간 통계 백필 SQL문도 WITH RECURSIVE CTE를 사용했다. streak_status는 weekly_streak를 참고하여 데이터를 생성하도록 했다.

INSERT INTO weekly_streak (
    year, week_number, start_date_time, end_date_time, post_count, created_at, updated_at
)
WITH RECURSIVE weeks AS (
    SELECT 0 AS n,
           DATE_ADD(
                   DATE_SUB(DATE(NOW() - INTERVAL 6 HOUR), INTERVAL WEEKDAY(NOW() - INTERVAL 6 HOUR) DAY),
                   INTERVAL 6 HOUR
           ) AS start_dt
    UNION ALL
    SELECT n + 1,
           DATE_ADD(start_dt, INTERVAL -1 WEEK)
    FROM weeks
    WHERE n < 19
)
SELECT
    YEAR(start_dt) AS year,
    WEEK(start_dt, 1) AS week_number,
    start_dt,
    start_dt + INTERVAL 1 WEEK - INTERVAL 1 SECOND AS end_date_time,
    COUNT(p.id) AS post_count,
    NOW() - INTERVAL 6 HOUR,
    NOW() - INTERVAL 6 HOUR
FROM weeks w
         LEFT JOIN post p
                   ON p.published_at >= w.start_dt
                       AND p.published_at <  w.start_dt + INTERVAL 1 WEEK
GROUP BY year, week_number, start_dt
ORDER BY start_dt;

2. Spring @Scheduled를 이용한 스트릭 갱신 자동화

지금까지 비지니스 로직은 사용자의 행동(저장, 수정, 삭제 등)에 기반했다. 하지만 스트릭은 매주 갱신되어야 하므로, 사용자가 행동하지 않아도 서버에서 매주 자동으로 DB가 업데이트 되어야 한다.

이제 데이터가 어떻게 업데이트 되어야 하는지 정리해보겠다.

스케쥴링 시나리오

  1. 매주 월요일 오전 06시에 스케쥴러 실행
  2. 저번주 weekly_status 데이터와 'streak_status'의 가장 최근 데이터 조회
  3. 둘의 week_number와 날짜가 같다면(글을 작성한 상황) streak_status 최신화 종료, 다르다면 streak_status에 maxLength를 0으로 초기화하여 새 데이터 추가(저번주에 글을 작성하지 않았으므로 0으로 초기화)
  4. 이번주 'weekly_status' 데이터 생성 후 저장

<br><br>

스케쥴러 코드

    @Scheduled(cron = "0 0 6 * * MON", zone = "Asia/Seoul")
    public void runWeeklyTask() {
        log.info("주간 작업 시작");
        try {
            // 저번주(가장 최근 StreakStatus) StreakStatus 갱신
            streakService.updateStreakStatus(LocalDateTime.now());

            // 이번주 스트릭 생성
            streakService.createWeeklyStreak(LocalDateTime.now());
        } catch (Exception e) {
            log.error("주간 작업 실패", e);
        } finally {
            log.info("주간 작업 종료");
        }
    }

위의 코드를 통해서 스케쥴러 시나리오를 구현할 수 있었다.


그 외의 스트릭 관련 비지니스 로직은 간단한 CRUD라서 추가로 언급하지 않겠다. 어떻게 구현했는지 궁금하다면 개발 블로그 백엔드 깃허브 주소를 참고하면 좋을 것 같다.

감사합니다!