TIL : Spring Boot를 활용한 캐시 삭제 문제 해결

Skadi·2024년 9월 4일
0

오늘은 Spring Boot 애플리케이션에서 리뷰 서비스 기능 구현 중 발생한 캐시 삭제 문제와 예외 처리의 안전성 부족에 대해 해결한 경험을 정리해보려 합니다.


1. 문제 상황

@Service
public class ReviewService {

    @Cacheable(value = "averageRatingCache", key = "#storeId")
    public Double calculateAverageRating(UUID storeId) {
        // 실제 평균 평점 계산 로직 (예: 데이터베이스 조회)
        double rating = 0.0; // 예시로 데이터베이스에서 평점 가져오기
        return rating;
    }

    // @CacheEvict(value = "averageRatingCache", key = "#storeId")
    public void reportReview(UUID reviewId, String reportMessage, Long userId) {
        // 리뷰를 신고하는 로직
        // ...

        UUID storeId = getStoreIdFromReview(reviewId); // 리뷰에서 가게 ID를 가져오는 메서드
        evictCache(storeId);  // 이 호출은 프록시를 거치지 않음 (Self-Invocation 문제 발생)
    }

    @CacheEvict(value = "averageRatingCache", key = "#storeId")
    public void evictCache(UUID storeId) {
        System.out.println("Cache evicted for storeId: " + storeId);
    }

    private UUID getStoreIdFromReview(UUID reviewId) {
        // 리뷰 ID로부터 가게 ID를 가져오는 로직
        return UUID.randomUUID(); // 실제 로직으로 변경 필요
    }
}

Spring Boot 애플리케이션에서 리뷰 서비스를 구현하는 과정에서, 캐시 삭제와 관련된 문제가 발생했습니다. 리뷰를 신고할 때 해당 가게의 평균 평점 캐시를 삭제해야 하는 요구사항이 있었고, 이를 위해 @CacheEvict 어노테이션을 사용했습니다. 하지만, 캐시가 예상대로 삭제되지 않는 문제가 발생했습니다.

또한, 캐시 삭제 과정에서 cacheManager.getCache 메서드 호출 시, 캐시 객체가 존재하지 않을 경우 NullPointerException (NPE)가 발생할 수 있는 위험이 있었습니다. 이와 같은 상황을 충분히 고려하지 않고 구현한 점이 아쉬웠습니다.


2. 문제의 원인 분석

이 문제는 두 가지 원인과 관련이 있었습니다:

  1. Spring AOP의 프록시 메커니즘과 Self-Invocation 문제

    프록시 메커니즘: Spring AOP는 프록시 객체를 사용하여 메서드 호출을 가로채고, 부가적인 로직(예: 캐싱, 트랜잭션 관리)을 추가합니다. 그러나 프록시 객체는 외부에서 메서드를 호출할 때만 개입합니다.

    Self-Invocation 문제: 동일 클래스 내의 메서드 호출은 프록시를 거치지 않고 직접 호출되기 때문에, AOP 기능(예: @CacheEvict)이 적용되지 않았습니다. reviewReport 메서드 내에서 evictCache 메서드를 직접 호출했을 때, 프록시를 경유하지 않아 캐시 무효화가 수행되지 않았습니다.

  2. 캐시 관리와 예외 처리의 안전성 부족:

    • 캐시 삭제 시 cacheManager.getCache가 null을 반환할 경우 NPE가 발생할 가능성을 충분히 고려하지 않았습니다. 이는 예외 처리에 대한 안전성 부족을 의미하며, 안정적인 애플리케이션 동작을 보장하기 위해 개선이 필요했습니다.

3. 해결 방안

문제를 해결하기 위해 두 가지 접근 방안을 고려했습니다:

  1. CacheManager를 사용한 명시적 캐시 무효화:

    • @CacheEvict 대신 Spring의 CacheManager를 사용하여 캐시를 직접 무효화하도록 수정했습니다. 이 방법을 통해 프록시 문제를 우회할 수 있었고, 캐시가 정상적으로 삭제되었습니다. 또한, cacheManager.getCache 호출 시 null 체크를 추가하여 예외를 방지했습니다.
    public class ReviewService {
    
        private final CacheManager cacheManager;
    
        public ReviewService(CacheManager cacheManager) {
            this.cacheManager = cacheManager;
        }
    
        public ReviewResponseDto reportReview(UUID reviewId, String reportMessage, Long userId) {
            // 리뷰를 신고하는 로직
            UUID storeId = getStoreIdFromReview(reviewId);
            clearAverageRatingCache(storeId);  // 명시적으로 캐시 무효화
    
            return new ReviewResponseDto(); // 실제 필요한 반환 값
        }
    
        private void clearAverageRatingCache(UUID storeId) {
            Cache cache = cacheManager.getCache("averageRatingCache");
            if (cache != null) {
                cache.evict(storeId);
                System.out.println("Cache evicted for storeId: " + storeId);
            } else {
                System.out.println("Cache not found: averageRatingCache");
            }
        }
    
        private UUID getStoreIdFromReview(UUID reviewId) {
            // 리뷰 ID로부터 가게 ID를 가져오는 로직
            return UUID.randomUUID(); // 실제 로직으로 변경 필요
        }
    }
  2. 서비스 구조 리팩토링:

    • 캐시 관리 로직과 비즈니스 로직을 분리하여, 캐시와 관련된 작업을 별도의 서비스나 유틸리티 클래스로 관리할 수 있도록 리팩토링했습니다. 이를 통해 캐시 관리의 책임을 명확히 하고 코드의 가독성과 유지보수성을 향상시켰습니다.

4. 문제 해결 결과

CacheManager를 사용하여 캐시를 명시적으로 무효화하고 null 체크를 추가한 후, 리뷰 신고 기능에서 캐시가 정상적으로 삭제되는 것을 확인했습니다. 다양한 테스트 케이스를 통해 캐시 무효화가 올바르게 작동하고 예외가 발생하지 않는지 검증할 수 있었습니다.


5. 배운점 및 향후 개선점

Spring AOP의 프록시 메커니즘과 자기 호출에 대한 이해가 중요함을 배웠습니다. 캐시 무효화와 같은 기능은 CacheManager를 사용하여 명시적으로 관리하는 것이 더 안정적일 수 있습니다.
예외 처리의 중요성 또한 다시 한번 깨닫게 되었습니다. 예상치 못한 예외 상황을 충분히 고려하여, 애플리케이션의 안정성을 높여야 합니다.

  1. 캐시 관련 로직을 더욱 모듈화하여 코드의 재사용성과 유지보수성을 높일 계획입니다.
  2. 캐시 무효화의 성능과 영향을 면밀히 분석하여 최적화할 수 있는 부분을 찾고 개선할 예정입니다.
  3. 예외 상황에 대한 처리를 강화하고, 예상치 못한 예외 발생 시에도 애플리케이션이 안정적으로 동작할 수 있도록 더욱 면밀히 검토할 것입니다.
  4. 로그와 모니터링 도구를 활용하여 캐시 동작과 무효화가 적절히 이루어지고 있는지 지속적으로 모니터링하고, 문제 발생 시 빠르게 대응할 수 있도록 준비합니다.

0개의 댓글