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 |