캐싱이란 무엇인가
- 오랜 시간이 걸리는 작업의 결과를 저장해서 향후 동일한 요청 시 시간과 비용을 단축시키는 기법입니다.
- 고성능 애플리케이션을 만드는데 가장 중요한 포인트 중 하나입니다.
캐싱의 종류 및 비교
캐싱에는 크게 로컬캐싱과 글로벌 캐싱으로 나누어서 생각해볼 수 있습니다.
로컬 캐싱 : WAS 서버에서 사용
글로벌 캐싱 : 별도의 캐시 서버를 둬서 사용
로컬캐싱 vs 글로벌 캐싱
항목 | 로컬 캐싱 | 글로벌 캐싱 |
---|---|---|
저장위치 | 애플리케이션 서버에 저장 | 별도의 캐시 서버에 저장 |
데이터간 공유 | 다른 서버에서 참조하기 어려움 | 서버간 데이터 공유 쉬움 |
Memory, Disk 사용 | 애플리케이션 장비와 공유 | 별도의 서버를 사용함으로 해당 서버의 리소스를 모두 사용가능 |
캐시 데이터 변경 시 | - 해당 서버를 제외하고 모든 Peer에 변경사항 전달 - All-to-All Replication - 애플리케이션 서버가 Scale out하게되면 성능저하됨 |
서비스가 확장될때, 캐시 데이터가 클 수록 효과적임 |
장단점
구분 | 장점 | 단점 |
---|---|---|
로컬 캐시 | 같은 JVM내에 혹은 같은 장비에서 데이터를 가져오므로 성능이 좋다. | 하나의 장비내에 있는 데이터만 동기화가 이루어 진다. |
글로벌 캐시 | 데이터 동기화가 실시간으로 이루어지기 때문에 모든 사용자가 동일한 데이터를 가질수 있다. | 네트워크를 통해 데이터를 전달하기 때문에 로컬 캐시에 비해 상대적으로 느리다. |
두 캐시를 비교 후 Careers 프로젝트에서는 대용량 트래픽에 대한 고려와 서비스 확장에도 무리없이 소화가능한 글로벌 캐시를 사용하기로 하였습니다.
Cache hit, cache miss 란
캐싱을 반영하기 위해서는 cache hit, cache miss 개념에 대해서 인지해야 합니다.
cache hit : 요청한 데이터가 캐시에 존재할 경우 캐시 히트라고 말합니다.
cache miss : 요청한 데이터가 캐시에 없을 경우 캐시 미스라고 말합니다.
애플리케이션에서 캐시 히트율이 높은 경우에 성능이 좋은 애플리케이션이라 할 수 있습니다.
캐시 미스가 발생되면 DB에 해당 데이터를 요청하고 캐시에 저장 후 클라이언트에 전송됩니다. 해서 추후 동일 요청 시 캐시 히트가 발생하게됩니다.
즉 여러 곳에 반영하는 것이 아니라 서비스를 이용하면서 어떤 부분에 캐싱 처리를 하여 cache hit율을 높일지 고민해야만 했습니다.
그럼 어느 기능에 적용하는게 좋은가?
고려사항
- 요청에 따른 동일한 결과값
- 자주 조회되는 데이터
- 자주 업데이트가 발생하지 않는 데이터
여러 기능 중 업데이트 빈도수가 적으면서, 자주 조회하는 데이터가 무엇일지 고민해보니 Profile 기능이 적합하다고 판단하였습니다. 그래서 이미 Session 스토리지로 사용하고있는 Redis를 이용하여 캐싱을 적용하기로 하였습니다.
Spring Boot cache적용방법
/**
* @author junehee
*/
@EnableCaching
@SpringBootApplication
public class CareersApplication {
public static void main(String[] args) {
SpringApplication.run(CareersApplication.class, args);
}
}
스프링 부트에서는 @EnableCaching 애노테이션으로 캐싱 기능을 활성화합니다.
캐싱 기능이 활성화가 되면 내부적으로 Proxy 패턴을 사용하여 @Cacheable 애노테이션이 달린 메서드들이 호출될 때마다 캐싱 처리를 하게 됩니다. 물론 처음 호출되면 캐싱 된 데이터가 없기 때문에 DB에서 데이터를 가져온 뒤 이후 요청부터 캐싱 처리하게 됩니다.
@Bean
public CacheManager redisCacheManager(
@Qualifier("redisCacheConnectionFactory") RedisConnectionFactory redisConnectionFactory) {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration
.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.entryTtl(Duration.ofDays(cacheTtl));
return RedisCacheManager.RedisCacheManagerBuilder
.fromConnectionFactory(redisConnectionFactory)
.cacheDefaults(redisCacheConfiguration).build();
}
RedisCacheManager를 빈으로 등록하여 사용하도록 하였으며, ttl 설정을 주어 일정 시간이 지나면 데이터를 삭제하도록 설정하였습니다.
@Cacheable(key = "#curatorId", value = "getProfile")
public Profile getProfile(int curatorId) {
return profileMapper.getUserProfile(curatorId);
}
캐시의 이름은 getProfile로 설정하였으며, 해당 캐시의 key 값은 매개변수로 넘어오는 curatorId값으로 하였습니다. 즉 getProfile요청이 들어오면 getProfile의 캐시의 key값을 통해 값을 확인 후 체크하여 없으면 DB에서 값을 가져오고 있으면 캐시에 데이터를 읽어와 바로 전달하게 됩니다.
Redis 캐싱을 반영 후 성능개선 확인 😎
Redis 캐시 도입 전
Redis 캐시 도입 후
도입 전 722ms, 도입 후 109ms로 약 7배 정도 성능효과를 보았습니다.
Reference
https://zdnet.co.kr/view/?no=20131119174125
https://j0free.tistory.com/3
https://12bme.tistory.com/550
https://dahye-jeong.gitbook.io/spring/spring/2020-04-09-cache#undefined-2
https://meetup.toast.com/posts/225
'프로젝트 > Careers' 카테고리의 다른 글
Logging Framework는 어떤게 좋을까? (0) | 2021.08.16 |
---|---|
Jenkins를 사용한 CD 구성 (0) | 2021.08.12 |
Jenkins를 사용한 CI 구성 (0) | 2021.08.05 |
CI/CD 왜 필요한가? (0) | 2021.08.05 |
Session Storage 선택과정 (0) | 2021.07.29 |
댓글