Skip to content

Volume 5#208

Open
katiekim17 wants to merge 62 commits intoLoopers-dev-lab:katiekim17from
katiekim17:volume-5
Open

Volume 5#208
katiekim17 wants to merge 62 commits intoLoopers-dev-lab:katiekim17from
katiekim17:volume-5

Conversation

@katiekim17
Copy link

@katiekim17 katiekim17 commented Mar 13, 2026

📌 Summary

  • 배경: 상품이 10만 건 이상으로 증가하면서 목록 조회 성능 저하, 좋아요 순 정렬 미지원, 반복 조회에 대한 캐시 부재 문제가 있었다.
  • 목표: 인덱스 최적화로 DB 조회 속도를 개선하고, 비정규화(like_count) 기반 좋아요 정렬을 구현하며, Redis 캐시로 반복 조회 부하를 줄인다.
  • 결과: EXPLAIN 기준 풀스캔 → 인덱스 레인지 스캔으로 전환, 좋아요 등록/취소 시 이벤트 기반 count 동기화 구현, 상품 목록/상세 API에 TTL 5분 캐시 적용 및 Redis 장애 시 DB 자동 폴백 처리 완료.

🧭 Context & Decision

문제 정의

  • 현재 동작/제약: 상품 목록 조회 시 brandId 필터 + 좋아요 순 정렬을 지원해야 하나, 인덱스 없이 풀스캔 발생
  • 문제(리스크): 10만 건 데이터 기준 p99 응답 시간이 수 초 수준, Redis 캐시 미적용으로 동일 쿼리 반복 실행
  • 성공 기준: EXPLAIN에서 인덱스 사용 확인, 캐시 히트 시 DB 미호출, Redis 장애 시 서비스 정상 응답

선택지와 결정

좋아요 수 관리

  • A. 비정규화(like_count): Product에 필드 추가, 이벤트로 동기화 → 선택
  • B. MaterializedView: DB 레벨 뷰 사용
  • 트레이드오프: 좋아요 이벤트 처리(AFTER_COMMIT) 지연 동안 count가 일시적으로 실제와 다를 수 있음 (Eventual Consistency)

캐시 무효화

  • A. TTL만 사용: 구현 단순하나 변경 후 최대 5분 stale
  • B. TTL + @CacheEvict: 상품 수정/비활성화 시 즉시 무효화 → 선택
  • 트레이드오프: likeCount 변경(좋아요 등록/취소)은 evict 없이 TTL 소멸에 위임 — 빈번한 evict로 캐시 효과가 없어지는 것을 방지

Redis 장애 대응

  • A. CacheErrorHandler: Spring Cache 레벨에서 예외 삼킴 → DB 폴백 → 선택 (즉시 적용 가능)
  • B. Circuit Breaker(Resilience4j): 장애 감지 + 자동 차단, 의존성 추가 필요
  • 트레이드오프: EVICT 실패 시 stale 캐시가 TTL까지 잔존할 수 있음

🏗️ Design Overview

변경 범위

  • 영향 받는 모듈/도메인: domain/product, domain/like, application/product, config
  • 신규 추가: ProductSortType, LikeEventListener, ProductDetailCacheIntegrationTest

주요 컴포넌트 책임

  • Product: likeCount 비정규화 필드 + 5개 복합 인덱스 정의
  • LikeEventListener: AFTER_COMMIT + REQUIRES_NEWlikeCount 비동기 동기화
  • ProductFacade: @Cacheable("productDetail") — 상세 캐시 적재
  • AdminProductFacade: @Caching — 수정/비활성화 시 productList(allEntries) + productDetail(단건) 동시 evict
  • CacheConfig: 캐시별 독립 serializer + CacheErrorHandler 등록 (Redis 장애 → DB 폴백)

✅ Checklist

🔖 Index

  • 상품 목록 API에서 brandId 기반 검색, 좋아요 순 정렬 등을 처리했다

    // ProductRepositoryImpl.java
    BooleanBuilder where = new BooleanBuilder();
    if (brandId != null) where.and(product.brandId.eq(brandId));
    if (statuses != null) where.and(product.status.in(statuses));
    
    OrderSpecifier<?> order = switch (sortType) {
        case LIKES_DESC -> product.likeCount.desc();
        case PRICE_ASC  -> product.price.value.asc();
        default         -> product.createdAt.desc();
    };
  • 조회 필터, 정렬 조건별 유즈케이스를 분석하여 인덱스를 적용하고 전 후 성능비교를 진행했다

    // Product.java — 유즈케이스별 복합 인덱스
    @Table(indexes = {
        @Index(name = "idx_product_status_created",       columnList = "status, created_at DESC"),
        @Index(name = "idx_product_status_like",          columnList = "status, like_count DESC"),
        @Index(name = "idx_product_brand_status_created", columnList = "brand_id, status, created_at DESC"),
        @Index(name = "idx_product_brand_status_like",    columnList = "brand_id, status, like_count DESC"),
        @Index(name = "idx_product_brand_status_price",   columnList = "brand_id, status, price ASC"),
    })

    → 성능 비교 결과: docs/performance/index-optimization-plan.md 참고

❤️ Structure

  • 상품 목록/상세 조회 시 좋아요 수를 조회 및 좋아요 순 정렬이 가능하도록 구조 개선을 진행했다

    // Product.java
    // 비정규화 카운트: 좋아요 등록/취소 시 비동기 이벤트로 갱신 (Eventual Consistency)
    @Column(name = "like_count", nullable = false)
    private long likeCount = 0;
  • 좋아요 적용/해제 진행 시 상품 좋아요 수 또한 정상적으로 동기화되도록 진행하였다

    // LikeEventListener.java
    @Async
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void handleLikeCreated(LikeCreatedEvent event) {
        productRepository.findById(event.productId()).ifPresent(product -> {
            product.incrementLikeCount();
            productRepository.save(product);
        });
    }
    // handleLikeDeleted도 동일 구조로 decrementLikeCount() 호출

⚡ Cache

  • Redis 캐시를 적용하고 TTL 또는 무효화 전략을 적용했다

    // ProductFacade.java
    @Cacheable(cacheNames = "productDetail", key = "#productId")
    public ProductDetailInfo getProductDetail(Long productId) { ... }
    
    @Cacheable(cacheNames = "productList", key = "'' + #brandId + '_' + #sort + '_' + #page + '_' + #size")
    public ProductListPage getProductList(...) { ... }
    
    // AdminProductFacade.java — 수정/비활성화 시 두 캐시 동시 무효화
    @Caching(evict = {
        @CacheEvict(cacheNames = "productList", allEntries = true),
        @CacheEvict(cacheNames = "productDetail", key = "#productId")
    })
    public AdminProductInfo updateProduct(Long productId, ...) { ... }
  • 캐시 미스 상황에서도 서비스가 정상 동작하도록 처리했다

    // CacheConfig.java — Redis 장애 시 예외를 삼키고 DB로 자동 폴백
    @Override
    public CacheErrorHandler errorHandler() {
        return new CacheErrorHandler() {
            @Override
            public void handleCacheGetError(RuntimeException e, Cache cache, Object key) {
                log.warn("[Cache] GET 실패 - cache={}, key={}, error={}", cache.getName(), key, e.getMessage());
                // 예외를 삼킴 → 캐시 미스로 처리 → 실제 메서드(DB 조회)로 폴백
            }
            // handleCachePutError, handleCacheEvictError, handleCacheClearError도 동일하게 예외 삼킴
        };
    }
    // ProductDetailCacheIntegrationTest.java — 캐시 동작 검증
    @Test
    void returnsCachedValue_evenWhenDbIsModified() {
        productFacade.getProductDetail(product.getId()); // 캐시 적재
    
        product.updateInfo(..., "변경된 이름", ...);
        productJpaRepository.save(product);              // DB만 직접 변경
    
        ProductDetailInfo result = productFacade.getProductDetail(product.getId());
        assertThat(result.name()).isEqualTo("에어맥스 90"); // 캐시된 원래 값 반환 확인
    }

🔁 Flow Diagram

🔖 상품 목록 조회 (brandId 필터 + 좋아요 순 정렬)

sequenceDiagram
  autonumber
  participant Client
  participant ProductFacade
  participant Redis
  participant ProductRepositoryImpl
  participant DB

  Client->>ProductFacade: GET /api/v1/products?brandId=1&sort=likes_desc
  ProductFacade->>Redis: GET productList::1_likes_desc_0_20
  alt 캐시 히트
    Redis-->>ProductFacade: ProductListPage
  else 캐시 미스
    ProductFacade->>ProductRepositoryImpl: findAll(brandId=1, LIKES_DESC, statuses, pageable)
    Note over ProductRepositoryImpl: BooleanBuilder(brandId) + ORDER BY like_count DESC<br/>idx_product_brand_status_like 인덱스 사용
    ProductRepositoryImpl->>DB: SELECT ... WHERE brand_id=1 ORDER BY like_count DESC
    DB-->>ProductFacade: Page<Product>
    ProductFacade->>Redis: SET productList::1_likes_desc_0_20 (TTL 5분)
  end
  ProductFacade-->>Client: ApiResponse<ProductListPage>
Loading

❤️ 좋아요 등록 → likeCount 동기화

sequenceDiagram
  autonumber
  participant Client
  participant LikeService
  participant DB_Like as DB (likes)
  participant LikeEventListener
  participant DB_Product as DB (products)

  Client->>LikeService: addLike(userId, productId)
  LikeService->>DB_Like: INSERT INTO likes
  Note over LikeService: LikeCreatedEvent 발행
  LikeService-->>Client: 201 Created
  Note over LikeEventListener: AFTER_COMMIT + @Async (커밋 후 비동기)
  LikeEventListener->>DB_Product: UPDATE products SET like_count = like_count + 1
Loading

⚡ 상품 상세 캐시 + Redis 장애 폴백

sequenceDiagram
  autonumber
  participant Client
  participant ProductFacade
  participant CacheErrorHandler
  participant Redis
  participant DB

  Client->>ProductFacade: GET /api/v1/products/{productId}
  alt Redis 정상
    ProductFacade->>Redis: GET productDetail::{productId}
    alt 캐시 히트
      Redis-->>ProductFacade: ProductDetailInfo
    else 캐시 미스
      ProductFacade->>DB: SELECT product + brand + options + images
      DB-->>ProductFacade: result
      ProductFacade->>Redis: SET productDetail::{productId} (TTL 5분)
    end
  else Redis 장애
    Redis--xCacheErrorHandler: RedisConnectionFailureException
    CacheErrorHandler-->>ProductFacade: 예외 삼킴 (warn 로그)
    ProductFacade->>DB: SELECT product + brand + options + images
    DB-->>ProductFacade: result
  end
  ProductFacade-->>Client: ApiResponse<ProductDetailInfo>
Loading

katiekim17 and others added 30 commits February 5, 2026 01:01
- 회원가입 시퀀스 다이어그램 (핵심 + 예외 플로우)
- 내 정보 조회 시퀀스 다이어그램 (헤더 인증 포함)
- 비밀번호 변경 시퀀스 다이어그램 (핵심 + 예외 플로우)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
fix : 예제 테스트 코드 오류 해결을 위한 testcontainers 버전 업
Removed the version reference for User entity in requirements.
# Conflicts:
#	docs/design/브랜드_상품/01-requirements.md
#	docs/design/좋아요/01-requirements.md
[2주차] 설계 문서 제출 - 김평숙
katiekim17 and others added 28 commits February 24, 2026 03:54
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Mar 13, 2026

Important

Review skipped

Too many files!

This PR contains 192 files, which is 42 over the limit of 150.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b310e72d-66ec-408a-99a5-1834babcac04

📥 Commits

Reviewing files that changed from the base of the PR and between ab3e7a7 and 3793869.

⛔ Files ignored due to path filters (24)
  • .claude/skills/anaylize-query/SKILL.md is excluded by !**/*.md and included by **
  • .claude/skills/requirements-analysis/SKILL.md is excluded by !**/*.md and included by **
  • CLAUDE.md is excluded by !**/*.md and included by **
  • MERMAID.md is excluded by !**/*.md and included by **
  • docs/design/01-requirements.md is excluded by !**/*.md and included by **
  • docs/design/03-class-diagram.md is excluded by !**/*.md and included by **
  • docs/design/04-erd.md is excluded by !**/*.md and included by **
  • docs/design/브랜드_상품/01-requirements.md is excluded by !**/*.md and included by **
  • docs/design/브랜드_상품/02-sequence-diagrams.md is excluded by !**/*.md and included by **
  • docs/design/어드민/브랜드_상품/01-requirements.md is excluded by !**/*.md and included by **
  • docs/design/어드민/브랜드_상품/02-sequence-diagrams.md is excluded by !**/*.md and included by **
  • docs/design/어드민/브랜드_상품/03-class-diagram.md is excluded by !**/*.md and included by **
  • docs/design/어드민/주문/01-requirements.md is excluded by !**/*.md and included by **
  • docs/design/좋아요/01-requirements.md is excluded by !**/*.md and included by **
  • docs/design/좋아요/02-sequence-diagrams.md is excluded by !**/*.md and included by **
  • docs/design/주문/01-requirements.md is excluded by !**/*.md and included by **
  • docs/design/주문/02-sequence-diagrams.md is excluded by !**/*.md and included by **
  • docs/design/주문생성/01-requirements.md is excluded by !**/*.md and included by **
  • docs/design/주문생성/02-sequence-diagrams.md is excluded by !**/*.md and included by **
  • docs/design/주문생성/03-class-diagram.md is excluded by !**/*.md and included by **
  • docs/design/주문생성/04-erd.md is excluded by !**/*.md and included by **
  • docs/design/쿠폰/requirement.md is excluded by !**/*.md and included by **
  • docs/performance/index-optimization-plan.md is excluded by !**/*.md and included by **
  • docs/performance/redis-cache-production-concerns.md is excluded by !**/*.md and included by **
📒 Files selected for processing (192)
  • .claude/settings.local.json
  • apps/commerce-api/src/main/java/com/loopers/CommerceApiApplication.java
  • apps/commerce-api/src/main/java/com/loopers/application/brand/AdminBrandFacade.java
  • apps/commerce-api/src/main/java/com/loopers/application/brand/AdminBrandInfo.java
  • apps/commerce-api/src/main/java/com/loopers/application/brand/BrandFacade.java
  • apps/commerce-api/src/main/java/com/loopers/application/brand/BrandInfo.java
  • apps/commerce-api/src/main/java/com/loopers/application/coupon/CouponFacade.java
  • apps/commerce-api/src/main/java/com/loopers/application/coupon/CouponInfo.java
  • apps/commerce-api/src/main/java/com/loopers/application/coupon/UserCouponInfo.java
  • apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java
  • apps/commerce-api/src/main/java/com/loopers/application/like/LikeListItem.java
  • apps/commerce-api/src/main/java/com/loopers/application/order/OrderDetailInfo.java
  • apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java
  • apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java
  • apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemInfo.java
  • apps/commerce-api/src/main/java/com/loopers/application/product/AdminProductFacade.java
  • apps/commerce-api/src/main/java/com/loopers/application/product/AdminProductInfo.java
  • apps/commerce-api/src/main/java/com/loopers/application/product/OrderFacade.java
  • apps/commerce-api/src/main/java/com/loopers/application/product/ProductDetailInfo.java
  • apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java
  • apps/commerce-api/src/main/java/com/loopers/application/product/ProductHistoryInfo.java
  • apps/commerce-api/src/main/java/com/loopers/application/product/ProductListInfo.java
  • apps/commerce-api/src/main/java/com/loopers/application/product/ProductListPage.java
  • apps/commerce-api/src/main/java/com/loopers/application/users/UserFacade.java
  • apps/commerce-api/src/main/java/com/loopers/application/users/UserInfo.java
  • apps/commerce-api/src/main/java/com/loopers/config/AsyncConfig.java
  • apps/commerce-api/src/main/java/com/loopers/config/CacheConfig.java
  • apps/commerce-api/src/main/java/com/loopers/config/SecurityConfig.java
  • apps/commerce-api/src/main/java/com/loopers/config/WebMvcConfig.java
  • apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java
  • apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandDeactivatedEvent.java
  • apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandEventListener.java
  • apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandRepository.java
  • apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandStatus.java
  • apps/commerce-api/src/main/java/com/loopers/domain/common/Money.java
  • apps/commerce-api/src/main/java/com/loopers/domain/common/Quantity.java
  • apps/commerce-api/src/main/java/com/loopers/domain/coupon/Coupon.java
  • apps/commerce-api/src/main/java/com/loopers/domain/coupon/CouponDiscount.java
  • apps/commerce-api/src/main/java/com/loopers/domain/coupon/CouponRepository.java
  • apps/commerce-api/src/main/java/com/loopers/domain/coupon/CouponService.java
  • apps/commerce-api/src/main/java/com/loopers/domain/coupon/CouponType.java
  • apps/commerce-api/src/main/java/com/loopers/domain/coupon/UserCoupon.java
  • apps/commerce-api/src/main/java/com/loopers/domain/coupon/UserCouponRepository.java
  • apps/commerce-api/src/main/java/com/loopers/domain/coupon/UserCouponService.java
  • apps/commerce-api/src/main/java/com/loopers/domain/coupon/UserCouponStatus.java
  • apps/commerce-api/src/main/java/com/loopers/domain/like/Like.java
  • apps/commerce-api/src/main/java/com/loopers/domain/like/LikeCreatedEvent.java
  • apps/commerce-api/src/main/java/com/loopers/domain/like/LikeDeletedEvent.java
  • apps/commerce-api/src/main/java/com/loopers/domain/like/LikeEventListener.java
  • apps/commerce-api/src/main/java/com/loopers/domain/like/LikeRepository.java
  • apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java
  • apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java
  • apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java
  • apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItemRepository.java
  • apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItemStatus.java
  • apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java
  • apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java
  • apps/commerce-api/src/main/java/com/loopers/domain/order/OrderStatus.java
  • apps/commerce-api/src/main/java/com/loopers/domain/order/ProductSnapshot.java
  • apps/commerce-api/src/main/java/com/loopers/domain/order/StockDeductionService.java
  • apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java
  • apps/commerce-api/src/main/java/com/loopers/domain/product/ProductHistory.java
  • apps/commerce-api/src/main/java/com/loopers/domain/product/ProductHistoryRepository.java
  • apps/commerce-api/src/main/java/com/loopers/domain/product/ProductImage.java
  • apps/commerce-api/src/main/java/com/loopers/domain/product/ProductImageRepository.java
  • apps/commerce-api/src/main/java/com/loopers/domain/product/ProductImageService.java
  • apps/commerce-api/src/main/java/com/loopers/domain/product/ProductOption.java
  • apps/commerce-api/src/main/java/com/loopers/domain/product/ProductOptionRepository.java
  • apps/commerce-api/src/main/java/com/loopers/domain/product/ProductOptionService.java
  • apps/commerce-api/src/main/java/com/loopers/domain/product/ProductOptionStatus.java
  • apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java
  • apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java
  • apps/commerce-api/src/main/java/com/loopers/domain/product/ProductSortType.java
  • apps/commerce-api/src/main/java/com/loopers/domain/product/ProductStatus.java
  • apps/commerce-api/src/main/java/com/loopers/domain/stock/Stock.java
  • apps/commerce-api/src/main/java/com/loopers/domain/stock/StockRepository.java
  • apps/commerce-api/src/main/java/com/loopers/domain/users/UserRepository.java
  • apps/commerce-api/src/main/java/com/loopers/domain/users/UserService.java
  • apps/commerce-api/src/main/java/com/loopers/domain/users/Users.java
  • apps/commerce-api/src/main/java/com/loopers/domain/users/vo/Email.java
  • apps/commerce-api/src/main/java/com/loopers/domain/users/vo/EncryptedPassword.java
  • apps/commerce-api/src/main/java/com/loopers/domain/users/vo/LoginId.java
  • apps/commerce-api/src/main/java/com/loopers/domain/users/vo/RawPassword.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandEntity.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandRepositoryImpl.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/coupon/CouponJpaRepository.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/coupon/CouponRepositoryImpl.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/coupon/UserCouponJpaRepository.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/coupon/UserCouponRepositoryImpl.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeJpaRepository.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderEntity.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderItemEntity.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderItemJpaRepository.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderItemRepositoryImpl.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductHistoryJpaRepository.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductHistoryRepositoryImpl.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductImageEntity.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductImageJpaRepository.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductImageRepositoryImpl.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductOptionEntity.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductOptionJpaRepository.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductOptionRepositoryImpl.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/stock/StockJpaRepository.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/stock/StockRepositoryImpl.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/users/UserJpaRepository.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/users/UserRepositoryImpl.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/admin/AdminAuthInterceptor.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/admin/AdminBrandV1Dto.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/admin/AdminController.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/admin/AdminCouponApiSpec.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/admin/AdminCouponController.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/admin/AdminCouponV1Dto.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/admin/AdminProductV1Dto.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/admin/AdminV1ApiSpec.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/brand/BrandController.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/brand/BrandV1ApiSpec.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/brand/BrandV1Dto.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/coupon/UserCouponApiSpec.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/coupon/UserCouponController.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/coupon/UserCouponV1Dto.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeController.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeV1ApiSpec.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeV1Dto.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderController.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1ApiSpec.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Dto.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Dto.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductsV1ApiSpec.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductsV1Controller.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/users/UserV1ApiSpec.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/users/UserV1Controller.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/users/UserV1Dto.java
  • apps/commerce-api/src/main/java/com/loopers/support/error/ErrorType.java
  • apps/commerce-api/src/main/resources/application.yml
  • apps/commerce-api/src/test/java/com/loopers/application/product/ProductDetailCacheIntegrationTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/common/MoneyTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/common/QuantityTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/coupon/CouponServiceIntegrationTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/coupon/CouponTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/coupon/UserCouponServiceConcurrencyTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/coupon/UserCouponServiceIntegrationTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/coupon/UserCouponTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceConcurrencyTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/order/OrderItemTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/order/ProductSnapshotTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceIntegrationTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/product/ProductTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/stock/StockDeductionServiceConcurrencyTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/stock/StockDeductionServiceIntegrationTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/stock/StockTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/users/UsersServiceIntegrationTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/users/UsersTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/users/vo/EmailTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/users/vo/LoginIdTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/users/vo/RawPasswordTest.java
  • apps/commerce-api/src/test/java/com/loopers/interfaces/api/AdminCouponV1ApiE2ETest.java
  • apps/commerce-api/src/test/java/com/loopers/interfaces/api/AdminV1ApiE2ETest.java
  • apps/commerce-api/src/test/java/com/loopers/interfaces/api/BrandV1ApiE2ETest.java
  • apps/commerce-api/src/test/java/com/loopers/interfaces/api/LikeV1ApiE2ETest.java
  • apps/commerce-api/src/test/java/com/loopers/interfaces/api/OrderV1ApiE2ETest.java
  • apps/commerce-api/src/test/java/com/loopers/interfaces/api/ProductsV1ApiE2ETest.java
  • apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserCouponV1ApiE2ETest.java
  • apps/commerce-api/src/test/java/com/loopers/interfaces/api/UsersV1ApiE2ETest.java
  • build.gradle.kts
  • docker/infra-compose.yml
  • docker/load-test-compose.yml
  • docs/performance/explain-queries.sql
  • docs/performance/index-migration.sql
  • docs/performance/product-after.txt
  • docs/performance/실행순서
  • docs/prom/brand
  • docs/prom/brand_result
  • docs/scripts/fixture/product_performance_100k.sql
  • gradle.properties
  • http/commerce-api/admin-brand-v1.http
  • http/commerce-api/admin-product-v1.http
  • http/commerce-api/brand-v1.http
  • http/commerce-api/cache-problem-demo.http
  • http/commerce-api/like-v1.http
  • http/commerce-api/order-v1.http
  • http/commerce-api/products-v1.http
  • k6/product-list.js
  • modules/jpa/src/main/resources/jpa.yml

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
✨ 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.

Tip

CodeRabbit can use TruffleHog to scan for secrets in your code with verification capabilities.

Add a TruffleHog config file (e.g. trufflehog-config.yml, trufflehog.yml) to your project to customize detectors and scanning behavior. The tool runs only when a config file is present.

- ProductFacade.getProductDetail()에 @Cacheable("productDetail") 적용
- AdminProductFacade 수정/비활성화 시 @caching으로 productList + productDetail 동시 evict
- CacheConfig: 캐시별 독립 serializer(productList/productDetail) + CacheErrorHandler 등록
- CacheErrorHandler: Redis 연결 실패 시 예외를 삼키고 DB 조회로 자동 폴백
- ProductDetailCacheIntegrationTest: 캐시 저장/히트/무효화 5개 케이스 검증
- docs: productDetail 캐시 설계 및 무효화 전략 문서화

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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.

3 participants