단순 캐시를 넘어서: Redis로 쿼리 구조를 설계하다

호기성세균·2025년 4월 21일
0

트러블슈팅

목록 보기
24/24
post-thumbnail

※ 본 글은 실제 서비스에서 적용된 사례를 기반으로 작성하였으나, 서비스명·도메인·DB 구조 등 민감한 정보는 모두 익명 처리하였으며, 외부 공개를 위해 일부 구현 방식은 재구성하였습니다.

1. 문제 상황

내가 개선하고자 했던 검색 기능은 프로젝트의 핵심 기능 중 하나로, 관리자들이 다양한 필터 조건을 기반으로 수시로 데이터를 조회하는 구조였다. 그러나 아래와 같은 제약으로 인해 성능 저하가 발생하고 있었다.

[문제 요약]

  • 인덱스 설정 불가: 외부 시스템의 DB를 사용하고 있어, 인덱스 추가나 쿼리 튜닝이 불가능한 구조였다.
  • 풀스캔 쿼리 발생: 실행계획 분석 결과, 조건 조합이 다양하고 서브쿼리/조인이 복잡하여 테이블 풀스캔이 지속적으로 발생하였다.
  • 동시 요청 환경: 백오피스 상 주요 기능으로, 여러 관리자가 동시에 검색을 수행할 가능성이 매우 높음
  • 검색 대상 데이터량: 검색 대상 데이터는 150만 건 이상

2. 개선 목표 및 방향성

개선 목표

  • 쿼리 대상 범위 축소를 통해 RDB의 I/O 부하를 줄이고 응답 시간을 단축한다.
  • 동시 요청에 견딜 수 있도록 캐시 중심의 구조로 트래픽을 분산시킨다.
  • 외부 DB의 인덱스 제약을 우회하고, 실질적으로 보조 인덱스 역할을 수행할 구조를 만든다.

→ 단순한 쿼리 튜닝이나 페이징 처리만으로는 해결이 불가능했다.


3. Redis 전략

단순 결과 캐싱은 조건 조합이 너무 다양하고, 실시간성이 중요한 검색 기능에 적합하지 않았다.

  • 검색 조건은 30개 이상, 조합 수는 수천만 이상
  • 모든 조건을 키로 구성하면 Redis 키 수 폭발
  • 조건 일부만 변경되어도 캐시 miss 발생 → DB 풀스캔 재발

→ 캐시의 실효성이 없었고, 성능 개선 효과도 거의 없음

위와 같은 상황때문에 단순히 결과를 캐시하는 것이 아니라,
Redis를 보조 인덱스로 활용해 쿼리 대상 자체를 줄이는 전략을 설계하였다.

  • Redis에 저장된 키 기준으로 선 필터링
  • Redis에 저장된 ID만 대상으로 RDB 쿼리 수행

핵심 목적: 풀스캔 제거, 대상 건수 감소, 쿼리 병목 해소


4. 키 설계 및 조건 선정 방식

고객사 데이터 분석 기반

사용자 행동 데이터 기반으로 분석

  • 실제 고객사 요청 중 자주 사용되는 조건 조합 TOP 2 분석
  • 이 중 카디널리티가 낮고, 필터링 효과가 높은 조건만을 추려 Redis 키로 구성
우선순위조건명
1dbSource
2UniversityCode
3masterUniversityCode
4changeJobCount
5adminId

예시 키 구조

resume:search:{dbSource}:{bachelorUniversityCode}:{masterUniversityCode}:{adminId}
resume:search:DB2:null:null:7

Null 처리 전략

모든 조건이 없어도 "null" 문자열로 명시하여 키 일관성을 유지함으로써 모든 요청이 캐시 hit가 가능하도록 설계하였다.


5. Redis 캐시의 동작 흐름

  1. 사용자가 검색 요청 시, Redis 키 조합으로 캐시 조회
  2. Redis에는 해당 조건 조합에 대한 이력서 ID 리스트만 저장되어 있음
  3. 애플리케이션은 해당 ID 리스트를 기준으로 RDB에 WHERE id IN (...) 쿼리 수행
  4. 이후 복잡한 조건(회사명, 키워드 등)은 해당 ID 범위 내에서만 필터링 수행

💡 실제로 Redis 선 필터링으로 50% 이상 데이터가 제거되었으며, DB 조회량은 수백만 건에서 수만 건 이하로 축소됨


6. 성능 개선 결과

실측 데이터 기준

항목개선 전개선 후비고
평균 응답 시간2,325ms450msRedis 선 필터링으로 쿼리 대상 축소, 풀스캔 제거 효과 반영
전체 대상 수1,500,000건필터링 후 평균 50~100K 건Redis 보조 인덱싱으로 검색 대상 범위 대폭 축소
Redis 캐시 hit율없음100%null 포함 명시적 키 구성으로 모든 요청에 대해 캐시 hit 유도
GC 병목빈번없음전체 객체 로딩 대신 ID 기반 조회로 메모리 사용량 최소화

Redis 캐시 hit율은 왜 100%인가?

  • 모든 조건은 null일 경우 명시적으로 "null" 문자열로 치환되기 때문에,
    모든 요청은 항상 유효한 Redis 키로 변환된다.
  • 이 구조상 Redis는 단순 결과 캐시가 아니라 조건 조합 인덱스 역할을 수행하며,
    설계상 miss가 발생하지 않도록 되어 있다.
  • Redis에 해당 키가 없다면 최초 miss지만, 이후 요청부터는 항상 hit된다.

➡️ 캐시 hit율은 논리적으로 100%에 수렴한다.


7. 정리 및 결론

✅ 핵심 전략

  • Redis를 단순 결과 저장소가 아니라, “선 필터링용 보조 인덱스”로 사용
  • 조건 분석을 통해 최소 키 조합으로 캐시 키 폭발 방지
  • Java 후처리는 필터링 전 전체 로딩 대신, Redis ID 기반 정제된 대상만 조회
  • 캐시 miss 없이 null 처리와 adminId 기반 분기처리로 정합성과 보안까지 확보

✅ 구조적 효과

  • 쿼리 대상이 150만 건 → 수만 건 이하로 감소
  • DB 풀스캔 제거
  • 동시 요청 환경에서도 성능 안정화
  • 메모리 부하 없이 애플리케이션 안정성 확보

인덱스 못 박는 환경에서 버벅이는 쿼리 때문에 고생하고 있다면,
이 방식이 꽤 쓸만할 수도 있다.
Redis는 꼭 결과 캐싱에만 쓰라는 법 없다.
오히려 이렇게 "보조 인덱스"로 쓰는 전략이 실전에서 더 빛을 발하는 경우도 있다.
이 경험이 비슷한 문제를 겪는 팀들에게 작은 참고라도 되길...

profile
공부...열심히...

0개의 댓글