현재 내가 속해있는 동아리 큐시즘에선 실제 기업들과 협업해 3주 동안 프로젝트를 진행한다.
내가 속한 팀은 렛츠커리어와 함께 프로젝트를 진행하고 있다.
렛츠커리어에선 현재 대표님이자 멘토님 한 분이 프로그램을 담당한다.
그러다보니 프로그램이 자주 업데이트 될 수 없는 환경이었기에 프로그램 목록을 보여주는 페이지에 캐싱을 적용하는 것이 조회 성능을 올려 줄 것이라고 생각했다.
Redis를 선택한 이유
캐싱을 하는 방법은 여러가지가 있다.
인메모리에 구현하는 방법이 있고, 분산 캐시 시스템을 사용하는 방법도 있다.
인메모리 캐시를 사용하면 구현이 간단하고 빠르다.
렛츠커리어는 실제 제공되는 홈페이지니까 확장성을 고려해 Redis를 사용해 캐싱을 구현하기로 했다.
어떻게 적용했을까?
캐싱 전략 중 Look Aside 전략을 사용하기로 했다.
이유는 다음과 같다.
1. 프로그램 목록 페이지의 특성
DB와 Redis 사이의 데이터 정합성이 깨지는 문제가 있지만, 프로그램 목록 페이지의 경우 새로운 데이터가 업데이트 잘 되지 않고, 조회는 많이 발생한다.
그렇기에 Read Through 보단 Look Aside 전략이 더 알맞을 것이라 생각했다.
2. 구현의 간단함
또 가장 좋은 점은 구현이 매우 간단하다.
@Cacheable 어노테이션을 사용하면 구현할 수 있다.
1. RedisCacheConfiguration을 설정해준다.
@Configuration
@EnableCaching
public class RedisCacheConfig {
//Redis 캐시를 관리하기 위한 설정을 위한 클래스
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper()
.findAndRegisterModules() // JavaTimeModule을 자동으로 등록
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
@Bean
public CacheManager programCacheManager(RedisConnectionFactory redisConnectionFactory, ObjectMapper objectMapper) {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration
.defaultCacheConfig()
.serializeKeysWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new StringRedisSerializer()))
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new Jackson2JsonRedisSerializer<>(objectMapper, ProgramListResponse.class)))
.entryTtl(Duration.ofMinutes(5L));
return RedisCacheManager
.RedisCacheManagerBuilder
.fromConnectionFactory(redisConnectionFactory)
.cacheDefaults(redisCacheConfiguration)
.build();
}
}
@EnableCaching 어노테이션을 달아줘야 캐싱 기능이 활성화 된다.
2. 캐싱을 할 부분에 @Cacheable 어노테이션을 추가한다.
@Cacheable(cacheNames = "getProgramList",
key = "'programList:careerTag:' + #careerTag + ':programTypes:' + #programTypes + ':v' + @programService.getCurrentVersion() + ':page:' + #page")
public ProgramListResponse getProgramList(String careerTag, List<String> programTypes, int page) {
...
}
(캐시 키를 설정하게 된 이유는 https://securityinit.tistory.com/254 글에서 볼 수 있습니다.)
이렇게 구성을 끝냈고 , 성능을 비교해보자.
부하 테스트 진행
우리가 만든 서버가 어느정도 부하를 견디는지 알기위해 부하테스트를 진행했다.
Test tool
k6 : v0.50.0
현재 개발중인 프로젝트의 경우 고객이 프로그램 리스트 페이지에 먼저 도달한다.
모든 유저가 프로젝트 리스트 페이지를 봐야만 하기에 해당 페이지에 대한 성능을 테스트했다.
렛츠커리어의 경우 하루 평균 3~400명의 유저가 방문하고(특별한 경우 제외) , 대략 1분 36초 정도 활발하게 활동한다.
그래서 요청 인원은 350~400명, 테스트 시간은 96초를 기준으로 테스트를 진행하였다.
캐시 적용 전
1. 400명의 유저가 96초동안 최대 요청을 보낸다.
응답 시간
최대 : 1m0s
최소 : 4.81ms
평균 : 35.03s
요청 응답률 : 30.12%
초당 처리량 (throughput) : 8.952177/s
2. 96초동안 20초마다 100명씩 인원을 증가시켜 요청보냄
응답 시간
최대 : 57.25s
최소 : 2.07ms
평균 : 33.28s
요청 응답률 : 59.53%
초당 처리량 (throughput) : 5.372191/s
3. 응답시간이 5초를 넘어가는 경우
구글 리서치를 보면 응답시간이 5초를 넘어가면 90퍼 유저가 이탈을 한다고 나와있다.
응답을 받기까지 5초가 넘게 걸리는 유저의 수를 봐보자.
350명의 유저가 1초동안 최대의 요청을 보냈다.
checks를 보면 100 %로 모든 요청이 5초 이내에 처리되었음을 알 수 있다.
checks가 5% 로 95%의 요청이 응답을 받는데 5초 이상임을 확인
대부분의 유저가 응답이 오지 않아 이탈할 확률이 높아진다.
캐시 적용 후
Redis를 활용해 캐싱을 적용한 뒤 성능을 비교해보았다.
1. 400명의 유저가 96초동안 최대 요청을 보낸다.
응답 시간
최대 : 99.7ms
최소 : 113µs
평균 : 6.02ms
요청 응답률 : 99.24%
초당 처리량 (throughput) : 397.264125/s
2. 96초동안 20초마다 100명씩 인원을 증가시켜 요청보냄
응답 시간
최대 : 2.55s
최소 : 591µs
평균 : 5.71ms
요청 응답률 : 100%
초당 처리량 (throughput) : 231.502515/s
3. 응답시간이 5초를 넘어가는 경우
checks가 100% 로 모든 응답이 5s 이내로 들어왔음을 확인
유저가 응답이 오지 않아 이탈하는 경우는 발생하지 않을 것으로 판단된다.
캐시전용 전과 캐시적용 후 초당처리량(Throughput)을 비교해보면 44.37배 초당처리량이 올라간 것을 확인했다.