기술면접 (2) - Redis, 캐시 전략

ssongyi·2025년 5월 27일
1

Java/Spring TIL

목록 보기
13/22

1. 왜 ZSet을 사용했나요?

ZSet(정렬된 집합)은 각 원소에 점수(score)를 부여하고 자동 정렬이 되는 Redis 자료구조입니다.

  • 사용이유:
    인기 검색어는 점수(검색 횟수)에 따라 실시간으로 정렬되어야 하며, 상위 N개를 빠르게 조회해야 합니다. ZSet은 이를 위해 설계된 구조입니다.
  • 핵심 명령어:
    - ZINCRBY: 특정 키의 점수를 증가시킴 O(logN)
    - ZREVRANGE: 높은 점수 순으로 정렬된 상위 항목 조회 O(logN + M) (M: 반환할 개수)

ZSet의 시간복잡도는 ZINCRBY/ZREVRANGE 모두 평균 O(logN)입니다. 높은 조회 성능이 필요한 실시간 트래픽 환경에 적합합니다.

2. 동시에 여러 사용자가 동일한 검색어를 입력할 때 ZSet 업데이트는 어떻게 처리되나요?

Redis는 단일 스레드 기반의 명령어 실행 모델을 사용하기 때문에, ZINCRBY 같은 연산도 동시에 요청되어도 race condition 없이 순차적으로 처리됩니다.

동시성이 필요한 상황에서도 Redis는 안전한 연산 보장을 해줍니다. 다만 트래픽 급증 시 Redis 성능 모니터링 및 클러스터 구성 고려는 필수입니다.

3. 인기 검색어 집계에 TTL(만료 시간)은 설정되어 있나요? 왜 그런 설계를 했나요?

보통 인기 검색어 ZSet에는 TTL을 설정하지 않는 경우가 많습니다.
대신, 점수 자체에 기간 조건을 반영하는 방식으로 단기/장기 랭킹을 관리합니다.

  • 예: 매일 0시에 ZSet 초기화(Top-N 백업 후 reset) → 하루 기준 인기 검색어 제공
  • 또는 주기적으로 오래된 검색어의 점수를 줄이는 방식도 사용

TTL을 설정하면 인기 검색어 데이터가 갑자기 사라질 수 있으므로, 랭킹용 데이터에는 명시적 만료 대신 주기적 정리(batch 처리)가 더 안정적

4. Spring Cache 추상화를 사용한 이유는 무엇인가요? 직접 RedisTemplate을 사용하지 않고요.

Spring Cache 추상화는 @Cacheable, @CachePut, @CacheEvict 등의 어노테이션 기반으로 캐싱을 쉽게 적용할 수 있게 해줍니다.

장점:

  • 코드 간결화 (비즈니스 로직에 캐싱 로직 분리)
  • Redis 외의 다양한 캐시 제공자와도 쉽게 전환 가능
  • AOP 기반이기 때문에 캐시 적용 위치가 명확함

RedisTemplate은 유연하지만 코드가 복잡해지며, 비즈니스 로직과 캐시 로직이 섞일 수 있습니다. 빠르게 캐시를 적용하고 유지보수를 쉽게 하기 위해 Spring Cache가 적합합니다.

5. Look-Aside Caching 패턴이란 무엇인가요?

Look-Aside(혹은 Lazy-Loading) 캐싱은 요청이 들어올 때 캐시에서 먼저 조회하고, 없으면 DB 등에서 조회한 후 캐시에 저장하는 방식

동작 흐름:
1. 캐시에 데이터 있는지 확인
2. 없으면 원본 데이터 소스에서 조회
3. 캐시에 저장하고 응답

Look-Aside는 실패 시 유연하게 대응 가능하며, 캐시 미스 시 fallback이 쉬운 장점이 있습니다. 반면, Read-through는 미스가 발생해도 내부적으로 자동으로 로딩되므로 설정은 더 복잡할 수 있습니다.

6. 검색 키워드를 캐싱 키로 쓸 때 정제는 어떤 기준으로 했고, 왜 그렇게 했나요?

정제 기준:

  • 앞뒤 공백 제거 (trim)
  • 소문자 통일 (대소문자 구분 제거)
  • 특수문자 제거 또는 encoding 처리

7. 검색어에 대한 캐시 유효 시간은 어떻게 설정했나요? 고정인가요, 동적인가요?

일반적으로 고정 TTL(Time-To-Live)을 설정합니다. 예: 5분, 10분 등.

  • 고정 TTL: 빠른 검색 결과 제공, 자주 변하지 않는 데이터에 적합
  • 동적 TTL: 인기나 갱신 빈도에 따라 TTL을 다르게 설정 (복잡도 증가)

검색 결과는 빈번히 변경되지 않으므로 짧은 TTL로 캐싱하고, 만료 후 자동으로 재조회하는 방식이 적절합니다. Redis 메모리 보호 측면에서도 일정 TTL 설정은 필수입니다.

8. Caffeine과 Redis에 각각 10분 TTL을 설정한 이유는 무엇인가요? TTL을 다르게 설정하지 않은 이유는요?

  • Caffeine(로컬 캐시)는 애플리케이션 내에서 빠르게 응답하기 위한 1차 캐시이고,
  • Redis(분산 캐시)는 서비스 간 공유를 위한 2차 캐시입니다.

10분 TTL로 통일한 이유:
데이터 정합성 유지와 TTL 관리 복잡도 최소화 때문입니다.

  • 너무 짧은 TTL은 캐시 미스를 자주 유발하고,
  • 너무 긴 TTL은 오래된 데이터를 보여줄 위험이 있어, 적당한 타협점으로 10분을 설정했습니다.

TTL을 분리하는 것도 가능하지만, TTL 차이로 인한 동기화 문제나 불일치 현상이 생길 수 있어 관리 복잡도가 증가합니다.

9. 자주 조회되는 검색어라도 TTL이 만료되면 다시 DB 조회가 발생하는데, 이런 캐시 미스 상황을 어떻게 처리하셨나요?

  • 캐시 미스 시에는 Look-Aside 패턴으로 DB를 조회하고, 해당 결과를 다시 Redis 및 Caffeine에 저장합니다.
  • 빈번히 조회되는 검색어는 자연스럽게 캐시에 다시 채워지는 구조입니다.

추가적으로, 자주 조회되는 검색어를 사전에 Pre-warm하거나 Hot 키로 분류하여 TTL을 연장하는 전략도 고려할 수 있습니다.

캐시 미스 발생이 잦은 키는 Access Log를 기반으로 식별하여 Preloading하거나 TTL을 유동적으로 조정하면 효과적입니다.

10. TTL 설정이 과도하게 짧거나 길 경우 발생할 수 있는 문제점은 무엇인가요?

TTL 유형문제점
너무 짧은 TTL캐시 미스 빈번 → Redis/DB 부하 증가, 응답 지연
너무 긴 TTL오래된 데이터 제공 가능 → 최신성 저하, UX 악화

특히 인기 검색어와 같이 실시간성이 중요한 기능에서는 TTL이 너무 길 경우 트렌드 반영이 늦어지는 문제가 생깁니다.

트래픽 특성에 따라 TTL을 동적으로 조절하는 전략 (예: LFU 기반 가중 TTL) 도 고려할 수 있습니다.

11. ZSet은 어떤 면에서 Top-N 연산에 최적화되어 있다고 생각하시나요?

  • ZSet은 각 원소에 Score(숫자)를 부여하고 자동으로 오름차순/내림차순 정렬됩니다.
  • ZINCRBY로 점수를 증가시키고, ZREVRANGE로 상위 N개의 검색어를 빠르게 추출할 수 있습니다.

정렬된 데이터를 Top-N 형태로 자주 조회해야 하는 상황에서는 ZSet이 List나 Hash에 비해 성능/구현 면에서 매우 유리합니다.

12. Top-N 인기 검색어는 어떻게 조회하고, 어떤 기준으로 갱신하고 있나요?

  • 조회는 ZREVRANGE popular_keywords 0 9로 진행 (Top 10 예시)

기준:

  • 실시간 검색 횟수(ZINCRBY) 누적 점수
  • 정해진 주기 (예: 1시간, 1일)에 초기화 or decay 처리
    (점수를 절반으로 줄이거나, ZSet을 삭제하고 새로 집계)

운영 환경에서는 시간대별 ZSet(예: popular_keywords:20250526)을 분리하여 시간 기반 통계도 제공합니다.

인기 검색어가 너무 자주 바뀌면 사용자 혼란을 유발할 수 있으므로, 일정 주기마다 스냅샷을 저장하고 사용자에게 제공하는 데이터는 완화된 버전을 사용합니다.

13. Redis 캐시를 적용한 후 정확히 어떤 수치적 성과(응답 시간, DB 부하 등)가 있었나요?

  • DB 쿼리 수 약 80% 감소
  • 검색 응답 시간 300ms → 30ms로 개선
  • 동시 접속자가 많은 피크 타임에도 안정적인 응답 유지

14. 조회수는 어떻게 증가시키고 있나요? Redis에서 어떤 명령어를 사용했고, 이유는 무엇인가요?

조회수는 Redis에서 INCR 명령어를 통해 증가시키고 있습니다.

  • view:song:{id} 라는 키에 대해 INCR을 호출하면 해당 곡의 조회수가 1씩 증가합니다.
  • 이 명령어는 O(1)의 시간 복잡도, 원자성 보장, 고속 처리 가능이라는 장점이 있어 실시간 집계에 적합합니다.

INCR은 존재하지 않는 키에 대해 호출하면 자동으로 0에서 시작되므로 별도의 초기화 없이도 사용이 가능합니다.

15. INCR 연산이 무엇인가요?

INCRRedis의 Atomic Integer Increment 연산으로, 지정된 키의 값을 정수 1만큼 증가시킵니다.

  • 단일 스레드 기반의 Redis 구조 덕분에 INCR 연산은 Race Condition 없이 동시성 안전하게 처리됩니다.
  • 실무에서는 트래픽이 몰리는 상황에서도 정확한 카운팅이 필요한 조회수, 좋아요 수 등에 사용됩니다.

멀티스레드 환경에서도 별도 Lock 없이 안전하게 사용할 수 있다는 점이 핵심

16. 1시간 TTL로 설정한 이유는 무엇인가요? 다른 TTL로 설정하면 어떤 문제가 발생할 수 있을까요?

1시간 TTL은 다음 목적에 최적화된 시간입니다:

  • 어뷰징 방지 유효 시간으로 적절함 (1시간 내 중복 조회 방지)
  • Redis 메모리 보호를 위해 유휴 키를 자동 제거
  • 인기 콘텐츠 감지 주기로도 합리적

다른 TTL의 문제점:

  • 너무 짧은 TTL (예: 1분): 중복 조회 방지 효과가 적고, 불필요한 조회수 증가 발생
  • 너무 긴 TTL (예: 24시간): Redis 메모리 점유 증가 → 메모리 pressure 및 eviction 발생 가능

17. 조회수 키(view:song:{id})와 사용자 기록 키(view:song:{id}:user:{userId})를 분리한 이유는 무엇인가요?

두 키를 분리한 이유는 역할과 기능의 분리입니다:

  • view:song:{id}: 전체 곡의 누적 조회수를 관리
  • view:song:{id}:user:{userId}: 해당 사용자의 중복 조회 여부를 체크

이렇게 분리함으로써, 조회수 집계는 INCR로 빠르게 처리하고, 어뷰징 제어는 개별 사용자 키의 TTL 존재 여부로 판단할 수 있습니다.

하나의 키에 모든 정보를 넣으면 복잡해지고 성능 저하 위험이 있습니다. 역할 분리를 통해 관리성과 확장성을 높입니다.

18. 이러한 키 구조가 실제로 어뷰징을 어떻게 막는지 설명해 주세요. 사용자 IP나 기기 ID 등을 활용하지 않은 이유는요?

키 구조 예: view:song:123:user:456

  • 이 키가 존재하면 1시간 내 동일 사용자의 중복 조회로 간주하여 조회수 증가를 막습니다.
  • 이 키가 없으면 INCR 실행 후, 해당 키를 생성하고 TTL을 1시간 설정

User ID 기반 중복 확인을 통해 서버 측에서 중복 여부를 실시간으로 체크합니다.

왜 IP/기기 ID는 사용하지 않았나?

  • IP: NAT 환경에서는 여러 사용자가 동일 IP를 쓰기 때문에 정확도 낮음
  • 기기 ID: 보안/프라이버시 문제, 수집 및 관리가 복잡함

19. Redis에 저장된 수많은 사용자 조회 기록 키(view:song:{id}:user:{userId})로 인해 메모리가 부족해질 경우 어떻게 대응하시겠어요?

대응 전략:

  • TTL 설정을 통한 자동 정리
    → 1시간 후 자동 삭제되어 메모리 점유 지속적으로 해소

  • Redis의 메모리 정책 활용
    maxmemory-policyvolatile-ttl 등으로 설정하면 TTL이 있는 키 위주로 제거됨

  • Key Prefix 제한
    view:song:*:user:* 키 수가 너무 많아질 경우, 인기 콘텐츠에만 제한 적용하거나 샘플링 적용 가능

  • LRU 캐시 정책 + 모니터링
    → Redis 메모리 상태를 실시간으로 확인하고, Keyspace notification 등을 활용한 자동 감시

20. 매일 자정에 조회수 캐시를 초기화하는 이유는 무엇인가요? 왜 다른 시간대가 아니라 자정인가요?

  • 자정(00:00)은 일일 통계 기준의 시작점으로, 대부분의 로그, 마케팅 분석, 통계 집계 기준 시점과 일치합니다.

  • 자정을 기준으로 초기화하면 일별 조회수, 일일 인기 콘텐츠 순위 등을 계산할 때 기준이 명확해지고, UI에 노출되는 데이터도 사용자에게 익숙한 기준이 됩니다.

21. 스케줄링은 어떤 방식으로 구현하셨나요? Spring 기반이라면 어떤 기능을 사용했는지 설명해주세요.

  • Spring Boot의 @Scheduled 애노테이션과 cron 표현식을 사용했습니다.

  • 실행 주기를 설정하고, 조회수 관련 Redis 키 삭제 로직을 메서드로 분리하여 명확하게 관리했습니다.

@Scheduled(cron = "0 0 0 * * *")
public void clearViewCountCache() {
    // Redis 키 삭제 로직
}

@EnableScheduling 설정이 누락되면 스케줄링이 동작하지 않기 때문에 반드시 Application 클래스에 설정이 필요합니다.

22. Cron 표현식으로 스케줄링을 설정했다고 했는데, 사용한 표현식은 무엇인가요?

사용한 표현식: 0 0 0 * * *

  • 매일 자정 0시 0분 0초에 실행
  • 형식: 초 분 시 일 월 요일

크론 표현식의 시간 기준은 JVM이 실행 중인 서버의 로컬 타임존을 따르기 때문에, 시간대 문제가 발생하지 않도록 설정 확인이 필요합니다.

23. 만약 자정에 스케줄러가 실패하거나 Redis와의 연결이 일시적으로 끊긴다면 어떻게 대응하나요?

대응 방안:
1. 실패 로깅 및 모니터링

  • 삭제 작업의 시작/성공/실패 로그를 남기고, 오류 발생 시 Slack, 이메일 등 알림 연동
  1. 재시도 전략
  • @Scheduled(fixedDelay = X) 등으로 일정 간격으로 재시도하거나, 실패 시 별도 알람이 울린 후 수동 실행 가능
  1. 백업/상태 플래그 사용
  • Redis에 view:reset:YYYYMMDD 같은 키를 남겨, 하루에 한 번만 초기화되도록 제어

Redis가 다운된 경우를 대비해 삭제 로직은 반드시 예외 처리를 감싸고, 실행 실패 여부를 기록해야 추후 대응이 쉬워집니다.

24. 조회수 관련 키를 삭제할 때 key pattern이 view:song:*라고 하셨는데, 이 방식의 단점은 없을까요?

주요 단점:

  • KEYS view:song:*는 Redis 전체 키 공간을 탐색하기 때문에 대량의 키가 존재할 경우 성능 저하 및 블로킹 발생
  • Redis가 운영 중일 때는 KEYS 명령 사용은 권장되지 않습니다

운영 환경에서는 SCAN 명령으로 패턴 탐색 후 batch로 삭제하는 방식(SCAN + DEL)을 사용해야 성능과 안정성을 확보할 수 있습니다.

25. Redis에서 대량의 키 삭제를 수행할 경우 성능 문제나 블로킹 이슈는 없었나요?

  • 단순한 KEYS + DEL 조합은 데이터가 많을수록 Redis 블로킹 현상이 발생할 수 있어 위험합니다.
  • 실제 운영에선 SCAN 명령어를 반복 호출하여 배치로 삭제하거나, 비동기 삭제(SCAN + UNLINK) 방식으로 전환했습니다.

Redis 4.0+에서는 UNLINK 명령어를 활용하면 키를 비동기적으로 삭제할 수 있어 성능 저하를 줄일 수 있습니다.

26. 조회수 초기화를 통해 일일 인기 순위 재산정이 가능하다고 했는데, 이 기준은 어디에 저장되고 어떻게 계산되나요?

  • 초기화 이전에 ZSet 기반 인기 검색어(popular_keywords)나 조회수(view:song:{id}) 데이터를 별도 Redis 키 혹은 RDB에 저장합니다.
  • 이후 초기화된 다음 날에는 새로 기록된 데이터로 다시 집계하여 Top-N을 추출합니다.

하루 단위로 집계된 데이터를 RDB에 저장해두면 장기 통계, 주간/월간 랭킹 분석 등에도 활용 가능합니다.

0개의 댓글