-
Notifications
You must be signed in to change notification settings - Fork 44
[volume-10] Spring Batch 기반 주간/월간 랭킹 시스템 구현 - 윤유탁 #394
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: yoon-yoo-tak
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| package com.loopers.domain.ranking; | ||
|
|
||
| public interface MonthlyRankingRepository extends PeriodRankingRepository { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| package com.loopers.domain.ranking; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| public interface PeriodRankingRepository { | ||
|
|
||
| /** | ||
| * 특정 기간 키(예: "2026-W15", "2026-04") 의 랭킹을 offset/limit 기반으로 조회한다. | ||
| * | ||
| * @param periodKey 주간이면 "2026-W15", 월간이면 "2026-04" | ||
| * @param offset 건너뛸 row 수 (0부터 시작) | ||
| * @param limit 가져올 row 수 | ||
| */ | ||
| List<ProductRanking> findTopN(String periodKey, long offset, int limit); | ||
|
|
||
| /** | ||
| * 해당 기간에 저장된 전체 row 수. MV 는 TOP 100 만 저장되므로 최대 100. | ||
| */ | ||
| long countByPeriod(String periodKey); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.loopers.domain.ranking; | ||
|
|
||
| public enum RankingPeriod { | ||
| DAILY, | ||
| WEEKLY, | ||
| MONTHLY | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| package com.loopers.domain.ranking; | ||
|
|
||
| public interface WeeklyRankingRepository extends PeriodRankingRepository { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| package com.loopers.infrastructure.ranking; | ||
|
|
||
| import com.loopers.domain.ranking.MonthlyRankingRepository; | ||
| import com.loopers.domain.ranking.ProductRanking; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.jdbc.core.JdbcTemplate; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class MonthlyRankingRepositoryImpl implements MonthlyRankingRepository { | ||
|
|
||
| private static final String SELECT_SQL = | ||
| "SELECT product_id, score, ranking_position " | ||
| + "FROM mv_product_rank_monthly " | ||
| + "WHERE year_month_key = ? " | ||
| + "ORDER BY ranking_position " | ||
| + "LIMIT ? OFFSET ?"; | ||
|
|
||
| private static final String COUNT_SQL = | ||
| "SELECT COUNT(*) FROM mv_product_rank_monthly WHERE year_month_key = ?"; | ||
|
|
||
| private final JdbcTemplate jdbcTemplate; | ||
|
|
||
| @Override | ||
| public List<ProductRanking> findTopN(String periodKey, long offset, int limit) { | ||
| return jdbcTemplate.query( | ||
| SELECT_SQL, | ||
| (rs, rowNum) -> new ProductRanking( | ||
| rs.getLong("product_id"), | ||
| rs.getDouble("score"), | ||
| rs.getLong("ranking_position") | ||
| ), | ||
| periodKey, limit, offset | ||
| ); | ||
| } | ||
|
|
||
| @Override | ||
| public long countByPeriod(String periodKey) { | ||
| Long count = jdbcTemplate.queryForObject(COUNT_SQL, Long.class, periodKey); | ||
| return count != null ? count : 0L; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| package com.loopers.infrastructure.ranking; | ||
|
|
||
| import com.loopers.domain.ranking.ProductRanking; | ||
| import com.loopers.domain.ranking.WeeklyRankingRepository; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.jdbc.core.JdbcTemplate; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class WeeklyRankingRepositoryImpl implements WeeklyRankingRepository { | ||
|
|
||
| private static final String SELECT_SQL = | ||
| "SELECT product_id, score, ranking_position " | ||
| + "FROM mv_product_rank_weekly " | ||
| + "WHERE year_week = ? " | ||
| + "ORDER BY ranking_position " | ||
| + "LIMIT ? OFFSET ?"; | ||
|
|
||
| private static final String COUNT_SQL = | ||
| "SELECT COUNT(*) FROM mv_product_rank_weekly WHERE year_week = ?"; | ||
|
|
||
| private final JdbcTemplate jdbcTemplate; | ||
|
|
||
| @Override | ||
| public List<ProductRanking> findTopN(String periodKey, long offset, int limit) { | ||
| return jdbcTemplate.query( | ||
| SELECT_SQL, | ||
| (rs, rowNum) -> new ProductRanking( | ||
| rs.getLong("product_id"), | ||
| rs.getDouble("score"), | ||
| rs.getLong("ranking_position") | ||
| ), | ||
| periodKey, limit, offset | ||
| ); | ||
| } | ||
|
|
||
| @Override | ||
| public long countByPeriod(String periodKey) { | ||
| Long count = jdbcTemplate.queryForObject(COUNT_SQL, Long.class, periodKey); | ||
| return count != null ? count : 0L; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| package com.loopers.infrastructure.ranking.schema; | ||
|
|
||
| import jakarta.persistence.Column; | ||
| import jakarta.persistence.Entity; | ||
| import jakarta.persistence.Id; | ||
| import jakarta.persistence.IdClass; | ||
| import jakarta.persistence.Index; | ||
| import jakarta.persistence.Table; | ||
|
|
||
| import java.io.Serializable; | ||
| import java.time.Instant; | ||
| import java.util.Objects; | ||
|
|
||
| /** | ||
| * {@code mv_product_rank_monthly} schema-only 매핑. | ||
| * | ||
| * <p>runtime 쿼리는 {@code MonthlyRankingRepositoryImpl} 이 JdbcTemplate 으로 수행한다. | ||
| * 현재 프로젝트는 {@code ddl-auto} 로 DDL 을 관리한다. {@code sql/V12} 는 Flyway 이관 시 참조 스키마. | ||
| * | ||
| * <p><b>WARNING</b>: commerce-batch 쪽 Entity, 이 Entity, 참조 SQL 세 곳에서 같은 테이블을 각자 정의한다. | ||
| * {@link WeeklyRankingMvSchema} 의 WARNING 참조. {@code year_month_key} 네이밍 근거는 | ||
| * commerce-batch 쪽 {@code MonthlyRankingMvSchema} 주석 참조. | ||
| */ | ||
| @Entity | ||
| @Table( | ||
| name = "mv_product_rank_monthly", | ||
| indexes = @Index( | ||
| name = "idx_mv_product_rank_monthly_position", | ||
| columnList = "year_month_key, ranking_position" | ||
| ) | ||
| ) | ||
| @IdClass(MonthlyRankingMvSchema.PK.class) | ||
| public class MonthlyRankingMvSchema { | ||
|
|
||
| @Id | ||
| @Column(name = "year_month_key", nullable = false, length = 7) | ||
| private String yearMonth; | ||
|
|
||
| @Id | ||
| @Column(name = "product_id", nullable = false) | ||
| private Long productId; | ||
|
|
||
| @Column(name = "ranking_position", nullable = false) | ||
| private long rankingPosition; | ||
|
|
||
| @Column(name = "score", nullable = false) | ||
| private double score; | ||
|
|
||
| @Column(name = "created_at", nullable = false) | ||
| private Instant createdAt; | ||
|
|
||
| protected MonthlyRankingMvSchema() {} | ||
|
|
||
| public static class PK implements Serializable { | ||
| private String yearMonth; | ||
| private Long productId; | ||
|
|
||
| @Override | ||
| public boolean equals(Object o) { | ||
| if (this == o) return true; | ||
| if (!(o instanceof PK pk)) return false; | ||
| return Objects.equals(yearMonth, pk.yearMonth) && Objects.equals(productId, pk.productId); | ||
| } | ||
|
|
||
| @Override | ||
| public int hashCode() { | ||
| return Objects.hash(yearMonth, productId); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,72 @@ | ||||||||||||||
| package com.loopers.infrastructure.ranking.schema; | ||||||||||||||
|
|
||||||||||||||
| import jakarta.persistence.Column; | ||||||||||||||
| import jakarta.persistence.Entity; | ||||||||||||||
| import jakarta.persistence.Id; | ||||||||||||||
| import jakarta.persistence.IdClass; | ||||||||||||||
| import jakarta.persistence.Index; | ||||||||||||||
| import jakarta.persistence.Table; | ||||||||||||||
|
|
||||||||||||||
| import java.io.Serializable; | ||||||||||||||
| import java.time.Instant; | ||||||||||||||
| import java.util.Objects; | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * {@code mv_product_rank_weekly} schema-only 매핑. | ||||||||||||||
| * | ||||||||||||||
| * <p>runtime 쿼리는 {@code WeeklyRankingRepositoryImpl} 이 JdbcTemplate 으로 수행한다. | ||||||||||||||
| * 이 클래스는 Hibernate {@code ddl-auto} 가 동일 테이블을 생성할 수 있게 구조만 선언한다. | ||||||||||||||
| * | ||||||||||||||
| * <p>현재 프로젝트는 Flyway 를 쓰지 않고 {@code ddl-auto} 로 DDL 을 관리한다. | ||||||||||||||
| * {@code sql/V10__create_mv_product_rank_weekly.sql} 은 Flyway 이관 시 참조 스키마로 준비된 파일. | ||||||||||||||
| * | ||||||||||||||
|
Comment on lines
+20
to
+22
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. DDL 관리 방식 설명이 저장소 운영 정책과 충돌한다. Line 20-22 설명은 운영자가 제안 코드- * <p>현재 프로젝트는 Flyway 를 쓰지 않고 {`@code` ddl-auto} 로 DDL 을 관리한다.
- * {`@code` sql/V10__create_mv_product_rank_weekly.sql} 은 Flyway 이관 시 참조 스키마로 준비된 파일.
+ * <p>현재 프로젝트의 스키마 변경은 별도 DB 마이그레이션 파이프라인에서 관리한다.
+ * 이 Entity 는 런타임 매핑 일관성을 위한 선언이며, 스키마 관리의 단일 기준은 외부 마이그레이션이다.Based on learnings "In the Loopers-dev-lab/loop-pack-be-l2-vol3-java project, database index creation ... is handled via an internal, separate DB migration pipeline ...". 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
| * <p><b>WARNING — 3중 정의 주의</b>: 이 Entity 와 (1) commerce-batch 의 동명 Entity, (2) 참조 SQL 이 | ||||||||||||||
| * 같은 테이블을 각자 정의한다. 컬럼/인덱스 변경 시 세 곳을 함께 수정해야 drift 를 막을 수 있다. | ||||||||||||||
| */ | ||||||||||||||
| @Entity | ||||||||||||||
| @Table( | ||||||||||||||
| name = "mv_product_rank_weekly", | ||||||||||||||
| indexes = @Index( | ||||||||||||||
| name = "idx_mv_product_rank_weekly_position", | ||||||||||||||
| columnList = "year_week, ranking_position" | ||||||||||||||
| ) | ||||||||||||||
| ) | ||||||||||||||
| @IdClass(WeeklyRankingMvSchema.PK.class) | ||||||||||||||
| public class WeeklyRankingMvSchema { | ||||||||||||||
|
|
||||||||||||||
| @Id | ||||||||||||||
| @Column(name = "year_week", nullable = false, length = 10) | ||||||||||||||
| private String yearWeek; | ||||||||||||||
|
|
||||||||||||||
| @Id | ||||||||||||||
| @Column(name = "product_id", nullable = false) | ||||||||||||||
| private Long productId; | ||||||||||||||
|
|
||||||||||||||
| @Column(name = "ranking_position", nullable = false) | ||||||||||||||
| private long rankingPosition; | ||||||||||||||
|
|
||||||||||||||
| @Column(name = "score", nullable = false) | ||||||||||||||
| private double score; | ||||||||||||||
|
|
||||||||||||||
| @Column(name = "created_at", nullable = false) | ||||||||||||||
| private Instant createdAt; | ||||||||||||||
|
|
||||||||||||||
| protected WeeklyRankingMvSchema() {} | ||||||||||||||
|
|
||||||||||||||
| public static class PK implements Serializable { | ||||||||||||||
| private String yearWeek; | ||||||||||||||
| private Long productId; | ||||||||||||||
|
|
||||||||||||||
| @Override | ||||||||||||||
| public boolean equals(Object o) { | ||||||||||||||
| if (this == o) return true; | ||||||||||||||
| if (!(o instanceof PK pk)) return false; | ||||||||||||||
| return Objects.equals(yearWeek, pk.yearWeek) && Objects.equals(productId, pk.productId); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| @Override | ||||||||||||||
| public int hashCode() { | ||||||||||||||
| return Objects.hash(yearWeek, productId); | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
스키마 관리 설명 불일치로 모듈 간 DDL 동기화 절차가 흔들릴 수 있다.
운영에서 문서 기준이 분산되면 월간 랭킹 테이블 변경 시 배치/API 간 반영 누락 가능성이 커진다.
수정안은 현재 저장소의 실제 스키마 관리 체계를 기준으로 주석을 정리하고, 참조 SQL과 양 모듈 엔티티 동시 수정 규칙을 더 명확히 적는 것이다.
추가 테스트는 월간 MV 엔티티 정의와 기준 SQL 정의 간 동등성 검증(컬럼 타입/길이/PK/인덱스) 자동화를 권장한다.
Based on learnings: In the Loopers-dev-lab/loop-pack-be-l2-vol3-java project, database index creation (including JPA Index annotations) is NOT managed by JPA ddl-auto and is handled via an internal separate DB migration pipeline.
🤖 Prompt for AI Agents