Skip to content

[volume-5] 상품 목록 브랜드 필터링 및 상품 Redis 캐시 적용#206

Merged
letter333 merged 144 commits intoLoopers-dev-lab:letter333from
letter333:WEEK5
Mar 14, 2026
Merged

[volume-5] 상품 목록 브랜드 필터링 및 상품 Redis 캐시 적용#206
letter333 merged 144 commits intoLoopers-dev-lab:letter333from
letter333:WEEK5

Conversation

@letter333
Copy link

📌 Summary

  • 배경: 상품 목록 조회 시 브랜드별 필터링 요구사항과, 상품 상세 조회의 DB 부하 감소 필요
  • 목표: 브랜드 ID 기반 필터링 기능 추가 및 상품 상세 조회에 Cache-Aside 패턴의 Redis 캐시 적용
  • 결과: 상품 목록 조회 시 brandId 파라미터로 필터링 가능, 상품 상세 조회 시 Redis 캐시(TTL 10분)로 DB 부하 감소

🧭 Context & Decision

문제 정의

  • 현재 동작/제약: 상품 목록 조회 시 카테고리/키워드 필터만 지원, 상품 상세 조회는 매 요청마다 DB 조회
  • 문제(또는 리스크): 브랜드별 필터링 불가, 상품 상세 조회 빈도 높을 시 DB 부하 증가
  • 성공 기준(완료 정의): brandId 필터 정상 동작, 캐시 적중 시 DB 조회 생략, 데이터 변경 시 캐시 무효화

선택지와 결정

  • 고려한 대안:
    • A: Spring Cache 추상화 (@Cacheable) 사용
    • B: Redis 직접 접근 (Cache-Aside 패턴 수동 구현)
  • 최종 결정: B — Repository 인터페이스 기반 수동 Cache-Aside 구현
  • 트레이드오프: 코드가 다소 많아지지만 캐시 정책(TTL, eviction)을 세밀하게 제어 가능
  • 추후 개선 여지: 캐시 워밍업, 캐시 스탬피드 방지 (분산 락)

🏗️ Design Overview

변경 범위

  • 영향 받는 모듈/도메인: commerce-api (product, like, order)
  • 신규 추가: ProductDetailCacheRepository (인터페이스), ProductDetailCacheRepositoryImpl (Redis 구현체)
  • 제거/대체: 없음

주요 컴포넌트 책임

  • ProductDetailCacheRepository: 상품 상세 캐시 조회/저장/삭제 인터페이스 (application 계층)
  • ProductDetailCacheRepositoryImpl: Redis Master-Replica 기반 캐시 구현 (infrastructure 계층, 읽기: Replica, 쓰기: Master)
  • ProductFacade: Cache-Aside 로직 오케스트레이션 — 캐시 조회 → miss 시 DB 조회 → 캐시 저장
  • LikeFacade / OrderFacade: 좋아요 토글, 주문/취소 시 해당 상품 캐시 evict

🔁 Flow Diagram

Main Flow — 상품 상세 조회 (Cache-Aside)

sequenceDiagram
  autonumber
  participant Client
  participant ProductFacade
  participant CacheRepo
  participant ProductService
  participant DB

  Client->>ProductFacade: getProduct(productId)
  ProductFacade->>CacheRepo: get(productId)
  alt Cache Hit
    CacheRepo-->>ProductFacade: ProductDetailInfo
  else Cache Miss
    CacheRepo-->>ProductFacade: empty
    ProductFacade->>ProductService: getActiveProduct(productId)
    ProductService->>DB: SELECT
    DB-->>ProductService: Product
    ProductService-->>ProductFacade: Product
    ProductFacade->>CacheRepo: put(productId, info)
  end
  ProductFacade-->>Client: ProductDetailInfo
Loading

Main Flow — 브랜드 ID 필터링

sequenceDiagram
  autonumber
  participant Client
  participant Controller
  participant ProductFacade
  participant ProductService
  participant DB

  Client->>Controller: GET /api/v1/products?brandId=1
  Controller->>ProductFacade: getProducts(categoryId, brandId, keyword, sort, pageable)
  ProductFacade->>ProductService: getProducts(...)
  ProductService->>DB: SELECT WHERE brand_id = :brandId
  DB-->>ProductService: Page<Product>
  ProductService-->>ProductFacade: Page<Product>
  ProductFacade-->>Controller: Page<ProductInfo>
  Controller-->>Client: ApiResponse<Page<ProductResponse>>
Loading

letter333 and others added 30 commits February 1, 2026 20:43
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- member-erd.md: 회원 테이블 ERD 설계
- member-signup-design.md: 시퀀스/클래스 다이어그램, 패키지 구조
- CLAUDE.md: 개발 규칙 및 문서 작성 가이드 반영

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Member 도메인 객체 구현 (순수 Java, JPA 어노테이션 없음)
- 필드 검증: loginId, password, name, birthday, email
- 비밀번호 규칙: 8~16자, 영문+숫자+특수문자, 생년월일 포함 불가
- encryptPassword()로 암호화된 비밀번호 교체 지원

test: add MemberTest with 14 unit test cases

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
… entity

Member 도메인의 Infrastructure 레이어 구현:
- MemberEntity (JPA 영속성 엔티티, Domain↔Entity 변환)
- MemberRepository 인터페이스 (도메인 레이어)
- MemberJpaRepository (Spring Data JPA)
- MemberRepositoryImpl (Repository 구현체)
- MemberEntityTest, MemberRepositoryImplIntegrationTest
- spring-security-crypto 의존성 추가
- docker-java.properties (Docker Engine 29 TestContainers 호환)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- MemberService.signUp(): 중복 검사 → 도메인 생성 → 비밀번호 암호화 → 저장
- PasswordEncoderConfig: BCryptPasswordEncoder Bean 등록
- MemberServiceTest: 정상 가입, loginId 중복, email 중복 통합 테스트

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- MemberInfo: Domain → 응답 변환 record (password, birthday 제외)
- MemberFacade: MemberService 위임 및 MemberInfo 변환
- MemberInfoTest, MemberFacadeTest 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- POST /api/v1/members → 201 Created
- MemberV1Dto: SignUpRequest/SignUpResponse record
- MemberV1ApiSpec: Swagger 스펙 인터페이스
- MemberV1Controller: Facade 위임 및 응답 변환
- MemberV1ApiE2ETest: 정상 가입(201), 검증 실패(400), 중복(409) E2E 테스트

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Controller에서 birthday null/빈 문자열/잘못된 형식 시 400 BAD_REQUEST 반환
- birthday 관련 E2E 테스트 2건 추가
- 회원가입 API .http 파일 생성
- CLAUDE.md 프로젝트 규칙 보강

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- 내 정보 조회 기능 시퀀스/클래스 다이어그램 작성
- 회원가입 시퀀스 다이어그램 Entity 반환 화살표 누락 수정

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- MemberRepository 인터페이스에 findByLoginId 추가
- MemberJpaRepository, MemberRepositoryImpl 구현
- 통합 테스트 2건 추가 (존재/미존재 케이스)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- MemberService.authenticate() 메서드 추가 (loginId 조회 + 비밀번호 검증)
- ErrorType에 UNAUTHORIZED(401) 추가
- 통합 테스트 3건 추가 (성공, 회원 미존재, 비밀번호 불일치)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- GET /api/v1/members/me 엔드포인트 추가 (X-Loopers-LoginId, X-Loopers-LoginPw 헤더 인증)
- MemberInfo에 birthday 필드 추가, MyInfoResponse DTO 추가
- MemberFacade.getMyInfo(), MemberV1ApiSpec, MemberV1Controller 구현
- ApiControllerAdvice에 MissingRequestHeaderException 핸들러 추가
- E2E 테스트 4건 추가 (200, 401×2, 400)
- 통합 테스트 생성자 주입으로 리팩터링 (필드 주입 → 생성자 주입)
- member-v1.http에 내 정보 조회 요청 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- password MIN(8자), MAX(16자) 성공 테스트 추가
- name MIN(한글 2자), MAX(한글 20자) 성공 테스트 추가
- birthday 오늘 날짜(경계값) 성공 테스트 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Member.changePassword() 메서드를 통해 현재 비밀번호 검증, 동일 비밀번호 방지,
새 비밀번호 룰 검증(길이/패턴/생년월일), 암호화까지 도메인 엔티티에서 캡슐화

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
MemberService.updatePassword() 추가, MemberRepository에 updatePassword 메서드 정의,
MemberRepositoryImpl에서 JPA dirty checking 기반 UPDATE 구현, 통합 테스트 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
PATCH /api/v1/members/me/password 엔드포인트 추가,
헤더 PW와 Body currentPassword 일치 검증, E2E 테스트 8건 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
API 응답 규칙, 의존성 방향, 인증 헤더 규칙, TDD 단계별 진행 규칙,
테스트 경계값 케이스 가이드 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
도메인 계층의 PasswordEncoder 의존성을 제거하고,
유즈케이스 검증(현재 비밀번호 확인, 동일 비밀번호 확인)을
MemberService로 이동하여 의존성 방향 규칙 준수

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
refactor: 비밀번호 변경 검증 책임을 서비스 레이어로 분리
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
feat: 회원 도메인 기능 구현 (가입, 조회, 비밀번호 변경)
MemberInfo에 withMaskedName() 메서드 추가하여
이름의 마지막 글자를 *로 마스킹하는 기능 구현

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
feat: 내 정보 조회 시 이름 마스킹 기능 추가
LOGIN_ID_PATTERN을 추가하여 영문 대소문자와 숫자만 허용

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
letter333 and others added 24 commits March 4, 2026 15:07
- @component → @Service/@repository 어노테이션 일관성 통일 (coupon 도메인)
- getMyCoupons() 3개 COUNT 쿼리를 SUM+CASE 1개 통합 쿼리로 최적화
- getIssuableCoupons() 전체 ID 메모리 로딩을 DB LEFT JOIN 쿼리로 대체
- OrderFacade 쿠폰 검증/할인 계산 로직을 MemberCouponService로 추출
- 미사용 import 정리

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 통합 쿼리(getStatusCounts)로 대체된 개별 COUNT 메서드 3건 제거
- LEFT JOIN 쿼리로 대체된 getIssuedCouponIds, findAllIssuable 체인 제거
- MemberCouponListInfo 미사용 import 제거
- validateAndCalculateDiscount → validateAndGetCoupon으로 변경하여
  OrderFacade에서 MemberCoupon 재조회 없이 쿠폰 사용 처리

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
refactor: 코드 품질 개선 및 데드 코드 정리
SELECT ... FOR UPDATE 네이티브 쿼리로 상품 row를 잠근 후 재고를 변경하여
동시 주문 시 Lost Update로 인한 과매도(overselling)를 방지한다.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Product: 기존 재고용 비관적 락(findByIdForUpdate) 재사용하여 likeCount 증감 시 Lost Update 방지
- Brand: @Version 낙관적 락 + TransactionTemplate 재시도(최대 3회)로 동시성 문제 해결
- BrandEntity에 version 필드 추가, BrandRepository에 likeCount 전용 메서드 추가
- ApiControllerAdvice에 ObjectOptimisticLockingFailureException 핸들러 추가
- Product/Brand 각각 동시성 테스트(10 스레드) 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
useCoupon 호출 시 SELECT FOR UPDATE로 row-level lock을 획득하여
동시 사용 요청에서 정확히 1건만 성공하도록 보장한다.
원자적 UPDATE 대신 비관적 락을 선택한 이유:
- OrderFacade 구조상 할인 계산을 위한 SELECT 생략 불가
- 도메인 객체(MemberCoupon.use())에서 에러 세분화 유지
- 프로젝트 전체 동시성 패턴과 일관성 확보

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
주문 취소 시 재고 복구·쿠폰 반환 등 부수 효과의 이중 실행을 방지하기 위해
Order에 비관적 락(SELECT FOR UPDATE)을 적용하고, 단일 locking read 패턴으로
MySQL REPEATABLE READ 환경에서의 stale read 문제를 해결한다.

- OrderJpaRepository: @lock(PESSIMISTIC_WRITE) 적용 JPQL 추가
- OrderService: getOrderForUpdate, saveOrder 메서드 추가
- OrderFacade: cancelOrder, changeOrderStatusForAdmin 비관적 락 기반으로 변경
- 단위 테스트 수정 및 동시성 통합 테스트(OrderCancelConcurrencyTest) 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
createOrder, cancelOrder, changeOrderStatusForAdmin에서 Product 락을
클라이언트 요청 순서대로 획득하던 것을 productId 오름차순으로 정렬하여
순환 대기(Circular Wait) 조건을 원천 차단한다.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 락 없이 취소하는 위험 경로(OrderService.cancelOrder) 제거
- 취소는 OrderFacade에서 비관적 락 + 재고복원 + 쿠폰취소를 포함한 완전한 흐름으로만 실행
- 주문 상태 변경 동시성 테스트 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
feat: 동시성 문제 해결 — 비관적 락 및 데드락 방지 적용
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- MemberCouponRepository에서 미사용 existsByMemberIdAndCouponId 제거
- LikeFacadeTest의 transactionTemplate 모킹을 mockTransactionTemplate()으로 추출

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Product/Brand의 like_count 증감을 낙관적 락 + 엔티티 수정 방식에서 네이티브 SQL(like_count + 1) 원자적 UPDATE로 변경
- LikeFacade의 브랜드 좋아요에서 TransactionTemplate 재시도 로직 제거
- LikeFacadeConcurrencyTest 신규 추가: 10명 동시 좋아요/취소 시 like_count 정합성 검증
- ProductLikeCountPersistenceTest 신규 추가: DB 반영 검증

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- MemberCouponEntity에 @Version 필드 추가로 낙관적 락 적용
- 비관적 락(lockById, findByIdForUpdate) 제거 → findByIdWithCoupon 사용
- OrderFacade.createOrder()에 @retryable 추가 (OptimisticLock 실패 시 최대 3회 재시도)
- spring-retry 의존성 및 @EnableRetry 설정 추가
- 동시성 테스트에서 ObjectOptimisticLockingFailureException 처리 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
상품 목록 API에 brandId 필터 파라미터를 추가하고,
필터(brandId, categoryId, keyword)와 정렬(PRICE_ASC, LIKES_DESC) 조합이
정상 동작하는지 검증하는 E2E 테스트 4건을 보강

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
feat: 상품 목록 조회 브랜드 필터링, 인덱스 최적화 및 시드 데이터 배치 Job 추가
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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 244 files, which is 94 over the limit of 150.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5d08881c-998f-42ec-90a6-6a9eba59c211

📥 Commits

Reviewing files that changed from the base of the PR and between 3309617 and d8e2613.

⛔ Files ignored due to path filters (13)
  • .DS_Store is excluded by !**/.DS_Store and included by **
  • CLAUDE.md is excluded by !**/*.md and included by **
  • claude/skills/analyze-query/SKILL.md is excluded by !**/*.md and included by **
  • claude/skills/requirements-analysis/SKILL.md is excluded by !**/*.md and included by **
  • docs/design/01-requirements.md is excluded by !**/*.md and included by **
  • docs/design/02-sequence-diagram.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/member-erd.md is excluded by !**/*.md and included by **
  • docs/member-profile-lookup-design.md is excluded by !**/*.md and included by **
  • docs/member-signup-design.md is excluded by !**/*.md and included by **
  • docs/product-query-performance-after-index.md is excluded by !**/*.md and included by **
  • docs/product-query-performance-before-index.md is excluded by !**/*.md and included by **
📒 Files selected for processing (244)
  • .gitignore
  • apps/commerce-api/build.gradle.kts
  • apps/commerce-api/src/main/java/com/loopers/CommerceApiApplication.java
  • apps/commerce-api/src/main/java/com/loopers/application/address/AddressCommand.java
  • apps/commerce-api/src/main/java/com/loopers/application/address/AddressFacade.java
  • apps/commerce-api/src/main/java/com/loopers/application/address/AddressInfo.java
  • apps/commerce-api/src/main/java/com/loopers/application/brand/BrandCommand.java
  • apps/commerce-api/src/main/java/com/loopers/application/brand/BrandDetailInfo.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/category/CategoryCommand.java
  • apps/commerce-api/src/main/java/com/loopers/application/category/CategoryDetailInfo.java
  • apps/commerce-api/src/main/java/com/loopers/application/category/CategoryFacade.java
  • apps/commerce-api/src/main/java/com/loopers/application/category/CategoryInfo.java
  • apps/commerce-api/src/main/java/com/loopers/application/coupon/CouponCommand.java
  • apps/commerce-api/src/main/java/com/loopers/application/coupon/CouponDetailInfo.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/CouponIssueInfo.java
  • apps/commerce-api/src/main/java/com/loopers/application/coupon/MemberCouponInfo.java
  • apps/commerce-api/src/main/java/com/loopers/application/coupon/MemberCouponListInfo.java
  • apps/commerce-api/src/main/java/com/loopers/application/example/ExampleFacade.java
  • apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java
  • apps/commerce-api/src/main/java/com/loopers/application/like/LikeInfo.java
  • apps/commerce-api/src/main/java/com/loopers/application/member/MemberFacade.java
  • apps/commerce-api/src/main/java/com/loopers/application/member/MemberInfo.java
  • apps/commerce-api/src/main/java/com/loopers/application/order/OrderAdminDetailInfo.java
  • apps/commerce-api/src/main/java/com/loopers/application/order/OrderCommand.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/OrderProductInfo.java
  • apps/commerce-api/src/main/java/com/loopers/application/product/ProductAdminDetailInfo.java
  • apps/commerce-api/src/main/java/com/loopers/application/product/ProductCacheEvictEvent.java
  • apps/commerce-api/src/main/java/com/loopers/application/product/ProductCacheEvictListener.java
  • apps/commerce-api/src/main/java/com/loopers/application/product/ProductCommand.java
  • apps/commerce-api/src/main/java/com/loopers/application/product/ProductDetailCacheRepository.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/ProductImageInfo.java
  • apps/commerce-api/src/main/java/com/loopers/application/product/ProductInfo.java
  • apps/commerce-api/src/main/java/com/loopers/application/product/ProductListCacheRepository.java
  • apps/commerce-api/src/main/java/com/loopers/application/product/ProductOptionInfo.java
  • apps/commerce-api/src/main/java/com/loopers/domain/address/Address.java
  • apps/commerce-api/src/main/java/com/loopers/domain/address/AddressRepository.java
  • apps/commerce-api/src/main/java/com/loopers/domain/address/AddressService.java
  • apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java
  • apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandRepository.java
  • apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.java
  • apps/commerce-api/src/main/java/com/loopers/domain/category/Category.java
  • apps/commerce-api/src/main/java/com/loopers/domain/category/CategoryRepository.java
  • apps/commerce-api/src/main/java/com/loopers/domain/category/CategoryService.java
  • apps/commerce-api/src/main/java/com/loopers/domain/coupon/Coupon.java
  • apps/commerce-api/src/main/java/com/loopers/domain/coupon/CouponCodeGenerator.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/CouponValidator.java
  • apps/commerce-api/src/main/java/com/loopers/domain/coupon/IssuableCoupon.java
  • apps/commerce-api/src/main/java/com/loopers/domain/coupon/MemberCoupon.java
  • apps/commerce-api/src/main/java/com/loopers/domain/coupon/MemberCouponRepository.java
  • apps/commerce-api/src/main/java/com/loopers/domain/coupon/MemberCouponService.java
  • apps/commerce-api/src/main/java/com/loopers/domain/coupon/MemberCouponStatus.java
  • apps/commerce-api/src/main/java/com/loopers/domain/coupon/MemberCouponStatusCounts.java
  • apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleService.java
  • apps/commerce-api/src/main/java/com/loopers/domain/like/Like.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/like/TargetType.java
  • apps/commerce-api/src/main/java/com/loopers/domain/member/Member.java
  • apps/commerce-api/src/main/java/com/loopers/domain/member/MemberRepository.java
  • apps/commerce-api/src/main/java/com/loopers/domain/member/MemberService.java
  • apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java
  • apps/commerce-api/src/main/java/com/loopers/domain/order/OrderPeriod.java
  • apps/commerce-api/src/main/java/com/loopers/domain/order/OrderProduct.java
  • apps/commerce-api/src/main/java/com/loopers/domain/order/OrderProductStatus.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/product/DiscountType.java
  • apps/commerce-api/src/main/java/com/loopers/domain/product/ImageType.java
  • apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java
  • apps/commerce-api/src/main/java/com/loopers/domain/product/ProductImage.java
  • apps/commerce-api/src/main/java/com/loopers/domain/product/ProductImageValidator.java
  • apps/commerce-api/src/main/java/com/loopers/domain/product/ProductOption.java
  • apps/commerce-api/src/main/java/com/loopers/domain/product/ProductOptionValidator.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/product/ProductValidator.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/address/AddressEntity.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/address/AddressJpaRepository.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/address/AddressRepositoryImpl.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/category/CategoryEntity.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/category/CategoryJpaRepository.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/category/CategoryRepositoryImpl.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/coupon/CouponEntity.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/MemberCouponEntity.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/coupon/MemberCouponJpaRepository.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/coupon/MemberCouponRepositoryImpl.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeEntity.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/member/MemberEntity.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/member/MemberJpaRepository.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/member/MemberRepositoryImpl.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderEntity.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderProductEntity.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/product/CachedPage.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductDetailCacheRepositoryImpl.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductEntity.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductFullTextIndexInitializer.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductImageEntity.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepositoryCustom.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepositoryCustomImpl.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductListCacheRepositoryImpl.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductOptionEntity.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/address/AddressV1ApiSpec.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/address/AddressV1Controller.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/address/AddressV1Dto.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/brand/BrandAdminV1ApiSpec.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/brand/BrandAdminV1Controller.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/brand/BrandAdminV1Dto.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/BrandV1Controller.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/brand/BrandV1Dto.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/category/CategoryAdminV1ApiSpec.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/category/CategoryAdminV1Controller.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/category/CategoryAdminV1Dto.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/category/CategoryV1ApiSpec.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/category/CategoryV1Controller.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/category/CategoryV1Dto.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/coupon/CouponAdminV1ApiSpec.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/coupon/CouponAdminV1Controller.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/coupon/CouponAdminV1Dto.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/coupon/CouponV1ApiSpec.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/coupon/CouponV1Controller.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/coupon/CouponV1Dto.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/LikeV1Controller.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeV1Dto.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/member/MemberV1ApiSpec.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/member/MemberV1Controller.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/member/MemberV1Dto.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderAdminV1ApiSpec.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderAdminV1Controller.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderAdminV1Dto.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/OrderV1Controller.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/ProductAdminV1ApiSpec.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductAdminV1Controller.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductAdminV1Dto.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1ApiSpec.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Dto.java
  • apps/commerce-api/src/main/java/com/loopers/support/auth/AdminValidator.java
  • apps/commerce-api/src/main/java/com/loopers/support/config/PasswordEncoderConfig.java
  • apps/commerce-api/src/main/java/com/loopers/support/error/ErrorType.java
  • apps/commerce-api/src/test/java/com/loopers/application/address/AddressFacadeTest.java
  • apps/commerce-api/src/test/java/com/loopers/application/brand/BrandFacadeTest.java
  • apps/commerce-api/src/test/java/com/loopers/application/like/LikeFacadeConcurrencyTest.java
  • apps/commerce-api/src/test/java/com/loopers/application/like/LikeFacadeTest.java
  • apps/commerce-api/src/test/java/com/loopers/application/member/MemberFacadeTest.java
  • apps/commerce-api/src/test/java/com/loopers/application/member/MemberInfoTest.java
  • apps/commerce-api/src/test/java/com/loopers/application/order/OrderCancelConcurrencyTest.java
  • apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeTest.java
  • apps/commerce-api/src/test/java/com/loopers/application/order/OrderStatusChangeConcurrencyTest.java
  • apps/commerce-api/src/test/java/com/loopers/application/product/ProductCacheEvictListenerTest.java
  • apps/commerce-api/src/test/java/com/loopers/application/product/ProductFacadeTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/address/AddressServiceTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/address/AddressTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandLikeCountConcurrencyTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandServiceTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/category/CategoryServiceTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/category/CategoryTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/coupon/CouponIssueConcurrencyTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/coupon/CouponUseConcurrencyTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/member/MemberServiceTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/member/MemberTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/order/OrderProductTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/product/ProductImageTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/product/ProductLikeCountConcurrencyTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/product/ProductLikeCountPersistenceTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/product/ProductOptionTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/product/ProductStockConcurrencyTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/product/ProductTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/product/ProductValidatorTest.java
  • apps/commerce-api/src/test/java/com/loopers/infrastructure/member/MemberEntityTest.java
  • apps/commerce-api/src/test/java/com/loopers/infrastructure/member/MemberRepositoryImplIntegrationTest.java
  • apps/commerce-api/src/test/java/com/loopers/infrastructure/product/ProductDetailCacheRepositoryImplTest.java
  • apps/commerce-api/src/test/java/com/loopers/infrastructure/product/ProductListCacheRepositoryImplTest.java
  • apps/commerce-api/src/test/java/com/loopers/interfaces/api/address/AddressV1ApiE2ETest.java
  • apps/commerce-api/src/test/java/com/loopers/interfaces/api/brand/BrandAdminV1ApiE2ETest.java
  • apps/commerce-api/src/test/java/com/loopers/interfaces/api/brand/BrandV1ApiE2ETest.java
  • apps/commerce-api/src/test/java/com/loopers/interfaces/api/category/CategoryAdminV1ApiE2ETest.java
  • apps/commerce-api/src/test/java/com/loopers/interfaces/api/category/CategoryV1ApiE2ETest.java
  • apps/commerce-api/src/test/java/com/loopers/interfaces/api/coupon/CouponAdminV1ApiE2ETest.java
  • apps/commerce-api/src/test/java/com/loopers/interfaces/api/coupon/CouponV1ApiE2ETest.java
  • apps/commerce-api/src/test/java/com/loopers/interfaces/api/like/LikeV1ApiE2ETest.java
  • apps/commerce-api/src/test/java/com/loopers/interfaces/api/member/MemberV1ApiE2ETest.java
  • apps/commerce-api/src/test/java/com/loopers/interfaces/api/order/OrderAdminV1ApiE2ETest.java
  • apps/commerce-api/src/test/java/com/loopers/interfaces/api/order/OrderV1ApiE2ETest.java
  • apps/commerce-api/src/test/java/com/loopers/interfaces/api/product/ProductAdminV1ApiE2ETest.java
  • apps/commerce-api/src/test/java/com/loopers/interfaces/api/product/ProductV1ApiE2ETest.java
  • apps/commerce-api/src/test/java/com/loopers/support/auth/AdminValidatorTest.java
  • apps/commerce-api/src/test/resources/docker-java.properties
  • apps/commerce-batch/src/main/java/com/loopers/batch/job/seedproduct/SeedProductDataDictionary.java
  • apps/commerce-batch/src/main/java/com/loopers/batch/job/seedproduct/SeedProductJobConfig.java
  • apps/commerce-batch/src/main/java/com/loopers/batch/job/seedproduct/step/SeedProductTasklet.java
  • apps/commerce-batch/src/test/java/com/loopers/CommerceBatchApplicationTest.java
  • build.gradle.kts
  • docs/product-index-design.sql
  • docs/product-list-queries.sql
  • gradle.properties
  • http/address-v1.http
  • http/brand-admin-v1.http
  • http/brand-v1.http
  • http/category-admin-v1.http
  • http/category-v1.http
  • http/commerce-api/member-v1.http
  • http/coupon-admin-v1.http
  • http/coupon-v1.http
  • http/like-v1.http
  • http/order-admin-v1.http
  • http/order-v1.http
  • http/product-admin-v1.http
  • http/product-v1.http

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 suggest fixes for GitHub Check annotations.

Configure the reviews.tools.github-checks setting to adjust the time to wait for GitHub Checks to complete.

@letter333 letter333 changed the title feat: 상품 목록 브랜드 필터링 및 상품 상세 Redis 캐시 적용 [volume-5] 상품 목록 브랜드 필터링 및 상품 Redis 캐시 적용 Mar 13, 2026
letter333 and others added 3 commits March 13, 2026 19:55
- 상품 목록 조회 Cache-Aside 패턴 적용 (키워드 검색은 캐시 우회)
- ProductCacheEvictEvent/Listener로 상품 생성/수정/삭제 시 캐시 무효화
- CachedPage DTO로 Page 직렬화/역직렬화 처리

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- ProductDetailCacheRepository 직접 호출을 ProductCacheEvictEvent 발행으로 변경
- OrderFacade에서 루프 내 개별 evict 대신 Set으로 모아 한 번에 이벤트 발행

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Never Do 항목에 리팩토링 시 테스트 수정 금지 규칙 추가
- CommerceBatchApplicationTest에서 spring.batch.job.enabled=false 설정하여 Job 미지정 시 컨텍스트 로드 실패 방지

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@letter333 letter333 merged commit 1f4c902 into Loopers-dev-lab:letter333 Mar 14, 2026
1 check passed
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.

2 participants