Spring Batch로 주간·월간 상품 랭킹 구현하기

찌니의 램프 🧞‍♂️·2026년 7월 23일

일간 상품 통계를 기반으로 주간·월간 랭킹을 생성했다.

단순히 집계 쿼리를 추가하는 것에서 끝나지 않고, 다음 세 가지 관점에서 설계했다.

  • Spring Batch: 실패 지점부터 재시작 가능한 Job 구성
  • Batch Processing: 대량 데이터를 안정적으로 처리하고 성능을 측정하는 방법
  • Materialized View: 집계 결과를 미리 계산하고 API에 원자적으로 공개하는 방법

이번 글에서는 전체 설계와 함께 재시작, 멱등성, Chunk 크기, 스냅샷 게시, 성능 테스트 과정까지 정리한다.


1. 문제 상황

상품 조회, 좋아요, 주문 이벤트는 Kafka를 통해 처리되고 product_metrics 테이블에 일간 단위로 누적된다.

product_metrics

metric_date
product_id
view_count
like_count
order_count

기존 Ranking API는 Redis에 저장된 일간 랭킹을 제공하고 있었다.

여기에 주간·월간 랭킹을 추가할 때 API 요청마다 product_metrics를 집계하면 다음 문제가 생긴다.

  • 동일한 기간 집계를 요청마다 반복한다.
  • 데이터 증가에 따라 응답시간이 길어진다.
  • 집계 쿼리가 MySQL의 CPU와 I/O를 많이 사용한다.
  • API 트래픽과 집계 부하가 동일한 DB 자원을 경쟁한다.
  • 집계 도중 장애가 발생하면 처음부터 다시 실행해야 할 수 있다.

주간·월간 랭킹은 일간 랭킹만큼 초 단위 실시간성이 중요하지 않다.

따라서 다음과 같이 저장소와 처리 방식을 분리했다.

기간처리 방식저장소
일간이벤트 기반 실시간 반영Redis
주간Spring Batch 사전 집계MySQL 스냅샷
월간Spring Batch 사전 집계MySQL 스냅샷

API는 이러한 저장소 차이를 외부에 노출하지 않고 동일한 응답 구조를 제공한다.


2. 전체 아키텍처

전체 데이터 흐름은 다음과 같다.

상품 조회·좋아요·주문 이벤트
              │
              ▼
            Kafka
              │
              ▼
       product_metrics
              │
              ▼
       commerce-batch
       Spring Batch Job
              │
              ▼
           staging
              │
              ▼
      주간·월간 TOP 100
              │
       ┌──────┴──────┐
       ▼             ▼
 주간 스냅샷      월간 스냅샷
       │             │
       └──────┬──────┘
              ▼
         Ranking API

일간 Ranking API ──────────▶ Redis

commerce-batch는 상시 실행되는 서버가 아니다.

Jenkins가 정해진 시각에 Docker 컨테이너를 실행하면 하나의 Job을 처리한 후 종료되는 one-shot 애플리케이션이다.


3. 랭킹 집계 기준

랭킹 점수는 다음 공식으로 계산한다.

점수 = 조회수 × 0.1
     + 좋아요 수 × 0.2
     + 주문 건수 × 1.0

부동소수점 오차를 방지하기 위해 Java에서는 BigDecimal, MySQL에서는 DECIMAL(30, 1)을 사용한다.

기간별 집계 범위는 다음과 같다.

주간 랭킹

targetDate가 포함된 주의 월요일 ~ targetDate

월간 랭킹

targetDate가 포함된 월의 1일 ~ targetDate

예를 들어 targetDate2026-05-31이면 다음 범위를 집계한다.

주간: 2026-05-25 ~ 2026-05-31
월간: 2026-05-01 ~ 2026-05-31

동점 상품의 순서가 실행마다 달라지지 않도록 다음 정렬 기준을 사용한다.

ORDER BY score DESC, product_id ASC

순위는 공동 순위가 아니라 ROW_NUMBER()를 이용해 1부터 연속해서 부여한다.


Spring Batch 설계

4. Job과 Step 구성

Job 이름은 productRankingAggregationJob으로 정의했다.

하나의 Job은 다음 Step을 순서대로 실행한다.

1. Staging 초기화 Tasklet
2. 주간 집계 Chunk Step
3. 주간 스냅샷 게시 Tasklet
4. 월간 집계 Chunk Step
5. 월간 스냅샷 게시 Tasklet

집계와 스냅샷 게시를 분리한 이유는 API에 불완전한 중간 결과를 노출하지 않기 위해서다.

주간 게시가 끝난 뒤 월간 집계에서 실패하면 재시작 시 이미 완료된 주간 Step은 건너뛰고 월간 Step부터 다시 실행할 수 있다.


5. Chunk-Oriented Processing

주간·월간 집계는 Spring Batch의 Chunk-Oriented Processing으로 구현했다.

Reader → Processor → Writer → Commit

Reader

Reader는 product_metrics를 다음 순서로 조회한다.

metric_date ASC, product_id ASC

Paging Reader가 실패 지점부터 정확하게 재시작하려면 정렬 키가 유일해야 한다.

따라서 원천 테이블의 다음 제약을 전제로 한다.

UNIQUE(metric_date, product_id)

정렬 키가 유일하지 않으면 페이지 경계가 달라져 재시작 시 데이터가 중복되거나 누락될 수 있다.

Processor

Processor는 일간 통계를 랭킹 점수로 변환한다.

score = viewCount.multiply(new BigDecimal("0.1"))
        .add(likeCount.multiply(new BigDecimal("0.2")))
        .add(orderCount.multiply(new BigDecimal("1.0")));

Writer

Writer는 상품별 점수를 staging 테이블에 누적한다.

stg_product_rank_aggregation

staging의 식별 키는 다음과 같다.

(job_instance_id, period_type, product_id)
  • job_instance_id: 서로 다른 Batch 실행을 구분한다.
  • period_type: WEEKLYMONTHLY를 구분한다.
  • product_id: 상품별 누적 결과를 구분한다.

6. staging을 사용한 이유

집계 결과를 API가 조회하는 스냅샷 테이블에 직접 저장하면 Batch 실행 중 다음 상태가 노출될 수 있다.

  • 일부 상품만 집계된 상태
  • 순위 계산이 끝나지 않은 상태
  • 기존 결과가 삭제됐지만 새 결과가 없는 상태
  • Batch 실패로 불완전한 결과만 남은 상태

이를 방지하기 위해 집계 중간 결과와 조회용 결과를 분리했다.

product_metrics
      │
      ▼
Chunk 집계
      │
      ▼
staging
      │
      ▼
TOP 100 게시
      │
      ▼
조회용 스냅샷

Chunk 처리 중 장애가 발생해도 기존 스냅샷은 변경되지 않는다.

모든 집계가 끝난 뒤 게시 Tasklet이 실행될 때만 새로운 결과가 API에 공개된다.


7. Job Parameter와 재시작 정책

Job Parameter는 다음 두 개만 사용한다.

파라미터필수JobInstance 식별역할
targetDateOO집계 기준일
rebuildSequenceXO완료 결과를 다시 생성할 때 사용

실패한 Batch를 재시작할 때

동일한 파라미터를 사용한다.

targetDate=20260531

Spring Batch는 같은 JobInstance로 판단하고 완료된 Step 다음부터 재시작한다.

완료된 결과를 다시 생성할 때

원천 데이터 정정 등으로 완료된 날짜를 다시 계산해야 한다면 rebuildSequence를 증가시킨다.

targetDate=20260531
rebuildSequence=2

이 경우 새로운 JobInstance가 생성돼 전체 집계가 다시 수행된다.

정책을 정리하면 다음과 같다.

실패 복구
→ 같은 targetDate
→ 같은 rebuildSequence

완료 결과 재생성
→ 같은 targetDate
→ 증가한 rebuildSequence

매번 새로운 JobInstance를 생성하는 run.id는 사용하지 않았다. run.id가 실행할 때마다 변경되면 Spring Batch의 재시작 의미를 활용하기 어렵기 때문이다.


8. 재시작 시 중복 합산 방지

재시작 가능한 Batch에서 가장 주의해야 할 부분은 중복 처리다.

다음과 같은 상황을 생각할 수 있다.

1. Reader가 Chunk를 읽는다.
2. Writer가 staging에 점수를 누적한다.
3. 애플리케이션 장애가 발생한다.
4. 같은 JobInstance를 재시작한다.

Writer 결과는 커밋됐지만 Reader 위치가 저장되지 않았다면 같은 Chunk가 다시 실행돼 점수가 중복 합산될 수 있다.

이를 방지하려면 다음 항목이 같은 DataSource와 트랜잭션 매니저로 묶여야 한다.

  • staging 데이터 변경
  • StepExecution 상태
  • Reader의 ExecutionContext
  • Spring Batch 메타데이터

하나의 Chunk 트랜잭션으로 처리하면 다음과 같이 동작한다.

Chunk 성공
→ staging과 Batch 상태 모두 커밋

Chunk 실패
→ staging과 Batch 상태 모두 롤백

재시작 테스트에서는 다음 항목을 검증해야 한다.

  • 완료된 Chunk가 다시 처리되지 않는가?
  • 실패한 Chunk만 다시 처리되는가?
  • 점수가 중복 합산되지 않는가?
  • 정상 실행과 재시작 실행의 checksum이 같은가?

9. 변경 가능한 원천 데이터 문제

Spring Batch가 실패 지점부터 재시작할 수 있다고 해서 원천 데이터의 시점 일관성까지 보장되는 것은 아니다.

다음 상황을 생각해 볼 수 있다.

1. 일부 날짜의 집계 완료
2. Batch 실패
3. 이미 읽은 product_metrics 수정
4. 기존 JobInstance 재시작

이 경우 staging에는 변경 전 데이터가 있고, 재시작 이후에는 변경된 원천을 읽게 된다. 하나의 결과에 서로 다른 시점의 데이터가 섞일 수 있다.

현재 설계에서는 다음 정책을 사용했다.

  • 일 마감 이후에는 집계 범위의 원천 데이터를 고정한다.
  • 실패한 JobInstance를 재시작하는 동안 원천을 변경하지 않는다.
  • 지연 이벤트는 증가한 rebuildSequence를 이용해 전체 재집계한다.

장기적으로는 다음과 같은 일 마감 체계가 필요하다.

  • Kafka consumer lag 확인
  • 일 마감 상태 테이블
  • 처리 완료 watermark
  • 이벤트 수집 시각 기반 cutoff

Spring Batch의 재시작은 처리 위치를 복구하지만 변경 가능한 원천의 정합성까지 자동으로 해결하지는 않는다.


Materialized View 설계

10. MySQL에서 Materialized View 구현하기

MySQL은 Materialized View를 기본 기능으로 제공하지 않는다.

이번 설계에서 Materialized View는 Batch가 명시적으로 갱신하는 물리 스냅샷 테이블을 의미한다.

mv_product_rank_weekly
mv_product_rank_monthly

API는 원천 통계를 다시 집계하지 않고 해당 날짜의 최대 100행만 조회한다.

이를 통해 다음 효과를 얻을 수 있다.

  • API 조회 비용을 최대 100건으로 제한한다.
  • 같은 기간 집계를 요청마다 반복하지 않는다.
  • 집계 실패 시 기존 결과를 유지한다.
  • 특정 날짜의 랭킹을 재현할 수 있다.
  • API 응답시간을 예측하기 쉬워진다.

11. 스냅샷 데이터 모델

주간·월간 스냅샷은 동일한 구조를 가진다.

컬럼타입역할
aggregation_dateDATE집계 기준일
period_start_dateDATE집계 시작일
period_end_dateDATE집계 종료일
rank_positionSMALLINT1~100 순위
product_idBIGINT상품 ID
scoreDECIMAL(30, 1)랭킹 점수
generated_atDATETIME(6)생성 시각

Primary Key는 다음과 같다.

(aggregation_date, rank_position)

같은 상품이 하나의 스냅샷에 중복으로 들어가지 않도록 Unique Key도 둔다.

(aggregation_date, product_id)

상품 테이블과의 FK는 사용하지 않았다.

상품이 나중에 삭제되거나 변경돼도 집계 당시 순위를 보존하기 위해서다. 다만 API가 현재 상품 정보만 조회한다면 삭제·비활성 상품을 어떻게 처리할지는 별도의 정책이 필요하다.


12. 스냅샷 원자적 게시

집계가 끝나면 게시 Tasklet이 staging에서 TOP 100을 계산한다.

기존 스냅샷 삭제와 새로운 스냅샷 삽입은 하나의 트랜잭션으로 실행한다.

DELETE FROM mv_product_rank_weekly
WHERE aggregation_date = :targetDate;

INSERT INTO mv_product_rank_weekly (
    aggregation_date,
    period_start_date,
    period_end_date,
    rank_position,
    product_id,
    score,
    generated_at
)
SELECT
    :targetDate,
    :periodStartDate,
    :targetDate,
    rank_position,
    product_id,
    score,
    NOW(6)
FROM (
    SELECT
        product_id,
        score,
        ROW_NUMBER() OVER (
            ORDER BY score DESC, product_id ASC
        ) AS rank_position
    FROM stg_product_rank_aggregation
    WHERE job_instance_id = :jobInstanceId
      AND period_type = 'WEEKLY'
) ranked
WHERE rank_position <= 100;

게시 트랜잭션이 성공하면 새로운 TOP 100 전체가 공개된다.

실패하면 DELETEINSERT가 함께 롤백되므로 기존 스냅샷이 유지된다.

게시 성공
→ 새로운 스냅샷 전체 노출

게시 실패
→ 기존 스냅샷 유지

따라서 API는 기존 결과 또는 새로운 결과 전체만 보게 된다. 부분적으로 생성된 스냅샷은 조회하지 않는다.

주간과 월간 게시 트랜잭션은 서로 독립적이다. 주간 게시 후 월간 게시가 실패하면 주간만 최신 상태일 수 있지만, API가 한 번에 하나의 기간만 조회한다는 점을 고려한 선택이다.


13. Ranking API

Ranking API는 기간과 날짜를 입력받는다.

GET /api/v1/rankings
    ?period=daily|weekly|monthly
    &date=yyyyMMdd
    &page=1
    &size=20

기간별 저장소와 날짜 기본값은 다음과 같다.

기간저장소날짜 생략 시
dailyRedis오늘
weeklyMySQL 주간 스냅샷전일
monthlyMySQL 월간 스냅샷전일

주간·월간은 요청한 날짜와 정확히 일치하는 스냅샷을 조회한다.

aggregation_date = 요청한 date

스냅샷이 없으면 오류 대신 빈 목록을 반환한다.

{
  "totalCount": 0,
  "items": []
}

HTTP Status는 200 OK다.

이 방식은 클라이언트 처리를 단순하게 하지만 다음 두 상태를 구분하기 어렵다는 단점이 있다.

  • 실제 집계 결과가 비어 있는 경우
  • Batch가 실패했거나 아직 실행되지 않은 경우

API에서는 동일하게 처리하더라도 운영 모니터링에서는 두 상태를 구분해야 한다.

기간에 따라 테이블 이름이 달라지더라도 사용자 입력을 SQL 식별자로 직접 연결하지 않는다.

검증된 Enum을 이용해 Repository를 선택한다.

return switch (period) {
    case DAILY -> dailyRankingRepository.find(...);
    case WEEKLY -> weeklyRankingRepository.find(...);
    case MONTHLY -> monthlyRankingRepository.find(...);
};

Batch Processing과 성능 검증

14. 처리량 계산

일평균 product_metrics 행 수를 A라고 하면 월말 최대 처리량은 다음과 같다.

주간 Reader  = 7A
월간 Reader  = 31A
전체 처리량  = 38A

일간 상품 통계가 10만 건이라면 최대 380만 건을 처리한다.

7 × 100,000 + 31 × 100,000
= 3,800,000 items

월간 범위를 한 번 읽으면서 주간·월간을 함께 계산하면 조회량을 줄일 수 있다.

하지만 이번 설계에서는 다음 이유로 주간과 월간 Step을 분리했다.

  • 기간별 실행 상태를 독립적으로 관리한다.
  • 주간 완료 후 월간 실패 시 주간 Step을 건너뛴다.
  • 기간별 성능과 장애 원인을 구분한다.
  • Reader와 Writer 구조를 단순하게 유지한다.

대신 주간 범위를 중복해서 읽는 비용을 감수한다.


15. Chunk 크기와 커밋 비용

전체 item 수가 380만 건일 때 Chunk 크기에 따른 예상 커밋 횟수는 다음과 같다.

Chunk 100
→ 약 38,000회 커밋

Chunk 1,000
→ 약 3,800회 커밋

Chunk 5,000
→ 약 760회 커밋

Chunk가 작으면 다음 비용이 증가한다.

  • 트랜잭션 커밋
  • Spring Batch 메타데이터 갱신
  • Reader 페이지 전환
  • Writer upsert
  • DB 네트워크 왕복
  • Chunk 로그

반대로 Chunk가 너무 크면 다음 부담이 커진다.

  • 메모리 사용량
  • 트랜잭션 유지 시간
  • DB 부하 집중
  • 실패 시 재처리 범위
  • Lock 유지 시간

따라서 Chunk 크기는 추측으로 정하지 않고 실제 데이터로 비교해야 한다.


16. 성능 테스트 조건

월말 최대 범위를 재현하기 위해 다음 합성 데이터를 사용했다.

기준일: 2026-05-31
기간: 31일
일간 상품 수: 100,000
원천 데이터: 3,100,000행
Batch 총 read/write: 3,800,000건
Batch 자원 제한: 2 vCPU / 2GiB

측정 항목은 다음과 같다.

  • Job과 Step 실행시간
  • 초당 처리 item 수
  • read/write/commit/rollback/skip
  • Chunk 처리시간
  • CPU와 메모리
  • MySQL 상태 변화
  • 스냅샷 행 수와 checksum
  • Batch 실행 중 API p95·p99
  • 컨테이너 종료 코드와 OOM 여부

17. Chunk 성능 비교 결과

측정 결과는 다음과 같았다.

시나리오Job 시간처리량CPU p95Peak Memory
Chunk 100362.728초10,476 items/s44.05%288.7MiB
Chunk 1,000108.500초35,023 items/s164.45%310.2MiB
Chunk 1,000 + API140.377초27,070 items/s202.44%304.3MiB

Docker CPU는 한 개 Core 사용량을 100%로 표시한다. 2 vCPU 제한에서는 200%가 최대 사용량이다.

Chunk 1,000은 Chunk 100과 비교해 다음 결과를 보였다.

Job 시간 약 68.74% 감소
처리량 약 3.20배 증가
메모리 약 21.5MiB 증가

두 실행 모두 다음 기능 조건을 만족했다.

총 read/write: 3,800,000건
rollback: 0
skip: 0
주간 스냅샷: 100행
월간 스냅샷: 100행
최종 checksum 일치

성능은 달랐지만 최종 결과의 정합성은 같았다.


18. API와 Batch 자원 경쟁

Batch가 빠르게 끝난다고 해서 성능 검증이 끝나는 것은 아니다.

API와 Batch가 같은 MySQL을 사용하면 다음 자원을 경쟁한다.

  • CPU
  • 디스크 I/O
  • Buffer Pool
  • DB Connection
  • 임시 테이블
  • 상품·브랜드 조회 쿼리

API 단독과 Batch 동시 실행 결과는 다음과 같았다.

시나리오daily p95weekly p95monthly p95
API 단독9.37ms13.21ms13.03ms
Batch 동시 실행12.96ms17.99ms18.40ms
변화율+38.36%+36.17%+41.21%

모든 API는 절대 기준인 p95 100ms를 만족했다. 오류와 dropped iteration도 발생하지 않았다.

그러나 Batch 실행 전 대비 p95 증가율 20% 미만이라는 상대 기준은 만족하지 못했다.

절대 API SLO
→ PASS

Batch 전 대비 상대 p95 증가율
→ FAIL

Chunk를 크게 하면 Batch는 빨리 끝나지만 순간적인 DB 부하가 증가할 수 있다.

따라서 운영 Chunk 크기는 다음 조건을 함께 고려해 결정해야 한다.

Batch 완료시간
+ API 응답시간
+ DB 부하
+ 메모리
+ 실패 시 재처리량

19. Jenkins를 스케줄러로 선택한 이유

Spring Batch는 Job 실행과 재시작을 담당하지만 스케줄러는 아니다.

현재 환경은 Kubernetes가 없는 단일 Docker 호스트이므로 Jenkins가 매일 01:30 KST에 Batch 컨테이너를 실행하도록 설계했다.

Jenkins
   │
   ▼
commerce-batch 컨테이너
   │
   ▼
Spring Batch Job
   │
   ▼
종료 코드 전달

역할은 다음과 같이 분리한다.

Jenkins

  • 실행 시점 관리
  • 수동 backfill
  • 파라미터 전달
  • Credentials 주입
  • Pipeline 실행 이력
  • 중복 Pipeline 실행 제한

Spring Batch

  • JobInstance 식별
  • Step 상태 관리
  • Chunk 트랜잭션
  • 실패 지점 재시작
  • 실행 메타데이터 관리

Docker

  • 동일한 실행 환경 제공
  • CPU와 메모리 제한
  • Java 프로세스 종료 코드 전달

20. 장애와 운영 고려사항

Chunk 처리 실패

Chunk 트랜잭션이 롤백되고 기존 스냅샷은 영향을 받지 않는다.

같은 Job Parameter로 재실행하면 실패 지점부터 다시 처리한다.

게시 실패

DELETE + INSERT가 함께 롤백돼 기존 스냅샷이 유지된다.

원천 데이터가 없는 경우

정상적인 빈 결과로 처리한다. 해당 기준일의 기존 스냅샷을 삭제하고 빈 결과를 게시한다.

컨테이너 강제 종료

Java 프로세스가 정상적으로 실패 상태를 기록하지 못하면 JobExecution이 STARTED로 남을 수 있다.

이 경우 다음 순서로 복구해야 한다.

1. 실제 Batch 컨테이너가 종료됐는지 확인한다.
2. 실행 중이라면 메타데이터를 변경하지 않는다.
3. 종료가 확인되면 승인된 절차로 실행 상태를 FAILED로 정리한다.
4. 같은 Job Parameter로 다시 실행한다.

abandon()은 해당 실행의 재시작을 막으므로 복구 수단으로 사용하지 않는다.

스케줄 누락

Jenkins가 중단된 기간의 실행은 자동으로 보충되지 않는다.

복구 후에는 다음 항목을 비교한다.

서울 기준 전일
스냅샷의 최신 aggregation_date
Spring Batch 메타데이터의 완료 날짜

누락된 날짜가 있다면 오래된 날짜부터 순서대로 backfill한다.


21. 현재 결과의 한계

이번 측정은 운영 환경이 아닌 로컬 합성 부하다.

다음 항목은 아직 확인이 필요하다.

  • 실제 운영 P99 원천 데이터량
  • 실제 야간 API RPS
  • 명시적인 일 마감 신호
  • Writer 실패 후 재시작 성능
  • SIGTERM·SIGKILL 복구 테스트
  • JVM GC 시간
  • 운영 환경과 동일한 MySQL Buffer Pool
  • 운영과 동일한 API·Batch 자원 격리

따라서 Chunk 1,000은 현재 조건에서의 다음 운영 후보일 뿐, 운영 최적값으로 확정할 수는 없다.

현재 내릴 수 있는 결론
→ Chunk 1,000이 Chunk 100보다 처리량이 높다.

아직 내릴 수 없는 결론
→ Chunk 1,000이 운영 환경의 최적값이다.

22. 설계하면서 배운 점

Spring Batch의 핵심은 반복문이 아니다

Spring Batch의 핵심은 많은 데이터를 반복해서 읽는 기능보다 다음 항목에 있다.

  • JobInstance 식별
  • 실행 이력
  • Chunk 트랜잭션
  • 실패 지점 재시작
  • Step 상태 관리
  • 멱등성 있는 재처리

재시작과 원천 데이터 정합성은 다른 문제다

Spring Batch는 실패한 위치부터 다시 실행할 수 있지만, 재시작 사이에 원천 데이터가 변경되면 별도의 마감 정책이 필요하다.

Materialized View는 게시 과정이 중요하다

결과를 미리 계산하는 것만으로는 부족하다. 사용자가 부분적으로 생성된 결과를 보지 않도록 staging과 원자적 게시 과정이 필요하다.

Batch 성능은 API와 함께 측정해야 한다

Batch 실행시간이 짧아도 API 응답시간을 크게 악화시키면 좋은 운영 설계라고 보기 어렵다.

측정하지 않은 값은 추측하지 않는다

성능 수치에는 데이터 규모, CPU, 메모리, DB 설정, 캐시 상태와 같은 실행 환경이 함께 기록돼야 한다.


마무리

이번 설계에서는 일간 통계를 기반으로 주간·월간 상품 랭킹을 생성하고, 이를 조회 전용 Materialized View로 제공했다.

전체 흐름을 정리하면 다음과 같다.

1. product_metrics를 Chunk 단위로 읽는다.
2. staging에 상품별 점수를 누적한다.
3. 실패하면 완료된 Chunk 다음부터 재시작한다.
4. TOP 100을 조회용 스냅샷으로 원자적으로 게시한다.
5. API는 원천 데이터 대신 스냅샷을 조회한다.
6. 완료 결과의 재생성은 rebuildSequence로 구분한다.
7. Batch 실행시간과 API 영향을 함께 측정한다.

Spring Batch를 적용하는 것 자체보다 중요했던 것은 다음 질문에 답하는 일이었다.

  • 실패하면 어디서부터 다시 시작할 것인가?
  • 재시작해도 데이터가 중복되지 않는가?
  • 집계 중간 결과가 사용자에게 노출되지 않는가?
  • 원천 데이터가 변경되면 어떻게 다시 계산할 것인가?
  • Batch 실행이 API 성능에 어떤 영향을 주는가?

Batch는 정상 실행보다 실패와 재실행 과정에서 설계 품질이 드러난다.

이번 과제를 통해 단순한 정기 집계를 넘어, 재시작 가능하고 조회 일관성을 보장하는 Batch Processing 구조를 고민할 수 있었다.

profile
조용히 성장하는 대나무같은 개발세발네발자. 오늘 하루도 행복하세요.

0개의 댓글