스프링 부트 Cache
ConcurrentMapCacheManager
스프링 부트에서는 @EnableCaching
사용 시, 인메모리 캐싱이 활성화 된다.
이를 이용해 어플리케이션 내에 ConCurrentHashMap
을 활용한 캐싱이 간단하게 이루어진다.
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
CacheManager cacheManager() {
return new ConcurrentMapCacheManager();
}
}
다만,
-
- 캐싱되는 데이터의 유효기간을 설정하지 못한다는 단점.
-
- 다른 어플리케이션과 캐싱 데이터를 공유하지 못한다는 단점.
두 가지가 크게 작용한다.
RedisCacheManager
@Configuration
@EnableCaching
public class CacheConfig {
@Autowired
RedisProperties redisProperties;
@Bean
RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(
redisProperties.getHost(),
redisProperties.getPort()
);
}
RedisCacheConfiguration redisCacheConfiguration() {
return RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(60L))
.serializeKeysWith(SerializationPair.fromSerializer(RedisSerializer.string()))
.serializeValuesWith(SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues();
}
@Bean
CacheManager cacheManager() {
return RedisCacheManagerBuilder
.fromConnectionFactory(redisConnectionFactory())
.cacheDefaults(redisCacheConfiguration())
.build();
}
}
위와 같이 레디스를 사용하며, 캐싱에 적용될 규칙들을 더 엄밀히 사용할 수 있게 된다.
다음으로, 어플리케이션 레벨에서 이를 활용하는 방안을 살펴보자
캐시 사용 방법
캐시에는 CacheName
, Cache Key
라는 개념이 존재한다.
CacheName
은 해당 데이터가 저장될 캐시 저장소를 가리키며,CahceKey
는 지정된 저장소에 Key-Value
로 저장되는 상황에서의 Key
를 지정한다.
@CacheConfig
@CacheConfig
는 클래스레벨 내 메소드'들'에 적용될 범용적인 설정을 할 수 있도록 한다.
@Cacheable
은 캐시가 존재할 시, 해당 메소드를 호출하지 않고, 값을 캐시에서 불러온다.@CachePut
은 메소드를 무조건적으로 실행하며, 반환된 값을 캐시에 적재하고 이를 Caller에 응답한다.
메소드의 리턴 값으로 캐시가 업데이트 된다는 점을 유의해야한다.
@CacheEvict
는 메소드를 무조건적으로 실행하며, 캐시에서 해당 id로 적재된 데이터를 삭제한다.@Caching
은 메소드내에 위의 여러 캐싱을 적용할 수 있도록 허용한다.
캐시 사용 사례
캐싱을 적용하기 위해 다음과 같은 컨트롤러를 구성했다.
@RestController
@RequiredArgsConstructor
public class MemberController {
private final MemberService memberService;
@PostMapping
public ResponseEntity<Long> addRandomUser() {
return ResponseEntity.ok(memberService.addMember());
}
@GetMapping("/all")
public ResponseEntity<List<Member>> loadAll() {
return ResponseEntity.ok(memberService.loadAll());
}
@GetMapping("/{id}")
public ResponseEntity<Member> addRandomUser(
@PathVariable final Long id
) {
return ResponseEntity.ok(memberService.getDetail(id));
}
@PutMapping("/{id}/{name}")
public ResponseEntity<Member> modifyName(
@PathVariable final Long id,
@PathVariable final String name
) {
return ResponseEntity.ok(memberService.changeName(id, name));
}
@DeleteMapping("{id}")
public ResponseEntity<Member> deleteMember(
@PathVariable final Long id
) {
memberService.delete(id);
return ResponseEntity.ok().build();
}
}
마찬가지로, 이와 대응되는 Service 객체를 구성한다.
@Service
@RequiredArgsConstructor
@Transactional
@CacheConfig(cacheNames = "memberCache")
public class MemberService {
private final MemberRepository memberRepository;
@CacheEvict(key = "'all'")
public Long addMember() {
return memberRepository.save(new Member(new Random().toString())).getId();
}
@Transactional(readOnly = true)
@Cacheable(key = "'all'")
public List<Member> loadAll() {
return memberRepository.findAll();
}
@Cacheable(key = "#id")
@Transactional(readOnly = true)
public Member getDetail(final Long id) {
return memberRepository.findById(id).orElseThrow();
}
@CacheEvict(allEntries = true)
public void delete(final Long id) {
memberRepository.deleteById(id);
}
@CachePut(key = "#id")
public Member changeName(final Long id, final String name) {
final Member member = memberRepository.findById(id).orElseThrow();
member.setName(name);
return member;
}
}
@CacheEvict(allEntries = true)
가 눈에 띈다.
이 어노테이션은 addUser
, delete
메소드에 적용되어 있다.
해당 어노테이션은 키와 관계 없이 해당 캐시 저장소의 모든 데이터를 삭제하라는 의미이며, 이를 통해 loadAll
과 같은 메소드에서 데이터 오류를 피할 수 있다.
하지만, 테이블의 행이 수천에서 수만개가 되는 상황에서 전체 행을 불러오는 쿼리는 드뭄으로, 이런 쿼리를 캐싱하는 것은 메모리낭비를 유발한다고 볼 수 있다.
changeName
을 보면, 실제 DB에 대한 레코드 수정과 캐싱된 데이터를 모두 수정하고 있지만,
이는 loadAll
에서 리턴되는 응답값을 수정하지 못하고 있다.
이를 수정하기 위해
@Caching(
put = @CachePut(key = "#id"),
evict = @CacheEvict("'all'")
)
와 같이 바꿔줄 수도 있겠다.
또는, 직접 CacheManager
를 주입 받아 적용할 수 있겠지만, 이는 비즈니스 로직을 오염시키는 상황을 초래한다.
이럴빠엔 AOP를 왜 써?
캐싱 활용 - 페이징에서의 캐싱
하지만, 페이징에는 충분히 응용될 수 있다.
페이징에 대한 캐싱은 다음 포스트에서 다루겠다.
'SpringBoot' 카테고리의 다른 글
Spring boot - @Transactional 전파 주의 사항 (0) | 2025.01.13 |
---|---|
Spring boot - Cache (2) 페이지 조회 캐싱 (0) | 2025.01.08 |
[SpringBoot] 스프링의 에러처리 탐구 (0) | 2024.01.17 |
[Spring] Spring Security 인증 구성 (0) | 2024.01.02 |
[Spring] 다수의 SecurityFilterChain 구성 방법 (0) | 2024.01.01 |