728x90
반응형

[Spring Boot] @Cacheable 캐시가 동작하지 않는 경우

환경 : Spring Boot 3.x, Kotlin 1.9



1. 원인

  • @EnableCaching 설정 누락
  • 같은 클래스 내부에서 호출 (프록시 우회)
  • 반환값이 null인 경우 캐시 미저장
  • 캐시 키가 매번 달라지는 경우


2. 해결 방법

2.1) @EnableCaching 추가

Spring Boot에서 캐시를 사용하려면 명시적으로 활성화해야 함

@EnableCaching
@SpringBootApplication
class Application

fun main(args: Array<String>) {
    runApplication<Application>(*args)
}

의존성도 확인

// build.gradle.kts
dependencies {
    implementation("org.springframework.boot:spring-boot-starter-cache")
}

2.2) 같은 클래스 내부 호출 금지

@Transactional, @Async와 동일한 문제. Spring AOP 프록시 기반이라 내부 호출 시 캐시가 무시됨

// 동작 안 함
@Service
class MemberService {
    fun getMemberInfo(id: Long): MemberDto {
        val member = getMember(id) // 내부 호출 → @Cacheable 무시
        return MemberDto.from(member)
    }

    @Cacheable("members")
    fun getMember(id: Long): Member {
        return memberRepository.findById(id).orElseThrow()
    }
}

// 정상 — 별도 클래스로 분리
@Service
class MemberService(private val memberCacheService: MemberCacheService) {
    fun getMemberInfo(id: Long): MemberDto {
        val member = memberCacheService.getMember(id) // 외부 Bean 호출
        return MemberDto.from(member)
    }
}

@Service
class MemberCacheService(private val memberRepository: MemberRepository) {
    @Cacheable("members")
    fun getMember(id: Long): Member {
        return memberRepository.findById(id).orElseThrow()
    }
}

2.3) 캐시 키 확인

기본 캐시 키는 메서드 파라미터 전체. 파라미터가 매번 다른 객체면 캐시 히트가 안 됨

// 나쁜 예 — request 객체가 매번 새로 생성되면 키가 매번 다름
@Cacheable("members")
fun search(request: MemberSearchRequest): List<Member> { ... }

// 좋은 예 — 명시적으로 키 지정
@Cacheable(value = ["members"], key = "#name")
fun findByName(name: String): List<Member> { ... }

// 복합 키
@Cacheable(value = ["members"], key = "#name + '_' + #status")
fun findByNameAndStatus(name: String, status: Status): List<Member> { ... }

2.4) null 반환 처리

기본적으로 null도 캐시에 저장됨. 하지만 캐시 구현체에 따라 다를 수 있음

// null 결과는 캐시하지 않기
@Cacheable(value = ["members"], unless = "#result == null")
fun findByIdOrNull(id: Long): Member? {
    return memberRepository.findByIdOrNull(id)
}


3. 캐시 삭제

데이터가 변경되면 캐시도 갱신해야 함. 안 하면 오래된 데이터가 계속 반환됨

@Service
class MemberCacheService(private val memberRepository: MemberRepository) {

    @Cacheable(value = ["members"], key = "#id")
    fun getMember(id: Long): Member {
        return memberRepository.findById(id).orElseThrow()
    }

    @CacheEvict(value = ["members"], key = "#id")
    fun updateMember(id: Long, request: MemberUpdateRequest): Member {
        val member = memberRepository.findById(id).orElseThrow()
        member.update(request)
        return memberRepository.save(member)
    }

    @CacheEvict(value = ["members"], allEntries = true)
    fun clearAllCache() {
        // 전체 캐시 삭제
    }
}


4. 캐시 구현체 선택

별도 설정 없으면 ConcurrentMapCache(메모리)가 기본. 운영 환경에서는 Redis나 Caffeine 권장

4.1) Caffeine (로컬 캐시)

// build.gradle.kts
implementation("com.github.ben-manes.caffeine:caffeine")
# application.yml
spring:
  cache:
    type: caffeine
    caffeine:
      spec: maximumSize=500,expireAfterWrite=10m

4.2) Redis (분산 캐시)

// build.gradle.kts
implementation("org.springframework.boot:spring-boot-starter-data-redis")
# application.yml
spring:
  cache:
    type: redis
  data:
    redis:
      host: localhost
      port: 6379

※ 단일 서버면 Caffeine, 멀티 서버(팟 여러 개)면 Redis. 둘 다 쓰는 2-Level 캐시도 가능


728x90
반응형

'Java' 카테고리의 다른 글

[Spring] Profile 기준 Property 구분 적용  (0) 2023.06.28
[SpringBoot] Gradle 변수 Property 활용  (0) 2022.05.30
[MyBatis] List 형식 멤버 변수 조회  (0) 2021.02.12
[Spring] Web Cache 적용  (0) 2020.12.19
[SpringBoot] H2 연동  (0) 2020.07.21

+ Recent posts