물론 거의 그런 코드는 보지 못했고, 추천하지는 않습니다. (유지보수나 추후 기능 추가에 고려할 사항이 많아진다고 생각)
클래스 위에 @Cacheable을 선언하면 "해당 클래스 내에 선언된 모든 public 메서드"가 자동으로 @Cacheable의 영향권에 들어갑니다.
@Service@Cacheable(cacheNames = "users")// 1. 클래스 레벨 선언publicclassUserService {
// 2. 이 메서드도 캐싱 적용 (키는 id)public User findUser(Long id) { ... }
// 3. 이 메서드도 자동으로 캐싱 적용 (키는 email)public User findByEmail(String email) { ... }
}
하지만 이렇게 사용하면 키 충돌 (Key Collision) 및 타입 에러가 발생하거나
동일 class에 crud 모두 작성할 경우, Write/Update/Delete 메서드까지 모두 캐싱되는 대참사가 벌어지기 때문에, 사용하지 않는 것을 권장합니다.
그나저나 이건 왜 만들었는지 모르겠네요.
@CacheEvict
위에서 다룬 @Cacheable이 캐시의 등록과 조회 등을 담당했다면 이 @CacheEvict는 캐시 삭제(무효화)를 위한 Annotation 입니다.
만약 데이터가 추가되거나 삭제되는 경우에는 저장소에 등록된 캐시를 초기화 해줘야 하는데, 이를 위한 기능입니다.
ℹ️Info
초기화를 진행하지 않는다면, Cache Hit으로 인해, 과거의 데이터가 조회되어 데이터 정합성이 깨지는 문제가 발생할 수 있습니다.
이 Annotation 역시, Class와 Method 레벨이 모두 지정이 가능합니다. (😒)
@CacheEvict - Method 레벨
거의 대부분 메서드 레벨에 걸어서 사용하게 되며 다음과 같은 형식으로 작성하게 됩니다.
@CacheEvict(value = "users", key = "#userId")public UserDto updateNickname(Long userId, String newNickname) {
//... 실제 비즈니스 로직
}
동작은 value (혹은 cacheNames)와 key를 사용하여, 해당 캐시를 삭제하게 됩니다.
적용 가능한 Attribute는 다음과 같습니다.
어트리뷰트 명
타입
설명
특징 및 차이점
value / cacheNames
String[]
캐시 저장소 이름 지정
@Cacheable과 완전히 동일하게 동작
key
String
비울 캐시의 키 지정 (SpEL 사용)
@Cacheable과 동일하게 동작
keyGenerator
String
커스텀 키 생성기 빈(Bean) 이름
@Cacheable과 동일하게 동작
cacheManager
String
사용할 CacheManager 빈 이름
@Cacheable과 동일하게 동작
cacheResolver
String
커스텀 CacheResolver 빈 이름
@Cacheable과 동일하게 동작
condition
String
캐시 비우기 실행 조건 (SpEL 사용)
이 조건이 true일 때만 캐시를 삭제함
allEntries
boolean
지정된 캐시 내 모든 데이터 삭제 여부
@CacheEvict 전용 옵션 1 (기본값: false)
beforeInvocation
boolean
메서드 실행 전 캐시 삭제 여부
@CacheEvict 전용 옵션 2 (기본값: false)
value, cacheNames, key, keyGenerator.. 등은 모두 동일하고 일부 옵션만 추가 정리합니다.
allEntries는 전체 비우기를 위한 옵션입니다.
특정 키 하나만 지우는 게 아니라, 해당 캐시 영역(네임스페이스) 전체를 통째로 날려버릴 때 사용합니다.
@CacheEvict(value = "users", allEntries = true)
@Cacheable(value = "users", key = "#user.id")와 같은 캐시 처리가 되어있는 메서드가 실행되면 다음과 같은 캐시가 쌓이게 됩니다. (Redis 기준)
users::1 ➡️ { "id": 1, "name": "손흥민", "age: 20" }
users::2 ➡️ { "id": 2, "name": "황희찬", "age: 21" }
users::3 ➡️ { "id": 3, "name": "이강인", "age: 24" }
만약 해가 바뀌어 age 계산을 다시 해줘야하는 경우에는 기존의 캐시 데이터는 잘못된 데이터 입니다. (영원히 늙지 않는 사람..?)
이런 케이스에 @CacheEvict(value = "users", allEntries = true)을 사용하여 초기화 하면 편리하게 처리할 수 있습니다.
🔥Danger
Redis 환경에서 캐시 데이터가 수백만 개 쌓여있는 상태라면 allEntries = true를 조심히 써야 합니다.
Redis가 해당 키들을 패턴 매칭(KEYS)으로 한 번에 조회하고 지우는 과정에서 싱글 스레드인 Redis가 순간적으로 멈추는(Blocking) 장애가 발생할 수 있기 때문입니다.
데이터가 너무 많을 때는 캐시 만료 시간(TTL)을 짧게 주는 방식으로 우회하는 것이 안전합니다! (생각보다 케이스 있음)
beforeInvocationattribute는, 캐시를 메서드가 성공적으로 다 실행된 후에 지울지, 아니면 실행되기도 전에 미리 지울지 결정합니다.
기본값은 false이고, 이때 동작은 메서드가 에러 없이 정상적으로 끝났을 때만 캐시를 지웁니다.
true로 설정 시, 메서드가 실행되기 전에 캐시부터 먼저 지웁니다.
메서드 실행 중 예외(Exception)가 터져서 실패하더라도 캐시는 이미 지워진 상태가 됩니다.
@Cacheable 항목에서 다루었던 sync 옵션은 @CachePut에 존재하지 않습니다.
sync는 캐시 미스가 발생했을 때 다수의 쓰레드가 동시에 DB를 찌르는 것을 막는 옵션(조회용)이지만, @CachePut은 애초에 캐시를 조회하지 않고 무조건 메서드를 실행(CUD용)하기 때문입니다.
@Caching
@Caching 어노테이션은 하나의 메서드에 여러 개의 캐시 작업(조회, 등록, 삭제 등)을 복합적으로 수행해야 할 때 사용됩니다.
자바 어노테이션 특성상 동일한 어노테이션(예: @CacheEvict)을 하나의 메서드 위에 중복해서 선언할 수 없기 때문에, 스프링은 이를 묶어서 처리할 수 있도록 @Caching을 제공합니다.
이 어노테이션 역시 클래스 레벨과 메서드 레벨 모두 지정이 가능합니다.
@Caching - Method 레벨
주로 데이터의 생성, 수정, 삭제(CUD)가 일어날 때 연관된 여러 개의 캐시를 동시에 다뤄야 하는 상황에 사용됩니다.
다음과 같이 작성하게 됩니다.
호출부에서 updatePost()를 실행하면 내부 비즈니스 로직이 완료된 후, evict 배열에 선언된 3가지 캐시 제거 작업이 순차적으로 모두 수행됩니다.
ℹ️Info
@Caching 내부에 정의된 속성들은 배열 형태로 전달되며, 선언된 순서대로 실행됩니다.
만약 evict와 put을 동시에 섞어서 사용한다면, 스프링 내부 캐시 인터셉터의 기본 처리 순서(보통 eviction 후 put)에 따라 동작하게 됩니다.
만약 @Caching을 사용하지 않는다면 updatePost 메서드 실행 후 수동으로 CacheManager를 주입받아 캐시를 하나하나 지워주는 코드를 작성해야 하겠지만, 이 어노테이션 덕분에 선언적으로 깔끔하게 처리가 가능합니다.
ℹ️Info
Java 8부터 @Repeatable 기능이 지원되면서 동일한 어노테이션을 중복 선언할 수 있게 되었지만, 스프링의 캐시 어노테이션들은 하위 호환성과 명시적인 그룹화를 위해 여전히 컨테이너 어노테이션인 @Caching을 사용하는 방식을 유지 및 권장하고 있습니다.
@Caching은 자체적인 특별한 속성 대신, 이전 @Cacheable 과 @CacheEvict 어노테이션을 배열로 받습니다.
어트리뷰트 명
타입
설명
한 줄 요약
cacheable
Cacheable[]
여러 개의 @Cacheable을 묶어서 선언
여러 저장소에서 동시에 데이터를 조회/등록할 때
put
CachePut[]
여러 개의 @CachePut을 묶어서 선언
메서드 실행 후 여러 캐시를 동시에 업데이트할 때
evict
CacheEvict[]
여러 개의 @CacheEvict을 묶어서 선언
연관된 다수의 캐시를 한 번에 무효화(삭제)할 때
각 배열 내부에 들어가는 어노테이션 속성(key, condition, unless 등)은 단독으로 사용할 때와 완전히 동일하게 SpEL 문법을 지원합니다.
// 예시: 내 프로필을 수정하면 내 정보 캐시는 업데이트하고, 전체 랭킹 캐시는 초기화하는 복합 작업@Caching(
put = @CachePut(value = "users", key = "#userDto.id"),
evict = @CacheEvict(value = "userRanking", allEntries = true)
)public UserDto updateProfile(UserDto userDto) {
return userService.modify(userDto);
}
🔥Danger
🤔 @Caching 안에 @Cacheable을 여러 개 넣으면 어떻게 될까요?
결론부터 말씀드리면 권장하지 않으며, 예상과 다르게 동작할 수 있습니다.
@Caching(cacheable = { @Cacheable(value="A"), @Cacheable(value="B") }) 형태로 지정 시, 첫 번째 캐시(A)에서 Hit가 발생하면 메서드가 실행되지 않으므로 두 번째 캐시(B)는 체크조차 하지 않거나 데이터가 채워지지 않는 정합성 이슈가 발생할 수 있습니다.
따라서 cacheable 속성은 거의 쓸 일이 없으며, 다중 캐시를 날리는 evict 용도로만 사용하시는 것을 강력히 추천합니다.