Redis 캐시를 적용한 성능 개선
성능 테스트를 하는 이유
성능 테스트성능 개선 배경

2017년 구글의 발표에 따르면, 모바일 환경에서 로딩 시간이 1~3초 사이일 때 이탈률이 32%까지 증가한다고 합니다.
향후 서비스 규모가 커지고 트래픽과 데이터가 축적될 상황을 고려하면, 사용자에게 원활한 경험을 제공하기 위한 '성능 최적화'는 필수적입니다. 제가 생각하는 좋은 웹 성능이란 '콘텐츠가 빠르게 로딩되고 요청에 대한 응답 속도가 신속한 것'입니다.
이를 위해 여러 방법(N+1 문제 해결, 인덱스 최적화 등)이 있겠으나, 이번에는 Redis 캐시를 활용해 성능을 개선해 보았습니다.
테스트 계획 설정
성능 테스트의 핵심은 명확한 목적 설정에 있다고 생각합니다.
이번 테스트의 목표는 객관적인 지표(응답 시간, RPS)를 설정하고, 개선 전후의 차이를 수치로 증명하는 것입니다.
정확한 리소스 측정을 위해 로컬에서 진행하면 안되지만, 아쉽게도 프리티어의 자원으로는 부족했습니다.
응답 시간, RPS와 VUser 설정
목표 응답 시간
p95 기준 200ms 이내 (기존 평균 응답이 150ms 내외였던 점을 고려합니다.)
상위
95% (p95)는 95%의 요청이 해당 시간 이내에 처리되었음을 의미한다.
실제 지속적으로 운영한 서비스가 아니기 때문에 지표는 주관적으로 설정했습니다.
DAU(Daily Access User) : 하루 중 중복 없는 순수 사용자
RPS (Reqeust per second) : 초당 동시 요청 수
1일 총 요청 수
DAU ∗ 1명당 평균 요청 수
20만명 ∗ 3번 = 60만 번
1일 평균 RPS
1일 요청 수 / 하루 12시간을 초로 환산
60만 / 43,200 = 14RPS
1일 최대 트래픽
1일 최대 RPS
1일 평균 RPS ∗ 피크 시간 집중률
14 * 5 = 70RPS
VUser 계산 공식
VUser = 목표 RPS * (시나리오1 응답 시간 + 시나리오2 응답 시간 + .. + n) / 시나리오 개수
VUser = 목표 RPS * (시나리오1 응답 시간 + think time ... + n) / 시나리오 개수
70RPS ∗0.2 = 14명 (15으로 설정)
Think Time은 페이지가 로딩되고 나서 사용자가 데이터를 scan하는 시간으로 생각하고, 이번 테스트에서는 0으로 설정
따라서 목표를 70RPS에서 200ms의 응답을 기대합니다.
테스트 스크립트 작성 (k6)
부하 테스트 도구로는 JMeter, nGrinder 등이 여러 도구가 존재하지만 k6를 사용했습니다.
k6는 Grafana Labs에서 만든 SW이고 아래와 같은 장점이 있습니다.
Javascript로 작성할 수 있어서 비교적 간편하게 스크립트를 작성할 수 있습니다.
적은 메모리를 사용하고 공식 문서를 제공합니다.
Grafana와 호환성이 좋아서 다양한 GUI를 제공받을 수 있습니다.
성능 개선 전 측정 및 분석

사용자 수를 단계별로 늘려가며 시스템의 안정성과 스파이크(Spike)성 트래픽 대응 능력을 확인했습니다.
사용자의 수를 점진적으로 증가시키는 이유는 정확도를 올리기 위함입니다.
서비스 트래픽은 일정하게 오다가 특정 시점에 튀는 경우가 있는데, 이런 트래픽을
스파이크(spike)성 트래픽이라고 합니다.
이런 점을 고려해 볼 때, 선착순 쿠폰 이벤트 같은 걸 진행한다고 하면 짧은 시간에 트래픽이 몰릴 수 있기 때문에 성능 테스트를 진행하는 서비스의 특성을 파악하는 것도 중요합니다.
5
32
312.91ms
10
36
531.12ms
15
42
597.15ms

[분석 결과]
VUser가 5명인 시점부터 이미 목표치인 200ms를 넘어섰습니다.
CPU 점유율은 약 40%였으며, 실제 분산 서버 환경이었다면 서버 한 대가 다운될 경우 남은 서버들이 연쇄적으로 과부하를 견디지 못할 위험이 보였습니다.
캐시 적용 전략
데이터베이스나 외부 서버와의 네트워크 통신은 상대적으로 느리고 비용이 많이 드는 작업입니다.
하지만 캐시를 사용하면 자주 사용하는 데이터를 미리 가져와 저장해둘 수 있으므로, 매번 DB나 외부 서버에 접근할 필요가 줄어듭니다. 이렇게 하면 데이터 접근 속도를 단축시키고 애플리케이션의 전반적인 성능 향상을 기대할 수 있습니다.
다만 캐시를 잘못 사용하면 오히려 큰 효과를 얻지 못할 수도 있습니다. 따라서 해당 API의 캐시 유효 기간과 데이터 정합성을 충분히 검토한 뒤, 캐시를 적용하는 것이 적절한지 판단하는 과정이 필요합니다.
조회가 빈번한 경우
결과가 자주 변경되지 않는 경우
외부 API 호출을 통한 데이터 획득의 경우
계산 비용이 높은 경우 (ex 복잡한 쿼리, 연산)
캐시의 유효 기간 설정
캐시의 TTL(Time-to-Live)을 정하는 기준은 데이터 업데이트의 빈도, 시스템 부하, 요구사항과 같은 항목들을 고려하여적절하게 설정해야 하지만, 명확한 기준을 세우지 못했습니다.
TTL이 너무 길 경우에는 데이터의 신뢰성이 저하되고 데이터 일관성 문제가 발생될 수 잇습니다.
반대로 너무 짧으면 캐시를 사용함으로써 얻는 이점을 극대화할 수 없습니다.
테스트에서는 임의로 30분을 설정했습니다.
캐시 적용 후 개선 결과
5
81
93.76ms
10
88
142.49ms
15
89
375.79ms
VUser 5명 구간에서 응답 시간이 93.76ms로 대폭 개선되며 목표를 달성했습니다.
10명 구간까지는 병목 없이 RPS가 증가하지만 10명 초과 시점부터는 RPS가 더 이상 늘지 않고 응답 시간만 증가하는 포화 지점(Saturation point)이 나타났습니다. 이는 테스트 환경의 최대 요청 처리량의 한계를 보여줍니다.
[포화 지점의 원인 추정]
RPS가 멈추는 이유는 테스트 환경의 스레드 풀 개수나 Network I/O의 한계 때문일 가능성이 있습니다.

CPU 점유율 또한 이전보다 좋은 수치를 기록하면서, 캐시 적용을 통해 성능이 개선되었다는 점을 확인할 수 있습니다.
Summary
캐시를 도입하기 위해서는 명확한 기준이 필요합니다.
서비스 내 GET, POST, PUT, DELETE 요청 비율을 파악하고, 특히 GET 요청 비중이 높을 경우 캐싱 시스템이 큰 효율을 발휘할 수 있을 것입니다.
결과적으로 캐시 도입 이유는 DB 부하를 줄이고 비용을 절감하고자 했습니다.
캐싱은 매우 효과적인 기술 중 하나이지만, Redis와 같은 외부 캐시 서버는 애플리케이션과 독립된 프로세스로 동작하기 때문에 관리 포인트가 늘어난다는 점을 고려해야 합니다. 또한 웹 서버에서 캐시를 관리하는 구조는 시스템 복잡도를 증가시킬 수 있습니다.
따라서 캐시 서버를 도입할 때는 충분한 고민이 필요하며, 동시에 애플리케이션 내부에서도 시간 복잡도를 줄이려는 노력을 이어가야 한다고 생각합니다.
예를 들어, 적절한 알고리즘과 자료구조 선택, 해시 테이블 활용, DB 쿼리 및 인덱스 튜닝, 과도한 반복문 최소화, 비동기 I/O 구조 설계 등을 통해 캐싱 외적인 측면에서도 성능을 개선할 수 있을 것입니다.
Last updated