티스토리 뷰
1. 서론
인기글 API의 성능 문제를 해결하기 위해, 먼저 쿼리 구조를 개선하고 적절한 인덱스를 적용하여 DB 부하를 줄이고 응답 속도를 개선했습니다. 하지만, 사용자 접근 빈도가 높은 인기글 페이지의 특성상, 여전히 반복적인 조회에 의한 DB 접근 비용이 부담으로 남아있었습니다.
이번 글에서는 이를 해결하기 위해 적용한 캐싱 전략에 대해 다뤄보겠습니다. 캐싱이란 무엇인지, 왜 Redis를 선택했는지, 그리고 실제 적용 방식을 함께 살펴보겠습니다.
2. 데이터베이스는 왜 느릴까
우선 데이터 베이스의 종류를 간단히 살펴보면,
[RDB 특정] - 안정성
- HDD 혹은 SSD같은 보조기억 장치에 데이터를 저장
- 행과 열 구조의 정형 데이터 저장(엑셀처럼 생각하면 쉬움)
- SQL 언어로 데이터 조회 및 관리
- 테이블 간 관계를 통해 중복 데이터 최소화
- 스키마 고정 (데이터 구조 변경이 어렵고 사전에 정의 필요)
- 수직 확장 중심 (성능 향상을 위해 서버 성능 업그레이드)
- 대표적으로 MySQL, Oracle, PostgreSQL가 있음
[NoSQL 특징] - 유연성 + 확장성
- HDD 혹은 SSD같은 보조기억 장치에 데이터를 저장
- 비정형 데이터 저장에 적합 (JSON, 문서, Key-Value 등)
- SQL 대신 자체 쿼리 사용 (일부는 SQL 지원)
- 스키마 유연 (데이터 구조를 자유롭게 변경 가능)
- 수평 확장에 유리 (서버를 늘려 성능 향상)
- 대표적으로 MongoDB, Elasticsearch가 있음
[In-Memory DB 특징] - 속도 + 캐싱
- 메모리(RAM) 기반 데이터 저장
- 보통 Key-Value 형태, 실시간 처리에 최적화
- 데이터 휘발성 (일부는 디스크 백업 가능)
- 캐싱 용도, 세션 관리 등에서 자주 사용
- 대표적인 것으로 Redis, Memcached가 있음
2-1. 데이터 저장 장치의 한계
데이터를 저장하는 장치는 보통 HDD나 SDD 같은 보조 기억장치입니다. 이런 장치들은 전원이 꺼져도 데이터가 유지되는 비휘발성 특성을 가지고 있습니다. 하지만 물리적으로 CPU와 걸리가 멀고, 데이터 접근 속도 상대적으로 느립니다.
반면, 메모리(RAM)는 CPU와 가까워 데이터 접근 속도가 매우 빠릅니다. 대신 전원이 꺼지면 데이터가 모두 사라지는 휘발성 특성을 가집니다.
정리하면 HDD나 SDD는 느리지만 데이터가 영구적으로 저장되고, RAM은 빠르지만 데이터가 사라지게 됩니다.
결국 데이터베이스는 안정성을 위해 느린 저장장치를 사용할 수밖에 없기 때문에 기본적으로 느릴 수밖에 없습니다.
2-2. 느린 DB를 빠르게 하는 방법, 캐시
그렇다면 느린 데이터베이스를 빠르게 만들 수는 없을까?
데이터베이스가 느리다고 해서 매번 직접 디스크에 접근하는 것은 비효율적입니다. 그래서 "자주 조회되는 데이터"나 "계산 결과"를 따로 빠른 곳에 임시 저장해두는 방법이 등장했는데, 이 개념이 바로 캐시입니다.
캐시는 느린 저장장치로부터 자주 쓰이는 데이터를 미리 가져와 빠른 메모리 공간에 저장해두는 것입니다. 이를 통해 필요한 데이터를 다시 요청할 때 빠르게 응답할 수 있게 만듭니다.
2-3. 인메모리 DB의 등장 배경
인메모리 DB는 캐시의 시스템화된 형태라고 볼 수 있는데
단순한 캐시 메모리는 소규모 데이터나 일시적인 결과를 저장하는 데에 한정됩니다. 하지만 데이터의 양이 많아지고, 실시간 데이터 처리가 점점 더 중요해지면서 단순한 캐시 이상의 구조가 필요해졌습니다. 특히 2000년대 중반 이후 IoT와 클라우드 기반 서비스가 확산되면서, 데이터 생성량이 급격히 증가했고 수많은 사용자 요청을 빠르게 처리해야 하는 상황이 생겼습니다.
이때 기존 디스크 기반 데이터베이스만으로는 실시간 처리를 감당할 수 없었고, 메모리 가격이 하락하면서 대규모 데이터를 아예 메모리에 저장하는 방식이 현실적으로 가능해졌습니다. 이렇게 탄생한 것이 바로 인메모리 데이터베이스입니다.
인메모리 DB는 단순한 캐시처럼 일부 데이터를 저장하는 것이 아니라, 전체 데이터를 메모리에 올려 빠른 읽기, 쓰기 성능을 제공합니다. 대표적인 예로 Redis, Memcached 등이 있습니다.
3. 인메모리 DB에 대해
[Memcached 특징]
- Key-Value 형태의 단순한 데이터 저장
- 메모리 기반, 디스크에 저장하지 않음
- 데이터가 휘발성 (서버 재시작 시 데이터 소멸)
- 문자열, 숫자 등 간단한 데이터 타입만 지원
- 멀티 스레드 지원 (여러 요청을 병렬 처리 가능)
- 주로 "읽기 캐시" 용도로 사용 (ex: 세션, 짧은 TTL 데이터 저장)
[Redis 특징]
- Key-Value 저장은 기본, 다양한 데이터 타입 지원 (문자열, 해시, 리스트, 셋, 정렬된 셋 등)
- 메모리 기반이지만 디스크에 스냅샷 저장 가능 (퍼시스턴스 기능 지원)
- 데이터가 휘발성일 수도 있고 영구 저장도 가능
- 단일 스레드 기반 (하지만 빠른 처리 속도를 가짐)
- Pub/Sub, 트랜잭션, Lua 스크립트 등 다양한 기능 제공
- 고급 데이터 구조를 활용한 복잡한 캐싱 가능
- 레디스 클러스터를 통한 수평 확장 지원
- 캐시뿐 아니라 메시지 큐, 실시간 데이터 처리 등 다양한 활용 가능
사람들이 왜 Redis를 많이 사용할까..?
Memcached는 작고 간단한 데이터를 빠르게 저장하는 데 강점을 가집니다. 특히 멀티 스레드를 지원해서 다중 요청 처리에 유리한 점도 있습니다. 하지만 이외에는 제약이 많은데, 데이터 타입이 제한적이고, 디스크에 저장할 수 없기 때문에 서버 재시작 시 모든 데이터가 사라집니다. 또한 저장 공간이 부족하면 데이터를 삭제해가며 관리해야 하는데, 이는 중요한 데이터 처리에는 위험할 수 있습니다.
현실적으로, 극한의 상황(ex. 초대규모 트래픽 대응)에서는 Memcached의 성능 최적화가 필요할 수 있지만, 대부분의 상황에서는 개발 편의성과 데이터 안정성이 더 중요하게 여겨집니다. Redis는 다양한 기능과 안정성 덕분에 더 널리 선택되고 있습니다.
특히 실시간 서비스, 세션 관리, 큐 시스템 등 다양한 상황에서 Redis 하나로 해결할 수 있다는 점이 큰 장점입니다.
결국 "극한의 성능"을 원할 때만 Memcached를 쓰고, 일반적인 상황에서는 편의성과 확장성을 고려해 Redis를 선택하는 것이 자연스러운 흐름이라고 볼 수 있습니다.
4. 우리 서비스에서는?
인기글은 과연 캐시 대상일까?
인기글은 서비스 특성상 많은 사용자가 접근하며, 짧은 시간 동안 빈번히 조회되는 데이터입니다. 또한, 일단 선정된 인기글은 일정 시간 동안 크게 변동되지 않는 경우가 많습니다. 따라서 이런 데이터는 데이터베이스를 매번 조회하기보다, 빠른 응답을 위해 메모리에 캐싱해두는 것이 효과적입니다.
이제 현재 StarHub 서비스를 살펴보면, Refresh 토큰의 블랙리스트 관리와 인기글 캐싱이라는 두 가지 기능을 고려해야 합니다.
이때 선택할 수 있는 옵션으로는 Redis와 Memcached가 있습니다. 각각의 특징을 좀 더 자세하게 살펴보고, 어느 것이 더 적합한지 판단해보겠습니다.
4-1. Refresh 토큰의 블랙리스트 관리
Refresh 토큰을 블랙리스트에 저장하려면 영속성이 중요한 요소입니다. 즉, 서버가 재시작되거나 배포가 이루어져도 블랙리스트에 올라간 토큰은 계속 존재해야 합니다. 이 경우는 Memcached는 휘발성 데이터만을 저장하기 때문에, 서버 재시작 시 데이터가 사라지므로 적합하지 않습니다.
반면, Redis는 메모리 기반이지만 디스크에 스냅샷을 저장하거나 퍼시스턴스를 지원하여 영속성이 필요한 데이터를 관리할 수 있습니다. 따라서, Refresh 토큰 블랙리스트 관리에 있어 Redis가 적합한 선택입니다.
4-2. 인기글 캐싱
인기글 캐싱은 주로 읽기 성능과 TTL(Time-To-Live)을 고려해야 하는 부분입니다. 데이터가 자주 변경되거나 일정 시간이 지나면 자동으로 삭제되거나 갱신되어야 할 경우, Redis와 Memcached 모두 TTL을 지원하여 문제 없이 사용할 수 있습니다.
그러나 Memcached는 문자열이나 숫자와 같은 간단한 데이터만 처리할 수 있기 때문에, 복잡한 데이터 구조를 다뤄야 하는 경우에는 불편할 수 있습니다. 예를 들어, 인기글에 대한 데이터를 캐시할 때, Redis는 문자열, 해시, 리스트, 셋, 정렬된 셋 등 다양한 데이터 타입을 지원하여 더 복잡한 캐싱 요구사항을 처리할 수 있습니다.
4-3. Redis로 결정!
앞서 언급한 Refresh 토큰의 블랙리스트 관리를 위해 Redis를 이미 사용하고 있기 때문에, 인기글 캐싱을 위한 Redis의 활용 역시 더 자연스럽습니다. Memcached를 별도로 도입하기보다는 Redis로 모든 캐시 요구사항을 처리하는 것이 더 효율적일 것으로 판단됩니다.
5. Redis를 적용하자
StarHub 서비스에서는 이미 Refresh Token 블랙리스트 관리를 위해 Redis를 도입한 상태입니다.
이 Redis 환경을 그대로 활용해, 인기글 캐싱에도 적용해보기로 했습니다.
- [RedisConfig] Spring Boot에서 Redis와 연동하기 위한 설정 클래스
- [RedisService] RedisTemplate을 기반으로 한 캐시 저장/조회/삭제를 쉽게 사용할 수 있도록 추상화한 유틸 클래스
그런데 여기서 고민이 생겼습니다.
처음에는 인기글 API 내부에 직접 RedisService를 호출해 캐싱하려고 했습니다.
하지만 이렇게 하면 비즈니스 로직이 복잡해지고, 유지보수성이 크게 떨어질 것 같았습니다.
그래서 비즈니스 로직에 캐시 처리를 직접 넣는 대신, Spring AOP를 활용하여 커스텀 어노테이션 기반으로 분리하기로 했습니다.
- @CustomCacheable 같은 어노테이션으로 캐시를 선언
- Aspect에서 캐시 조회/저장 로직 자동 처리
이렇게 하면 비즈니스 코드가 깔끔해지고, 캐시 로직은 공통 처리 영역에서 관리할 수 있습니다.
✅ 단순히 Redis를 붙이는 것만으로는 좋은 캐시 구조라고 생각하지 않습니다. 아래와 같은 항목들을 신중하게 고민했습니다.
1) 캐시 대상 선정
모든 데이터를 캐시하면 메모리만 낭비됩니다. 변동이 적고, 자주 조회되는 데이터를 선정해야 합니다.
인기글은 변동이 적고, 조회가 빈번하므로 캐시 최적 대상입니다.
2) TTL 전략
TTL 설정은 캐시 품질과 서버 부하를 결정하는 중요한 요소입니다.
- TTL 짧음 → 데이터 신선함, 하지만 자주 DB 접근
- TTL 김 → 성능은 좋지만 데이터 오래될 수 있음
상황에 맞게 다르게 설정했습니다.
대상 | TTL 설정 |
인기글 | 5분 |
Refresh 토큰 블랙리스트 | 토큰 만료 시간과 동일하게 설정 |
3) 캐시 무효화 전략
TLL만 설정해도 충분할까? 데이터 변경 시 강제로 캐시를 삭제해야 할까?
인기글은 좋아요 수 변동 같은 소폭 변경에는 큰 영향을 받지 않습니다.
따라서 실시간 동기화 필요성이 크지 않다고 판단하여 TTL(5분) 만으로 충분하다고 결정했습니다.
4) 캐시 Key 설계
Key 네이밍 규칙이 명확해야 합니다.
- 일관된 Prefix 사용: popular:project
- Key 충돌 방지: Prefix를 구분자처럼 사용
5) Eviction 정책 (메모리 관리)
Redis는 메모리 기반 저장소이기 때문에, 메모리 관리 정책도 중요합니다.
- maxmemory-policy 설정 필수
- 대표적인 설정: allkeys-lru (가장 최근에 사용되지 않은 키를 제거)
그럼 실제로 적용한 코드를 확인해보겠습니다.
참고자료
'Troubleshooting' 카테고리의 다른 글
[인기글 불러오기] #4 @Cacheable로 발생한 메모리 문제와 GC 분석 실험기 (0) | 2025.04.30 |
---|---|
[인기글 불러오기] #2 인덱스 최적화 (0) | 2025.04.07 |
[인기글 불러오기] #1 쿼리 구조 개선 (0) | 2025.04.07 |
[이메일 발송 기능] #3 비동기 성능 최적화 (0) | 2025.04.04 |
[이메일 발송 기능] #2 별도 스레드를 활용한 비동기 호출 방식 (0) | 2025.01.10 |
- @Id
- 엔티티 매니저
- 인메모리 db
- 비영속
- @joincolumn
- onetoone
- 최적화
- @OneToMany
- 즉시 로딩
- @Cacheable
- @MappedSuperclass
- @Table
- 영속성 컨텍스트
- @TransactionalEventListener
- JPA
- 조인 전략
- @Entity
- N + 1
- 스키마 자동 생성
- @ManyToOne
- 준영속
- @GeneratedValue
- 연관관계
- 메일
- 변경감지
- mappedBy
- 1차 캐시
- 비동기
- 단일 테이블 전략
- Redis