이커머스 API 성능 개선기 (1편) - Redis 캐싱 전략으로 응답 시간 94% 단축하기

H_M·2025년 12월 16일

성능 개선기

목록 보기
1/2

들어가며

서비스 트래픽이 증가하면서 홈 페이지 API의 응답 속도가 점차 느려지기 시작했습니다. 사용자들이 가장 먼저 접하는 홈 화면의 로딩이 느려지면서, 이는 곧 전체 서비스 품질에 대한 인상으로 이어졌습니다. 이번 글에서는 최대 500ms 이상 소요되던 홈 상품 조회 API를 30ms로 단축시킨 과정을 공유하고자 합니다.

문제 상황

홈 화면에서 12개의 상품을 조회하는 API의 응답 시간이 최대 500ms 이상까지 소요되고 있었습니다. 특히 이벤트 등으로 트래픽이 증가하는 시간대에는 서버 리소스가 제한적인 상황에서 응답 시간이 더욱 늘어났고, 이 API가 시스템의 주요 병목 지점이 되었습니다.

서비스 특징: 멀티 국가 환경

현재 회사 서비스는 국가별로 독립된 이커머스 페이지를 운영하고 있습니다. 각 국가의 사용자는 해당 국가에 맞는 상품, 가격, 재고 정보를 조회하며, 국가마다 노출되는 상품 구성도 다릅니다. 이러한 멀티 국가 환경은 캐싱 전략 설계에 중요한 고려사항이 되었습니다.

원인 분석

응답 지연의 원인을 분석한 결과, 다음과 같은 문제점들이 발견되었습니다

  1. 복잡한 데이터베이스 쿼리: 5개 테이블을 조인하는 복잡한 쿼리 구조
  2. 매 요청마다 반복되는 연산: Django Serializer에서 동일한 데이터를 매번 재계산
  3. 국가별 조회 부하: 각 국가마다 동일한 쿼리가 반복 실행됨
  4. 확장성 문제: 사용자 수 증가에 따라 DB 부하가 선형적으로 증가

해결 전략

Redis 캐싱 도입

즉각적인 장애 해결을 위해 Redis 기반 캐싱 전략을 선택했습니다. 캐싱은 단기 해결책이지만, 빠르게 적용할 수 있고 효과가 즉각적이라는 장점이 있었습니다.

캐싱 전략 설계의 핵심 질문

캐싱을 도입하면서 가장 중요하게 고민한 질문들은 다음과 같습니다:
1. "언제 캐시를 무효화해야 하는가?" - 잘못된 무효화 전략은 데이터 정합성 문제를 일으킴
2. "국가별로 어떻게 캐시를 관리할 것인가?" - 각 국가마다 다른 데이터를 효율적으로 관리

멀티 국가 환경을 위한 캐시 키 설계

국가별로 독립된 캐시를 유지하면서도, 상품 업데이트 시 관련된 모든 국가의 캐시를 효율적으로 무효화할 수 있는 구조가 필요했습니다.

cache_key = f":iso_code"

# 태그 구조 - 모든 태그에는 ALL과 해당 국가 코드를 포함
cache_tags = ["ALL", country_code]

이렇게 설계하면:

  • 국가별 독립 캐싱: 태국 사용자와 베트남 사용자는 서로 다른 캐시를 조회
  • 선택적 무효화:
    • 특정 국가만 무효화: "TH" 태그 삭제
    • 모든 국가 동시 무효화: "ALL" 태그 삭제
  • 유연한 확장: 새로운 국가 추가 시 코드 변경 없이 자동 지원

Redis 캐시 태그 기반 무효화 전략

상품 데이터는 다양한 경로로 업데이트될 수 있습니다. 상품 정보 수정, 재고 변경, 가격 변경, 노출 여부 변경 등 여러 이벤트가 발생할 때마다 관련된 캐시를 정확하게 무효화해야 합니다.

이를 위해 태그 기반 캐시 무효화 시스템을 구축했습니다.

홈 상품 API에 캐싱 적용

상품 업데이트 시 캐시 무효화

상품 정보가 변경될 때마다 관련 캐시를 명시적으로 무효화하도록 구현했습니다. 특히 멀티 국가 환경에서는 어떤 국가의 캐시를 무효화할지 결정하는 것이 중요합니다.
<샘플 코드 정리>

국가별 캐시 무효화 시나리오

시나리오 1: 특정 국가의 상품 정보만 변경

python# 태국 상품의 가격 변경 → 태국 캐시만 무효화
tags = ["ALL", "TH"]
CacheManager.invalidate_by_tags(tags)
# 결과: TH 캐시만 무효화됨 (CN, 기타 국가는 영향 없음)

시나리오 2: 모든 국가에 영향을 주는 변경

python# 상품 자체가 삭제되거나 전역 설정 변경 → 모든 국가 캐시 무효화
tags = ["ALL"]
CacheManager.invalidate_by_tags(tags)
# 결과: ALL 태그 삭제로 모든 국가(TH, CN 등)의 캐시가 무효화됨

시나리오 3: 여러 국가에 동시 영향

python# 태국과 중국의 상품들이 동시에 변경됨
tags = ["ALL", "TH", "CN"]
CacheManager.invalidate_by_tags(tags)
# 결과: TH와 CN의 캐시만 선택적으로 무효화

결과

캐싱 전략 도입 후 다음과 같은 성과를 거두었습니다:

  • 응답 시간: 최대 500ms → 30ms (94% 단축)
  • 트래픽이 증가하는 시간대에도 안정적인 응답 시간 유지

추가로 고려했던 전략

실제 프로젝트에서는 빠른 적용을 위해 최소한의 구현만 했지만, 상황에 따라 다음과 같은 전략들을 추가로 확인했었습니다.

1. Django Signal을 활용한 자동 캐시 무효화

현재는 Service 레이어에서 명시적으로 캐시를 무효화하지만, Django Signal을 활용하면 모델 변경 시 자동으로 캐시를 무효화할 수 있습니다.

# signals.py
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from django.db import transaction
from .models import Product
from .cache_utils import CacheManager

@receiver(post_save, sender=Product)
def invalidate_product_cache(sender, instance, **kwargs):
    """상품 생성/수정 시 캐시 무효화"""
    def invalidate():
        tags_to_invalidate = [
            "home_products",
            "products",
            f"product:{instance.id}",
            f"category:{instance.category_id}"
        ]
        CacheManager.invalidate_by_tags(tags_to_invalidate)
    
    # 트랜잭션 완료 후 무효화
    transaction.on_commit(invalidate)
  • 장점: 모델 변경이 발생하는 모든 경로에서 자동으로 캐시 무효화
  • 단점: Signal 관리 복잡도 증가, 디버깅 어려움

2. 캐시 워밍(Cache Warming) 전략

서버 재시작 후 첫 요청에서도 빠른 응답을 제공하기 위해 미리 캐시를 로드하는 전략입니다.

다만, 회사 서비스의 경우 워밍해야 할 데이터의 범위가 명확하지 않았고 트래픽 규모도 첫 요청이 느린 것이 큰 문제가 되지 않아 캐시 워밍을 적용하지 않았습니다.

고려해볼 상황:

  • 대규모 트래픽이 예상되는 경우
  • 첫 요청 응답 시간이 중요한 경우
  • 워밍할 데이터 범위가 명확한 경우

주의사항 및 한계

캐싱을 도입하면서 다음과 같은 주의사항과 한계점을 발견했습니다.

1. 데이터 정합성

명시적으로 캐시를 무효화하는 방식은 개발자가 무효화 시점을 정확히 파악해야 합니다. 특히 여러 곳에서 데이터를 수정하는 경우 무효화 로직을 누락할 위험이 있습니다.

2. 캐시 무효화 누락 위험

예상치 못한 업데이트 경로에서 캐시가 무효화되지 않을 수 있습니다. 이를 대비해 정기적인 캐시 갱신 스케줄러를 운영하는 것도 고려해볼 수 있습니다.

3. 모니터링의 부재

이번 캐싱은 임시 해결책이었기 때문에 별도의 모니터링 시스템을 구축하지 않았습니다. 하지만 장기 운영을 한다면 다음과 같은 지표들을 확인하며 잘 동작하고 있는지 확인이 필요합니다.

  • 캐시 히트율: 캐시가 실제로 효과를 발휘하고 있는지 확인
  • 평균 응답 시간: 성능 저하 조기 감지
  • 캐시 무효화 빈도: 무효화가 너무 자주 발생하면 캐시 효율 저하

4. 근본적 해결책은 아님

캐싱은 증상을 완화했을 뿐, 5개 테이블 조인이라는 구조적 문제는 여전히 존재합니다.

  • 캐시 미스 시에는 여전히 느린 응답 시간 발생
  • 서비스가 복잡해질수록 캐시 무효화 로직도 복잡해짐
  • 장기적으로는 CQRS 패턴 도입 필요 → 다음 편에서 계속

마치며

Redis 캐싱 전략 도입으로 홈 API의 응답 속도를 94% 단축시킬 수 있었습니다. 태그 기반 무효화 전략을 통해 데이터 정합성도 함께 확보했습니다.
하지만 캐싱은 어디까지나 단기 해결책입니다. 5개 테이블을 조인하는 구조적 문제는 여전히 남아있으며, 서비스가 더 복잡해질수록 캐시 무효화 로직도 복잡해질 것입니다.
다음 편에서는 CQRS 패턴과 Kafka 기반 이벤트 드리븐 아키텍처를 도입하여 이 문제를 근본적으로 해결한 과정을 공유하겠습니다. 반정규화된 조회 테이블 설계와 이벤트 기반 데이터 동기화 전략을 통해 어떻게 지속 가능한 고성능 구조를 만들었는지 소개할 예정입니다.

0개의 댓글