Java

Spring @Cacheable 상속 시 파라미터명 불일치로 인한 캐시 키 충돌 문제 해결

1103동103호·2026년 3월 28일·조회 272
  발단: "왜 다른 데이터가 나오죠?"
 
  API 개발 중 황당한 버그를 만났습니다.
 
  GET /api/items?category=A → A 카테고리 데이터 ✓
  GET /api/items?category=B → A 카테고리 데이터 ✗ (B여야 함)
  GET /api/items?category=C → A 카테고리 데이터 ✗ (C여야 함)
 
  파라미터를 바꿔서 호출해도 첫 번째 조회 결과만 계속 반환됩니다. 서버를 재시작하면 잠깐 고쳐지다가 또 재발합니다.
 
  대체 뭐가 문제일까요?
 
  ---
  1차 시도: Hibernate 쿼리 의심
 
  Repository 코드를 확인했습니다.
 
  @Query("SELECT DISTINCT e.name FROM #{#entityName} e WHERE e.category = :category")
  List<String> findNamesByCategory(@Param("category") String category);
 
  #{#entityName}이 SpEL 표현식이라 캐싱이 꼬인 걸까요? Hibernate query plan cache 문제일까요?
 
  설정을 변경해봤습니다.
 
  spring.jpa.properties.hibernate.query.plan_cache_max_size=16
 
  해결되지 않았습니다.
 
  Native SQL로 변경해봤습니다.
 
  @Query(value = "SELECT DISTINCT name FROM items WHERE category = :category",
         nativeQuery = true)
 
  그래도 해결되지 않았습니다.
 
  ---
  2차 시도: Database 의심
 
  혹시 DB prepared statement 캐시 문제일까요?
 
  DB 콘솔에서 직접 쿼리를 실행해봤습니다.
 
  SELECT DISTINCT name FROM items WHERE category = 'B';
 
  정상 작동합니다. 올바른 결과가 반환됩니다.
 
  DB 문제가 아닙니다.
 
  ---
  결정적 단서: 같은 Repository인데 왜 하나만 문제?
 
  디버그용 API를 만들어서 확인해봤습니다.
 
  // 이건 정상! 각 카테고리별 올바른 데이터 반환
  List<Item> items = repository.findByCategory("B");
  // → B 카테고리 아이템들 (정상)
 
  // 이건 비정상! A 카테고리 이름 반환
  List<String> names = repository.findNamesByCategory("B");
  // → A 카테고리 이름들 (비정상)
 
  같은 Repository, 같은 파라미터, 같은 조건인데 하나만 잘못됩니다.
 
  이건 Repository 레이어 문제가 아닙니다. 더 상위에서 뭔가 꼬인 겁니다.
 
  ---
  범인 검거: Service 레이어 캐시
 
  갑자기 @Cacheable이 떠올랐습니다.
 
  grep -r "@Cacheable" src/main/java/
 
  // AbstractService.java (부모 클래스)
  @Cacheable(value = "items", key = "#root.target.class.simpleName + '_' + #category")
  public List<String> getNamesByCategory(String category) {
      return repository.findNamesByCategory(category);
  }
 
  캐시가 있었습니다. 캐시 키에 #category가 있으니 카테고리별로 다른 키가 생성되어야 하는데...
 
  잠깐, 자식 클래스를 확인해봤습니다.
 
  // ItemService.java (자식 클래스)
  @Override
  public List<String> getNamesByCategory(String categoryCode) {
      //                                        ^^^^^^^^^^^^
      //                                        어?!
  }
 
  파라미터명이 다릅니다.
 
  - 부모 클래스: String category
  - 자식 클래스: String categoryCode
 
  캐시 키 SpEL은 #category를 참조하는데, 자식 클래스 메서드에는 category라는 파라미터가 없습니다!
 
  ---
  원인 분석
 
  Spring @Cacheable은 AOP 프록시로 동작합니다. 부모 클래스에 붙은 어노테이션이 자식 클래스 메서드 호출에도 적용됩니다.
 
  문제는 SpEL 표현식 #category가 실제 호출되는 메서드의 파라미터명을 참조한다는 것입니다.
 
  // 기대한 캐시 키
  "ItemService_A"
  "ItemService_B"
  "ItemService_C"
 
  // 실제 생성된 캐시 키 (#category 파라미터 없음 → null)
  "ItemService_null"
  "ItemService_null"
  "ItemService_null"
 
  모든 호출이 동일한 캐시 키 ItemService_null을 사용합니다.
 
  첫 번째 호출(A 카테고리)이 캐시되고, 이후 B/C 호출도 같은 캐시에서 A 데이터를 반환한 것입니다.
 
  ---
  해결
 
  파라미터명을 부모 클래스와 일치시켰습니다.
 
  // Before
  public List<String> getNamesByCategory(String categoryCode)
 
  // After
  public List<String> getNamesByCategory(String category)
 
  끝입니다.
 
  3시간 삽질의 원인이 파라미터명 4글자 차이(category vs categoryCode)였습니다.
 
  ---
  대안적 해결 방법
 
  방법 1: 인덱스 기반 참조 사용
 
  // 파라미터명 대신 인덱스로 참조
  @Cacheable(value = "items", key = "#root.target.class.simpleName + '_' + #p0")
  public List<String> getNamesByCategory(String category) { ... }
 
  방법 2: 자식 클래스에 명시적 캐시 선언
 
  @Override
  @Cacheable(value = "items", key = "'ItemService_' + #categoryCode")
  public List<String> getNamesByCategory(String categoryCode) { ... }
 
  ---
  교훈
 
  1. "서버 재시작하면 고쳐져요"는 캐시 문제 신호입니다
    - 재시작으로 캐시가 초기화되어 일시적으로 해결된 것입니다
    - 근본 원인을 놓치기 딱 좋은 증상입니다
  2. @Cacheable 상속 시 파라미터명 일치가 필수입니다
    - SpEL #paramName은 실제 메서드의 파라미터명을 참조합니다
    - 부모-자식 간 파라미터명이 다르면 SpEL 평가가 실패합니다
  3. 증상과 원인이 멀리 있을 수 있습니다
    - 증상: Repository 쿼리가 이상한 데이터 반환
    - 원인: Service 레이어 캐시 키 문제
    - 증상 발생 지점만 파고들면 원인을 찾기 어렵습니다
  4. 인덱스 기반 참조(#p0)가 더 안전할 수 있습니다
    - 파라미터명에 의존하지 않습니다
    - 단, 가독성이 떨어지는 단점이 있습니다
 
  ---
  마무리
 
  Hibernate 버그, DB 버그, Spring Data JPA 버그를 차례로 의심했지만, 결국 제 코드의 파라미터명 불일치가 원인이었습니다.
 
  프레임워크 탓하기 전에 내 코드부터 의심해야 합니다. 특히 상속 구조에서 메서드 시그니처를 변경할 때는 캐시 어노테이션까지 확인하시기 바랍니다.

댓글 0

로그인 후 댓글을 남길 수 있습니다.

아직 댓글이 없습니다.