Skip to content

[volume-5] 인덱스와 캐시를 사용한 성능 최적화#204

Open
pable91 wants to merge 10 commits intoLoopers-dev-lab:pable91from
pable91:feature/round5/round5
Open

[volume-5] 인덱스와 캐시를 사용한 성능 최적화#204
pable91 wants to merge 10 commits intoLoopers-dev-lab:pable91from
pable91:feature/round5/round5

Conversation

@pable91
Copy link

@pable91 pable91 commented Mar 13, 2026

📌 Summary

  • 배경:
    상품 목록 조회 API(GET /products)에서 브랜드 필터링과 정렬(좋아요순/최신순) 쿼리가 인덱스 없이 동작하여 테이블 풀스캔과
    filesort가 발생했습니다. 데이터가 증가할수록 응답 시간이 선형적으로 증가하는 구조였습니다.

  • 목표:

    1. 인덱스 최적화를 통해 주요 쿼리의 실행 시간을 1ms 이하로 단축
    2. Redis 캐싱을 통해 반복 요청에 대한 DB 부하를 최소화
    3. 쓰기 성능과 읽기 성능 간의 균형 유지
    4. 좋아요 수 비정규화 + 원자적 update (전 주차에 완료하였습니다)
  • 결과:

    • 4개 인덱스 추가로 쿼리 성능 대폭 개선
      • 브랜드+좋아요순: 22.5ms → 0.02ms (1,000배 이상 개선)
      • 전체 좋아요순: 22.2ms → 0.05ms
      • 브랜드+최신순: 0.31ms → 0.04ms
      • 전체 최신순: 22.2ms → 0.05ms
    • Redis 캐시 도입으로 캐시 히트 시 DB 조회 완전 생략
      • 상세 조회: TTL 180초
      • 목록 조회: TTL 30초
    • Master/Replica 분리로 읽기/쓰기 부하 분산
  • cache-performance-test.md와 index-performance-test.md에 성능 측정 결과를 업데이트 하였습니다.

선택지와 결정

  • 인덱스 구성: 단일 인덱스 vs 복합 인덱스
  • 고려한 대안:
    • A. 단일 인덱스만 적용: ref_brand_id, like_count, created_at 각각 단일 인덱스로 구성. 범용성은 높지만 WHERE + ORDER BY를 동시에 커버하지 못해 filesort가 발생합니다.
    • B. 복합 인덱스만 적용: (ref_brand_id, like_count) 형태로 필터와 정렬을 한 번에 커버하지만, 브랜드 필터 없는 쿼리에는
      사용 불가합니다.
    • C. 복합 + 단일 조합: 브랜드 필터가 있는 쿼리는 복합 인덱스, 없는 쿼리는 단일 인덱스로 커버합니다.
  • 최종 결정: C. 복합 + 단일 조합. EXPLAIN ANALYZE 테스트 결과, 복합 인덱스는 브랜드 필터 쿼리에서 0.02~0.04ms로 안정적인
    성능을 보였고, 단일 인덱스는 전체 정렬 쿼리에서 필수적이었습니다. 두 패턴 모두 API에서 사용되므로 조합이 필요합니다.
  • 복합 인덱스 컬럼 순서: 필터 우선 vs 정렬 우선
  • 고려한 대안:
    • A. 정렬 컬럼 우선: (like_count, ref_brand_id) 순서로 구성하면 전체 좋아요순에도 활용 가능할 것으로 기대됩니다.
    • B. 필터 컬럼 우선: (ref_brand_id, like_count) 순서로 구성하여 WHERE 절을 먼저 처리합니다.
    • 최종 결정: B. 필터 컬럼 우선. 복합 인덱스는 왼쪽 컬럼부터 순서대로 사용해야 하므로, 정렬 컬럼을 앞에 두면 필터가 있는 쿼리에서 인덱스를 활용하지 못합니다. 테스트 결과 (ref_brand_id, like_count) 순서가 브랜드 필터 쿼리에서 filesort 없이 바로 10건을 가져오는 것을 확인했습니다.
  • 구현 방식: @Cacheable vs RedisTemplate
  • 고려한 대안:
    • A. @Cacheable: 설정이 간편하지만 반환값 위주로 캐싱되어 키 설계나 TTL 상세 제어에 제약이 있습니다.
    • B. RedisTemplate 직접 사용: 기존 인프라를 활용하여 직렬화 방식과 만료 전략을 명확하게 제어할 수 있습니다.
  • 최종 결정: B. RedisTemplate. 프로젝트에 이미 구축된 Master-Replica 구성을 적극 활용하고, 복잡한 파라미터 조합에 따른 키 설계와 세밀한 TTL 조정을 위해 직접 제어 방식을 택했습니다.
  • 목록 캐시 무효화 전략
  • 고려한 대안:
    • A. 패턴 삭제 적용: 데이터 변경 시 SCAN 등을 통해 관련 키를 찾아 삭제하지만, 구현이 복잡하고 운영 부하가 따릅니다.
    • B. TTL 기반 자연 소멸: 명시적 삭제 없이 짧은 TTL만 유지하여 구현을 단순화합니다.
  • 최종 결정: B. TTL 기반 자연 소멸. 목록 검색 조건이 매우 다양하여 정확한 무효화가 어렵기에, 30초라는 짧은 주기를 통해 최신성을 유지하는 것이 운영 효율 면에서 최선이라 판단했습니다.

📋 Checklist 현황

🔖 Index
[✅] brandId 기반 검색, 좋아요 순 정렬 처리
[✅] 인덱스 적용 및 전후 성능비교

❤️ Structure
[✅] 좋아요 수 조회 및 정렬 가능 (like_count 비정규화)
[✅] 좋아요 적용/해제 시 동기화 (원자적 UPDATE)

⚡ Cache
[✅] Redis 캐시 적용 (ProductCacheStore)
[✅] TTL 설정 (상세 180초, 목록 30초)
[✅] 캐시 미스 시 정상 동작 (DB fallback)

변경 목적

제품 조회의 성능 최적화를 위해 Redis 캐시 층과 데이터베이스 인덱스를 단계적으로 도입합니다. ProductEntity에 조회 쿼리 가속을 위한 인덱스를 추가하고, ProductCacheStore를 통해 자주 조회되는 상품 정보를 메모리에 캐싱하여 DB 접근을 줄입니다.

핵심 변경점

  • ProductEntity 인덱스 추가: 4개의 복합 인덱스 추가 (좋아요순 전체/브랜드별, 최신순 전체/브랜드별)
  • ProductCacheStore 신규 구현: RedisTemplate 기반의 캐시 계층으로 상세정보는 180초, 목록은 30초 TTL 설정; Master Redis로 쓰기, Replica로 읽기 분리
  • ProductFacade 통합: 캐시 의존성 추가하고 getProduct/getProducts에 캐시 조회 로직 구현
  • 성능 테스트 데이터: 100,000개 상품을 생성하는 seed 스크립트 추가

리스크 및 주의사항

  • 결합도 증가: ProductFacade가 ProductCacheStore에 직접 의존하므로 향후 캐시 전략 변경 시 영향 범위 고려 필요
  • 데이터 신선도: TTL로 인한 단기간 staleness 가능성; 재고나 좋아요 수 같은 실시간 데이터의 정확도 영향 검토 필요
  • 장애 시 부하: Redis 장애 발생 시 모든 캐시 미스 요청이 DB로 몰릴 우려 있음
  • Master-Replica 일관성: 쓰기 후 읽기 시 Replica 동기화 지연에 따른 데이터 불일치 가능성 여부 확인 필요

테스트/검증 방법 확인사항

  • seed-products.sql 실행 후 실제 캐시 Hit율과 DB 쿼리 수 감소 메트릭 측정 필요
  • 실시간 필드(재고, 좋아요수)의 TTL 설정이 비즈니스 요구사항을 충족하는지 검증 필요
  • Redis 장애 시뮬레이션으로 DB 부하 증가 패턴 확인 필요

@coderabbitai
Copy link

coderabbitai bot commented Mar 13, 2026

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (1)
  • docs/index-performance-test.md is excluded by !**/*.md and included by **

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a5061f9b-b3cb-4ed7-83dc-3d84d61f1095

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Product 엔티티에 복합 인덱스 메타데이터가 추가되고, 대량 데이터용 MySQL 시드 스크립트가 추가되었으며, Product 읽기 경로에 Redis 기반 캐시 컴포넌트(ProductCacheStore)와 해당 캐시를 사용하는 ProductFacade 변경이 도입되었다다.

Changes

Cohort / File(s) Summary
엔티티 인덱스
apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductEntity.java
@Table에 Jakarta @Index 4개 추가(idx_product_brand_like, idx_product_like, idx_product_brand_latest, idx_product_latest). 메타데이터 변경만 포함된다.
시드 데이터
db/seed-products.sql
10개 브랜드와 100,000개 상품을 생성하는 MySQL 시드 스크립트 신규 추가. 랜덤 브랜드/가격/재고/좋아요/생성일 분포로 대량 데이터 삽입을 구성한다.
캐시 도입 — 퍼사드 변경
apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java
ProductCacheStore 주입 및 getProduct/getProducts에서 캐시 우선 조회, 미스 시 서비스 조회 후 캐시 저장 로직 추가로 읽기 경로에 캐시 계층 통합이 이루어졌다.
캐시 도입 — 저장소 컴포넌트
apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductCacheStore.java
Redis 기반 캐시 컴포넌트 신규 추가. 상세 TTL 180초, 리스트 TTL 30초, Jackson 직렬화/역직렬화, 읽기용/쓰기용 RedisTemplate 분리 사용, 오류 시 로깅 후 Optional 반환 정책을 구현한다.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant Facade as ProductFacade
    participant Cache as ProductCacheStore
    participant Service as ProductService
    participant DB as Database
    participant RedisMaster as RedisMaster

    Client->>Facade: getProduct(id)
    Facade->>Cache: getProduct(id)
    alt cache hit
        Cache-->>Facade: ProductInfo(JSON)
        Facade-->>Client: ProductInfo
    else cache miss
        Cache-->>Facade: empty
        Facade->>Service: fetchProductById(id)
        Service->>DB: SELECT product ...
        DB-->>Service: row
        Service-->>Facade: ProductInfo
        Facade->>Cache: putProduct(id, ProductInfo)
        Cache->>RedisMaster: SETEX product:detail:{id}
        Cache-->>Facade: ack
        Facade-->>Client: ProductInfo
    end
Loading

운영 및 성능 관점의 검토 사항

  • 인덱스 유효성 검증 필요
    왜 문제인지(운영 관점): 인덱스가 쿼리 패턴과 맞지 않으면 불필요한 디스크/메모리 소비와 비효율적 쿼리 플랜을 초래한다.
    수정안: 주요 조회·정렬 쿼리에 대해 EXPLAIN 실행으로 인덱스 사용 여부 확인 및 인덱스 설계 조정한다.
    추가 테스트: EXPLAIN 결과 수집, 프로덕션 유사 부하에서 벤치마크 수행으로 응답시간 및 인덱스 사용률을 검증한다.

  • DESC 인덱스 호환성 확인
    왜 문제인지(운영 관점): 일부 DB 또는 마이그레이션 도구는 내림차순 키를 다르게 처리하여 인덱스가 사용되지 않을 수 있다.
    수정안: 대상 DB에서 DESC 인덱스 동작을 확인하고 필요 시 ASC 기반 보조 인덱스 또는 쿼리 리라이트로 대체한다.
    추가 테스트: 정렬 포함 쿼리에 대해 인덱스 사용 여부(EXPLAIN)와 응답시간 비교 테스트를 수행한다.

  • 시드 스크립트 운영 리스크
    왜 문제인지(운영 관점): 대량 로드는 개발 또는 테스트 환경에서 I/O·디스크·백업 정책에 영향과 실행 지연을 유발할 수 있다.
    수정안: 로드 규모를 파라미터화하거나 샘플/대용량 모드로 분리하고 트랜잭션 청크로 삽입을 수행한다.
    추가 테스트: 소·중·대 규모에서 로드 시간, DB 리소스 사용량, 실패 복구(롤백) 동작을 측정한다.

  • 캐시 일관성 및 무효화 설계 필요
    왜 문제인지(운영 관점): 제품 수정/삭제 시 캐시에 오래된 데이터가 남아 사용자에게 잘못된 정보를 제공할 위험이 있다.
    수정안: 쓰기 경로에서 캐시 무효화(또는 write-through), 제품 변경 이벤트 발생 시 관련 키 삭제 또는 pub/sub 기반 무효화 구현을 적용한다.
    추가 테스트: 제품 업데이트/삭제 시 캐시 무효화 시나리오와 레이스 컨디션 테스트를 수행한다.

  • Redis 가용성 및 읽기·쓰기 분리 모델 점검
    왜 문제인지(운영 관점): Redis 장애 또는 복제 지연 시 캐시 의존 경로가 서비스 응답성에 영향 줄 수 있다.
    수정안: Redis 장애 시 DB 폴백 경로 검증, 연결/타임아웃 설정과 리트라이 정책 점검, 모니터링 및 알림 체계 마련을 수행한다.
    추가 테스트: Redis 장애·지연 시나리오에서 서비스 동작(캐시 미스 패턴, 타임아웃 처리)을 검증한다.

  • 직렬화 실패 처리 정책 강화
    왜 문제인지(운영 관점): JSON 역직렬화 실패를 로깅만 하고 무시하면 반복 오류 추적과 복구가 어렵다.
    수정안: 실패 카운트 메트릭과 알림을 추가하고, 반복 실패 시 해당 키 삭제 또는 TTL 단축 정책을 적용한다.
    추가 테스트: 손상된/비정상 JSON에 대한 복구 동작과 메트릭/알림 흐름을 검증한다.

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 주요 변경사항인 인덱스 추가와 캐시 도입을 정확히 반영하고 있다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description check ✅ Passed PR 설명이 템플릿 구조를 대부분 따르고 있으나, Context & Decision의 문제 정의 섹션에서 구체적 정보가 누락되었다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@pable91 pable91 changed the title Feature/round5/round5 [volume-5] 인덱스와 캐시를 사용한 성능 최적화 Mar 13, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductEntity.java (1)

16-18: 동점 정렬 구간의 페이지 드리프트를 막으려면 보조 정렬 키를 포함한 인덱스가 필요하다.

운영 관점에서 like_count 동점이 많은 데이터에서는 offset 기반 페이징이 중복/누락을 만들 수 있다. 수정안은 id DESC를 보조 정렬 키로 추가하고 인덱스도 동일 순서로 확장하는 것이다. 추가 테스트로 동일 like_count 대량 데이터에서 페이지 1/2/3 조회 시 중복/누락이 없는지 검증해야 한다.

인덱스 수정 예시 diff
 `@Table`(name = "product", indexes = {
-    `@Index`(name = "idx_product_brand_like", columnList = "ref_brand_id, like_count DESC"),
-    `@Index`(name = "idx_product_like", columnList = "like_count DESC")
+    `@Index`(name = "idx_product_brand_like_id", columnList = "ref_brand_id, like_count DESC, id DESC"),
+    `@Index`(name = "idx_product_like_id", columnList = "like_count DESC, id DESC")
 })

apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java의 정렬도 likeCount.desc(), id.desc()로 맞춰야 효과가 완성된다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductEntity.java`
around lines 16 - 18, Update the product DB indexes and repository sort to
prevent page drift on equal like_count: in ProductEntity modify the `@Table`
indexes (idx_product_brand_like and idx_product_like) to include id DESC as a
tie-breaker (e.g., columnList = "ref_brand_id, like_count DESC, id DESC" and
"like_count DESC, id DESC"), and in ProductRepositoryImpl change the sort to use
likeCount.desc(), id.desc() so query ordering matches the index; then add a test
that pages through many products with identical like_count (page 1/2/3) to
assert no duplicates or gaps.
db/seed-products.sql (1)

7-36: 시드 스크립트가 비멱등이라 재실행 시 데이터가 누적되어 성능 결과를 오염시킨다.

운영/성능 관점에서 현재 구조는 재실행할 때마다 행이 누적되어 테스트 결과 비교가 불가능해진다. 수정안은 트랜잭션과 초기화(또는 명시적 upsert 정책)로 실행 결과를 결정적으로 만드는 방식이다. 추가 테스트로 스크립트를 2회 연속 실행한 뒤 brand=10, product=100000이 유지되는지 확인해야 한다.

수정 예시 diff
+START TRANSACTION;
+
+-- 테스트 환경에서만 실행: 결정적 결과 보장을 위한 초기화
+DELETE FROM product;
+DELETE FROM brand
+WHERE name IN ('나이키','아디다스','뉴발란스','컨버스','반스','푸마','리복','언더아머','살로몬','노스페이스');
+
 -- 브랜드 10개 삽입
 INSERT INTO brand (name, description, created_at, updated_at)
 VALUES
@@
 LIMIT 100000;
+
+COMMIT;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@db/seed-products.sql` around lines 7 - 36, The seed SQL is not idempotent:
re-running the INSERTs into brand and product will keep appending rows; modify
the script to make it deterministic by wrapping operations in a transaction and
clearing or upserting existing data first (e.g., BEGIN; TRUNCATE or DELETE FROM
brand; TRUNCATE or DELETE FROM product; then run the INSERTs), or implement
explicit UPSERT logic keyed on brand.name for the brand inserts and a
deterministic key for product if needed; ensure the product generation still
uses the `@i` counter and the same SELECT ... LIMIT 100000 pattern but after
initialization so repeated runs yield exactly 10 brands and 100000 products, and
add a quick verification step at the end (SELECT COUNT(*) FROM brand; SELECT
COUNT(*) FROM product;) to confirm counts remain brand=10 and product=100000
after two consecutive executions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@db/seed-products.sql`:
- Around line 8-19: The seed assumes brand IDs 1–10 and uses a hardcoded random
ID which breaks FK if existing brand IDs differ; update the product insertion
logic in db/seed-products.sql to pick ref_brand_id from the actual brand.id set
(e.g., SELECT id FROM brand ORDER BY ... LIMIT 1 or use a subquery/ARRAY
sampling) so product.ref_brand_id always references an existing brand, and add a
post-seed validation that every product.ref_brand_id exists in brand.id to
ensure no FK failures (check in the same script using EXISTS/COUNT).
- Line 4: 현재 db/seed-products.sql에 예시 명령어 "mysql -u application -papplication
loopers < db/seed-products.sql"처럼 평문 비밀번호가 노출되어 있으니 해당 예시를 수정해 평문 비밀번호를 제거하고 안전한
사용법을 안내하세요; 예: 권장 문구로 "mysql -u application -p loopers < db/seed-products.sql"
(프롬프트에서 비밀번호 입력) 또는 환경변수 사용("MYSQL_PWD" 또는 도커/CI 비밀 관리 방법)으로 대체하고, README나 주석에 이
방식과 함께 저장소 전역에서 평문 패스워드 패턴(-p<password>)이 있는지 정적 검색을 실행하도록(예: git grep -nE
"-p[^[:space:]]") 권장하도록 문구를 추가하세요.

---

Nitpick comments:
In
`@apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductEntity.java`:
- Around line 16-18: Update the product DB indexes and repository sort to
prevent page drift on equal like_count: in ProductEntity modify the `@Table`
indexes (idx_product_brand_like and idx_product_like) to include id DESC as a
tie-breaker (e.g., columnList = "ref_brand_id, like_count DESC, id DESC" and
"like_count DESC, id DESC"), and in ProductRepositoryImpl change the sort to use
likeCount.desc(), id.desc() so query ordering matches the index; then add a test
that pages through many products with identical like_count (page 1/2/3) to
assert no duplicates or gaps.

In `@db/seed-products.sql`:
- Around line 7-36: The seed SQL is not idempotent: re-running the INSERTs into
brand and product will keep appending rows; modify the script to make it
deterministic by wrapping operations in a transaction and clearing or upserting
existing data first (e.g., BEGIN; TRUNCATE or DELETE FROM brand; TRUNCATE or
DELETE FROM product; then run the INSERTs), or implement explicit UPSERT logic
keyed on brand.name for the brand inserts and a deterministic key for product if
needed; ensure the product generation still uses the `@i` counter and the same
SELECT ... LIMIT 100000 pattern but after initialization so repeated runs yield
exactly 10 brands and 100000 products, and add a quick verification step at the
end (SELECT COUNT(*) FROM brand; SELECT COUNT(*) FROM product;) to confirm
counts remain brand=10 and product=100000 after two consecutive executions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: df643a53-259c-4b01-87f1-cda83a1c1864

📥 Commits

Reviewing files that changed from the base of the PR and between aba7a60 and 403fc3f.

⛔ Files ignored due to path filters (1)
  • docs/index-performance-test.md is excluded by !**/*.md and included by **
📒 Files selected for processing (2)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductEntity.java
  • db/seed-products.sql

-- ============================================================
-- 상품 더미 데이터 시드 스크립트 (10만 건)
-- 실행 전제: 앱을 먼저 실행해서 테이블이 생성된 상태여야 합니다.
-- 실행 방법: mysql -u application -papplication loopers < db/seed-products.sql
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

실행 예시에 비밀번호를 평문으로 노출하면 보안 운영 습관이 약화된다.

Line 4처럼 자격증명을 커맨드에 직접 넣으면 히스토리/문서 복사로 유출될 수 있다. 수정안은 비밀번호 프롬프트(-p)나 안전한 환경변수 방식을 안내하는 것이다. 추가 테스트로 저장소 전역에서 평문 패스워드 패턴(-p<password>)이 없는지 정적 검색을 권장한다.

수정 예시 diff
--- 실행 방법: mysql -u application -papplication loopers < db/seed-products.sql
+-- 실행 방법: mysql -u application -p loopers < db/seed-products.sql
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
-- 실행 방법: mysql -u application -papplication loopers < db/seed-products.sql
-- 실행 방법: mysql -u application -p loopers < db/seed-products.sql
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@db/seed-products.sql` at line 4, 현재 db/seed-products.sql에 예시 명령어 "mysql -u
application -papplication loopers < db/seed-products.sql"처럼 평문 비밀번호가 노출되어 있으니 해당
예시를 수정해 평문 비밀번호를 제거하고 안전한 사용법을 안내하세요; 예: 권장 문구로 "mysql -u application -p loopers
< db/seed-products.sql" (프롬프트에서 비밀번호 입력) 또는 환경변수 사용("MYSQL_PWD" 또는 도커/CI 비밀 관리
방법)으로 대체하고, README나 주석에 이 방식과 함께 저장소 전역에서 평문 패스워드 패턴(-p<password>)이 있는지 정적 검색을
실행하도록(예: git grep -nE "-p[^[:space:]]") 권장하도록 문구를 추가하세요.

Comment on lines +8 to +19
INSERT INTO brand (name, description, created_at, updated_at)
VALUES
('나이키', '글로벌 스포츠 브랜드', NOW(), NOW()),
('아디다스', '독일 스포츠 브랜드', NOW(), NOW()),
('뉴발란스', '미국 스포츠 브랜드', NOW(), NOW()),
('컨버스', '캐주얼 스니커즈 브랜드', NOW(), NOW()),
('반스', '스케이트 브랜드', NOW(), NOW()),
('푸마', '독일 스포츠 브랜드', NOW(), NOW()),
('리복', '영국 스포츠 브랜드', NOW(), NOW()),
('언더아머', '미국 퍼포먼스 브랜드', NOW(), NOW()),
('살로몬', '아웃도어 브랜드', NOW(), NOW()),
('노스페이스','아웃도어 브랜드', NOW(), NOW());
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

브랜드 ID를 1~10으로 가정하는 참조 방식은 FK 실패를 유발한다.

운영 관점에서 Line 27의 하드코딩 랜덤 ID는 기존 데이터가 있는 DB에서 즉시 실패하거나 잘못된 브랜드 연결을 만든다. 수정안은 실제 삽입/존재 브랜드의 id 집합에서 ref_brand_id를 선택하도록 바꾸는 방식이다. 추가 테스트로 브랜드 ID가 1부터 시작하지 않는 상태(예: 101~110)에서 스크립트를 실행해 FK 오류가 없는지와 product.ref_brand_id가 모두 brand.id에 존재하는지 검증해야 한다.

수정 예시 diff
 INSERT INTO brand (name, description, created_at, updated_at)
 VALUES
@@
     ('노스페이스','아웃도어 브랜드',        NOW(), NOW());

+CREATE TEMPORARY TABLE tmp_seed_brand_ids AS
+SELECT id
+FROM brand
+WHERE name IN (
+    '나이키','아디다스','뉴발란스','컨버스','반스',
+    '푸마','리복','언더아머','살로몬','노스페이스'
+);

 -- 상품 10만 건 삽입 (cross join으로 빠르게 생성)
 SET `@i` = 0;

 INSERT INTO product (name, ref_brand_id, price, stock, like_count, created_at, updated_at)
 SELECT
     CONCAT('상품_', `@i` := `@i` + 1),
-    FLOOR(1 + RAND() * 10),
+    (SELECT id FROM tmp_seed_brand_ids ORDER BY RAND() LIMIT 1),
     FLOOR(1 + RAND() * 100) * 1000,
@@
 LIMIT 100000;
+
+DROP TEMPORARY TABLE tmp_seed_brand_ids;

Also applies to: 24-36

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@db/seed-products.sql` around lines 8 - 19, The seed assumes brand IDs 1–10
and uses a hardcoded random ID which breaks FK if existing brand IDs differ;
update the product insertion logic in db/seed-products.sql to pick ref_brand_id
from the actual brand.id set (e.g., SELECT id FROM brand ORDER BY ... LIMIT 1 or
use a subquery/ARRAY sampling) so product.ref_brand_id always references an
existing brand, and add a post-seed validation that every product.ref_brand_id
exists in brand.id to ensure no FK failures (check in the same script using
EXISTS/COUNT).

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java (2)

74-79: 목록 캐시 미스 경로에서 브랜드 조회 수가 결과 건수에 비례한다

현재는 목록을 ProductInfo 로 변환하면서 brandService.getById() 를 상품마다 호출한다. 캐시 미스가 난 페이지가 커질수록 이 구간의 지연이 결과 건수에 비례해 커지고, BrandService 가 저장소 접근을 포함한다면 이번 PR 의 인덱스/캐시 이득을 상당 부분 상쇄할 수 있다.

수정안은 refBrandId 를 distinct 로 모아 한 번에 조회한 뒤 Map<Long, Brand> 로 조합하거나, 아예 productService.findProducts() 가 브랜드 정보까지 함께 projection 하도록 바꾸는 것이다. 추가 테스트로는 동일 브랜드가 많은 페이지와 브랜드가 다양한 페이지 각각에서 브랜드 조회 수 또는 쿼리 수가 결과 건수와 1:1 로 늘지 않는지 검증하는 성능 회귀 테스트를 추가해야 한다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java`
around lines 74 - 79, The current mapping calls brandService.getById(...) once
per product causing O(n) brand lookups; instead collect distinct
product.getRefBrandId() from productService.findProducts(...), batch-fetch all
Brands in one call (e.g., a new brandService.findByIds(Collection<Long>) or
similar), build a Map<Long, Brand>, and then convert products to ProductInfo via
ProductInfo.of(product, brandMap.get(product.getRefBrandId())); alternatively,
extend productService.findProducts(...) to return product+brand projection so no
per-item lookup is needed; also add performance/regression tests that assert
brand lookup/query count does not scale 1:1 with result count for pages with
many identical brands and pages with diverse brands.

49-56: 만료 시 동시 미스로 DB 부하가 다시 몰릴 수 있다

지금은 전형적인 cache-aside 구조라서 핫 키가 비어 있거나 TTL 이 만료되는 순간, 같은 상품이나 같은 목록에 대한 동시 요청이 모두 productServicebrandService 경로를 탄다. 성능 최적화 PR 에서 이 구간을 그대로 두면 만료 시점의 burst traffic 이 DB 로 직행해서 p95/p99 지연이 다시 튈 수 있다.

수정안은 제품 ID 와 검색 조건 단위로 single-flight 를 두거나, Redis 분산 락 또는 짧은 프로세스 로컬 락으로 최초 1건만 재계산하게 만드는 것이다. 대안으로 stale-while-revalidate 도 가능하다. 추가 테스트로는 동일 productId 와 동일 ProductSearchCommand 에 대해 동시 요청을 여러 건 보냈을 때 DB 조회가 1회로 수렴하는지, 나머지 요청은 같은 캐시 결과를 받는지 검증하는 부하성 통합 테스트를 넣는 것이 좋다.

Also applies to: 61-82

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java`
around lines 49 - 56, The cache-aside path in ProductFacade
(productCacheStore.getProduct -> productService.getById / brandService.getById
-> ProductInfo.of -> productCacheStore.putProduct) is vulnerable to
thundering-herd on TTL expiry; modify the retrieval flow to ensure only one
request recomputes and populates the cache per productId (or per
ProductSearchCommand) by implementing a single-flight/locking mechanism (e.g., a
short in-process lock keyed by productId, or a Redis distributed lock) or adopt
stale-while-revalidate so stale values are served while a single background
refresh runs, and add an integration test that issues concurrent requests for
the same productId/ProductSearchCommand and asserts DB methods
(productService.getById/brandService.getById) are called exactly once and all
requests receive the same cached response.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductCacheStore.java`:
- Around line 43-60: The Redis read/write currently only catches
JsonProcessingException; wrap the redisTemplate.opsForValue().get(key) +
objectMapper.readValue(...) in a broader try that catches DataAccessException
(and other runtime Redis connectivity exceptions) and return Optional.empty() on
those errors, and on JsonProcessingException additionally delete the bad key
(redisTemplate.delete(key)) to avoid repeated poisoned reads before returning
Optional.empty(); in putProduct(Long productId, ProductInfo productInfo) wrap
masterRedisTemplate.opsForValue().set(...) and
objectMapper.writeValueAsString(...) in a try that catches
JsonProcessingException and DataAccessException (or RuntimeException from Redis)
and only log a warning (do not rethrow) so the request can proceed; add unit
tests that mock Redis to throw on read/write to verify
ProductFacade.getProduct()/getProducts() fall back to DB results and a test
inserting corrupted JSON verifies the key is removed and treated as a cache
miss.
- Around line 21-25: 현재 ProductCacheStore에는 조회/적재만 있고 무효화 경로가 없어
stock/likeCount/신규상품 반영 지연 문제가 발생하므로 ProductCacheStore에 public
evictProduct(productId) 메서드와 목록 캐시용 브랜드/글로벌 버전을 관리하는
incrementListVersion(brandId?) API를 추가하고, ProductFacade의 쓰기 경로(주문 처리·좋아요 토글·상품
생성)에서 해당 evictProduct(...)를 호출하거나 상세 캐시는 즉시 삭제/쓰기-스루로 갱신하고 목록 캐시는 개별 키 삭제 대신
브랜드/전역 버전 키를 조합해 무효화하도록 변경하세요; 또한 통합 테스트를 추가해(캐시 예열 후) 주문 완료 시 detail/list의
stock 감소, 좋아요 변경 시 likeCount 즉시 반영, 상품 생성 시 해당 브랜드 목록에서 신상품 즉시 노출을 검증하세요.

---

Nitpick comments:
In
`@apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java`:
- Around line 74-79: The current mapping calls brandService.getById(...) once
per product causing O(n) brand lookups; instead collect distinct
product.getRefBrandId() from productService.findProducts(...), batch-fetch all
Brands in one call (e.g., a new brandService.findByIds(Collection<Long>) or
similar), build a Map<Long, Brand>, and then convert products to ProductInfo via
ProductInfo.of(product, brandMap.get(product.getRefBrandId())); alternatively,
extend productService.findProducts(...) to return product+brand projection so no
per-item lookup is needed; also add performance/regression tests that assert
brand lookup/query count does not scale 1:1 with result count for pages with
many identical brands and pages with diverse brands.
- Around line 49-56: The cache-aside path in ProductFacade
(productCacheStore.getProduct -> productService.getById / brandService.getById
-> ProductInfo.of -> productCacheStore.putProduct) is vulnerable to
thundering-herd on TTL expiry; modify the retrieval flow to ensure only one
request recomputes and populates the cache per productId (or per
ProductSearchCommand) by implementing a single-flight/locking mechanism (e.g., a
short in-process lock keyed by productId, or a Redis distributed lock) or adopt
stale-while-revalidate so stale values are served while a single background
refresh runs, and add an integration test that issues concurrent requests for
the same productId/ProductSearchCommand and asserts DB methods
(productService.getById/brandService.getById) are called exactly once and all
requests receive the same cached response.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d7d23bc5-c07b-43a4-a575-db8fd2687395

📥 Commits

Reviewing files that changed from the base of the PR and between 403fc3f and f4b00a3.

⛔ Files ignored due to path filters (1)
  • docs/cache-performance-test.md is excluded by !**/*.md and included by **
📒 Files selected for processing (2)
  • apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductCacheStore.java

Comment on lines +21 to +25
private static final String DETAIL_KEY_PREFIX = "product:detail:";
private static final String LIST_KEY_PREFIX = "product:list:";

private static final Duration DETAIL_TTL = Duration.ofSeconds(180);
private static final Duration LIST_TTL = Duration.ofSeconds(30);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

캐시 적재 경로만 있고 무효화 전략이 없다

현재 변경분에서는 ProductCacheStore 에 조회/적재 API만 있고, apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java:48-82 도 캐시 적재만 호출한다. 그런데 apps/commerce-api/src/main/java/com/loopers/application/product/ProductInfo.java:1-29 에는 stock, likeCount 같이 자주 바뀌는 값이 포함되어 있어서, 주문·좋아요·상품 생성 직후에도 detail/list 캐시가 TTL 동안 이전 값을 계속 반환할 수 있다. 운영에서는 재고 오표시나 좋아요 수 지연 반영으로 바로 사용자 혼선을 만든다.

수정안은 write path 에서 호출할 evictProduct(...) 와 목록 캐시 버전 증가 API를 먼저 추가하는 것이다. detail 캐시는 즉시 삭제 또는 write-through 갱신으로 맞추고, list 캐시는 page/sort 조합이 많으므로 개별 키 삭제보다 브랜드/전역 버전 키를 조합하는 방식이 안전하다. 추가 테스트로는 캐시 예열 후 주문 완료 시 상세/목록의 stock 이 즉시 감소하는지, 좋아요 변경 후 likeCount 가 즉시 반영되는지, 상품 생성 후 해당 브랜드 목록에 신상품이 바로 보이는지 통합 테스트를 넣어야 한다.

Also applies to: 41-97

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductCacheStore.java`
around lines 21 - 25, 현재 ProductCacheStore에는 조회/적재만 있고 무효화 경로가 없어
stock/likeCount/신규상품 반영 지연 문제가 발생하므로 ProductCacheStore에 public
evictProduct(productId) 메서드와 목록 캐시용 브랜드/글로벌 버전을 관리하는
incrementListVersion(brandId?) API를 추가하고, ProductFacade의 쓰기 경로(주문 처리·좋아요 토글·상품
생성)에서 해당 evictProduct(...)를 호출하거나 상세 캐시는 즉시 삭제/쓰기-스루로 갱신하고 목록 캐시는 개별 키 삭제 대신
브랜드/전역 버전 키를 조합해 무효화하도록 변경하세요; 또한 통합 테스트를 추가해(캐시 예열 후) 주문 완료 시 detail/list의
stock 감소, 좋아요 변경 시 likeCount 즉시 반영, 상품 생성 시 해당 브랜드 목록에서 신상품 즉시 노출을 검증하세요.

Comment on lines +43 to +60
try {
String value = redisTemplate.opsForValue().get(key);
if (value == null) {
return Optional.empty();
}
return Optional.of(objectMapper.readValue(value, ProductInfo.class));
} catch (JsonProcessingException e) {
log.warn("Failed to deserialize product cache. key={}", key, e);
return Optional.empty();
}
}

public void putProduct(Long productId, ProductInfo productInfo) {
String key = detailKey(productId);
try {
masterRedisTemplate.opsForValue().set(key, objectMapper.writeValueAsString(productInfo), DETAIL_TTL);
} catch (JsonProcessingException e) {
log.warn("Failed to serialize product cache. key={}", key, e);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Redis 장애가 상품 조회 장애로 바로 번질 수 있다

현재는 Jackson 예외만 처리하므로 opsForValue().get()/set() 에서 발생하는 timeout, connection, DataAccessException 계열 예외가 그대로 전파된다. 운영에서는 캐시가 흔들려도 DB fallback 으로 서비스가 계속되어야 하는데, 지금 구현은 읽기 실패가 miss 로 전환되지 않고, 쓰기 실패도 이미 만든 응답을 반환하지 못한 채 요청 자체를 실패시킬 수 있다.

수정안은 읽기에서 Redis 접근 예외를 캐시 miss 로 흡수하고, 쓰기에서는 경고 로그만 남긴 뒤 본 요청은 성공시키는 것이다. 역직렬화 실패 시에도 같은 손상 데이터를 계속 읽지 않도록 문제 키를 삭제하는 처리까지 함께 넣는 편이 낫다. 추가 테스트로는 Redis read/write mock 이 예외를 던질 때 ProductFacade.getProduct()ProductFacade.getProducts() 가 DB 결과를 정상 반환하는지, 손상된 JSON 이 들어 있을 때 miss 처리 후 해당 키가 제거되는지를 검증해야 한다. As per coding guidelines, **/*.java: "예외 흐름이 명확한지 점검한다".

Also applies to: 66-83

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductCacheStore.java`
around lines 43 - 60, The Redis read/write currently only catches
JsonProcessingException; wrap the redisTemplate.opsForValue().get(key) +
objectMapper.readValue(...) in a broader try that catches DataAccessException
(and other runtime Redis connectivity exceptions) and return Optional.empty() on
those errors, and on JsonProcessingException additionally delete the bad key
(redisTemplate.delete(key)) to avoid repeated poisoned reads before returning
Optional.empty(); in putProduct(Long productId, ProductInfo productInfo) wrap
masterRedisTemplate.opsForValue().set(...) and
objectMapper.writeValueAsString(...) in a try that catches
JsonProcessingException and DataAccessException (or RuntimeException from Redis)
and only log a warning (do not rethrow) so the request can proceed; add unit
tests that mock Redis to throw on read/write to verify
ProductFacade.getProduct()/getProducts() fall back to DB results and a test
inserting corrupted JSON verifies the key is removed and treated as a cache
miss.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductEntity.java (1)

16-21: PRICE_ASC 정렬 인덱스 미적용 확인

ProductRepositoryImpl.javagetOrderSpecifier 메서드를 보면 PRICE_ASC 정렬 타입도 지원하지만, 해당 정렬에 대한 인덱스는 추가되지 않았다.

의도적인 결정이라면 문제없으나, PRICE_ASC 정렬 쿼리 빈도가 높을 경우 다음 인덱스 추가를 검토해야 한다:

  • (ref_brand_id, price ASC) - 브랜드 필터링 + 가격 정렬
  • (price ASC) - 전체 상품 가격 정렬

또한 4개의 인덱스가 추가되면 INSERT/UPDATE/DELETE 성능에 영향을 줄 수 있으므로, 상품 테이블의 쓰기 빈도와 읽기 빈도를 고려하여 인덱스 개수를 조정해야 한다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductEntity.java`
around lines 16 - 21, ProductEntity's `@Table` indexes do not include indices for
PRICE_ASC used by ProductRepositoryImpl.getOrderSpecifier; update the
`@Table`(indexes = {...}) on class ProductEntity to add indexes for price sorting
such as `@Index`(name = "idx_product_brand_price_asc", columnList = "ref_brand_id,
price ASC") and `@Index`(name = "idx_product_price_asc", columnList = "price ASC")
(or remove/adjust if PRICE_ASC is intentionally rarely used), and then validate
query plans and weigh write-impact before deploying.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductEntity.java`:
- Around line 16-21: ProductEntity 클래스의 `@Table/`@Index(예: idx_product_brand_like,
idx_product_like, idx_product_brand_latest, idx_product_latest) 어노테이션은 문서화 목적일 뿐
운영환경(jpa.ddl-auto: none)에서는 인덱스가 생성되지 않으므로, Flyway 또는 Liquibase 마이그레이션 파일을 추가해
동일한 인덱스들을 생성하는 DDL을 배포에 포함시키고(예: CREATE INDEX ... ON product (...)), MySQL에서는
대용량 테이블 잠금 완화를 위해 인덱스 생성 DDL에 ALGORITHM=INPLACE, LOCK=NONE 옵션을 적용하도록 하며(또는
롤링/오프라인 윈도우 계획), 스테이징에서 실행 시간/잠금 영향 테스트 후 운영 배포 윈도우를 확보해 적용하세요.

---

Nitpick comments:
In
`@apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductEntity.java`:
- Around line 16-21: ProductEntity's `@Table` indexes do not include indices for
PRICE_ASC used by ProductRepositoryImpl.getOrderSpecifier; update the
`@Table`(indexes = {...}) on class ProductEntity to add indexes for price sorting
such as `@Index`(name = "idx_product_brand_price_asc", columnList = "ref_brand_id,
price ASC") and `@Index`(name = "idx_product_price_asc", columnList = "price ASC")
(or remove/adjust if PRICE_ASC is intentionally rarely used), and then validate
query plans and weigh write-impact before deploying.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 67252c24-a9c4-4743-a40c-b26fa3f31f5e

📥 Commits

Reviewing files that changed from the base of the PR and between f4b00a3 and f247916.

📒 Files selected for processing (1)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductEntity.java

Comment on lines +16 to +21
@Table(name = "product", indexes = {
@Index(name = "idx_product_brand_like", columnList = "ref_brand_id, like_count DESC"),
@Index(name = "idx_product_like", columnList = "like_count DESC"),
@Index(name = "idx_product_brand_latest", columnList = "ref_brand_id, created_at DESC"),
@Index(name = "idx_product_latest", columnList = "created_at DESC")
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: 마이그레이션 스크립트에서 해당 인덱스 생성 DDL 존재 여부 확인

# 1. Flyway/Liquibase 마이그레이션 파일 검색
echo "=== Searching for migration files ==="
fd -e sql -e xml -e yaml -e yml | xargs grep -l -i "idx_product" 2>/dev/null || echo "No migration files found with idx_product"

# 2. 인덱스 DDL 패턴 검색
echo ""
echo "=== Searching for CREATE INDEX statements ==="
rg -i "CREATE\s+(UNIQUE\s+)?INDEX.*idx_product" --type sql 2>/dev/null || echo "No CREATE INDEX statements found"

# 3. resources 하위 SQL 파일 확인
echo ""
echo "=== Listing SQL files in resources ==="
fd -e sql . --full-path | head -20

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 305


🏁 Script executed:

# 1. ProductEntity.java 전체 내용 확인
cat apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductEntity.java

# 2. Hibernate 버전 확인
fd -name "pom.xml" -o -name "build.gradle" | xargs grep -A 5 "hibernate" 2>/dev/null | head -30

# 3. jpa.yml 설정 확인
fd -name "jpa.yml" -o -name "jpa.yaml" | xargs cat 2>/dev/null

# 4. 마이그레이션 디렉토리 구조 확인
find . -type d -name "db" -o -name "migration" -o -name "flyway" -o -name "liquibase" 2>/dev/null | head -10

# 5. 기존 SQL 마이그레이션 파일 형식 확인
cat db/seed-products.sql | head -20

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 4073


🏁 Script executed:

# 1. jpa.yml 설정 확인
find . -type f \( -name "jpa.yml" -o -name "jpa.yaml" -o -name "application*.yml" -o -name "application*.yaml" \) | head -10

# 2. Hibernate 버전 확인 (pom.xml 또는 build.gradle)
find . -type f \( -name "pom.xml" -o -name "build.gradle" \) -exec grep -l "hibernate" {} \;

# 3. 마이그레이션 관련 의존성 확인
find . -type f -name "pom.xml" -exec grep -A 3 "flyway\|liquibase" {} +

# 4. db 디렉토리 구조 확인
find db -type f 2>/dev/null | head -20

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 318


🏁 Script executed:

# 1. jpa.yml 전체 내용 확인
cat modules/jpa/src/main/resources/jpa.yml

# 2. application.yml 확인
cat apps/commerce-api/src/main/resources/application.yml

# 3. pom.xml에서 Hibernate 및 Spring Boot 버전 확인
find . -maxdepth 3 -name "pom.xml" -type f | xargs grep -E "spring-boot-starter-data-jpa|hibernate|version" | grep -E "hibernate|version" | head -20

# 4. Flyway 의존성 확인
rg "flyway|liquibase" --type xml

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 3191


🏁 Script executed:

# 1. 루트 및 각 모듈의 pom.xml 찾기
find . -name "pom.xml" -type f | head -10

# 2. 최상단 pom.xml에서 버전 정보 확인
cat pom.xml 2>/dev/null | grep -A 100 "<properties>" | grep -E "version|hibernate|spring" | head -20

# 3. 각 모듈 pom.xml에서 JPA/Hibernate 의존성 확인
find apps commerce-api -name "pom.xml" -type f -exec cat {} \; | grep -A 3 "spring-boot-starter-data-jpa\|hibernate-core" | head -20

# 4. Flyway 관련 설정파일 및 마이그레이션 디렉토리 확인
find . -type d -name "db" -o -type d -name "migration" 2>/dev/null
find . -type f -name "*flyway*" -o -name "*liquibase*" 2>/dev/null

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 154


🏁 Script executed:

# 1. pom.xml 위치 찾기
git ls-files | grep "pom.xml"

# 2. 루트 pom.xml 확인
cat pom.xml 2>/dev/null | head -50

# 3. Hibernate 버전 확인
git ls-files | xargs grep -l "hibernate" 2>/dev/null | head -5

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 493


🏁 Script executed:

# 1. pom.xml 찾기
fd "pom.xml" 

# 2. 최상단 pom.xml 읽기
head -100 pom.xml

# 3. Hibernate 버전 정보 검색
grep -r "hibernate" pom.xml 2>/dev/null | head -10

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 151


🏁 Script executed:

# 1. 루트 디렉토리 구조 확인
ls -la | head -20

# 2. build.gradle 찾기
fd "build.gradle"

# 3. Gradle wrapper 확인
ls -la gradlew* settings.gradle* 2>/dev/null

# 4. 프로젝트 구조 전체 확인
git ls-files | grep -E "(pom|gradle|maven)" | head -20

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 2198


🏁 Script executed:

# 1. 루트 build.gradle.kts 확인
cat build.gradle.kts | head -100

# 2. modules/jpa/build.gradle.kts 확인
cat modules/jpa/build.gradle.kts

# 3. gradle.properties 버전 정보
cat gradle.properties

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 4673


🏁 Script executed:

# 1. Hibernate 버전 확인 (Spring Boot BOM에 포함)
cat modules/jpa/build.gradle.kts | grep -i "spring-boot-starter-data-jpa\|hibernate"

# 2. Spring Boot 3.4.4에 포함된 Hibernate 버전 확인
# Spring Boot 3.4.x는 일반적으로 Hibernate 6.4.x 이상 포함
# 웹 검색으로 확인 필요

# 3. 마이그레이션 스크립트 부재 재확인
find . -type f \( -name "*.sql" -o -name "*.xml" \) -exec grep -l "idx_product_brand_like\|idx_product_like\|idx_product_brand_latest\|idx_product_latest" {} \;

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 237


🌐 Web query:

Spring Boot 3.4.4 Hibernate version

💡 Result:

Spring Boot 3.4.4 (its dependency management / BOM) uses Hibernate ORM 6.6.11.Final. [1]


운영 환경에서 인덱스 자동 생성이 되지 않으므로 마이그레이션 스크립트 필수

현재 jpa.yml에서 운영 환경(dev, qa, prd)의 ddl-auto: none 설정으로 인해 @Index 어노테이션은 스키마 문서화 목적일 뿐 실제 인덱스 생성되지 않는다.

현재 코드베이스에 마이그레이션 스크립트가 존재하지 않으므로, 운영 배포 전에 다음을 준비해야 한다:

  • Flyway 또는 Liquibase를 통한 마이그레이션 스크립트 작성 또는 수동 DDL 스크립트 준비
  • 대량 데이터가 존재하는 product 테이블의 경우 인덱스 생성으로 인한 테이블 락 발생 가능성 검토 (MySQL ALGORITHM=INPLACE, LOCK=NONE 옵션 활용)
  • 운영 환경에서 인덱스 생성 소요 시간 예측 및 배포 윈도우 확보
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductEntity.java`
around lines 16 - 21, ProductEntity 클래스의 `@Table/`@Index(예:
idx_product_brand_like, idx_product_like, idx_product_brand_latest,
idx_product_latest) 어노테이션은 문서화 목적일 뿐 운영환경(jpa.ddl-auto: none)에서는 인덱스가 생성되지 않으므로,
Flyway 또는 Liquibase 마이그레이션 파일을 추가해 동일한 인덱스들을 생성하는 DDL을 배포에 포함시키고(예: CREATE INDEX
... ON product (...)), MySQL에서는 대용량 테이블 잠금 완화를 위해 인덱스 생성 DDL에 ALGORITHM=INPLACE,
LOCK=NONE 옵션을 적용하도록 하며(또는 롤링/오프라인 윈도우 계획), 스테이징에서 실행 시간/잠금 영향 테스트 후 운영 배포 윈도우를
확보해 적용하세요.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant