대규모 트래픽을 견디는 데이터 구조 — 인덱스와 캐싱 전략(5)





대규모 트래픽을 견디는 데이터 구조 — 인덱스와 캐싱 전략 | 실무형 성능 최적화 완벽 가이드


대규모 트래픽을 견디는 데이터 구조 — 인덱스와 캐싱 전략

백엔드 개발에서 트래픽이 커질수록 드러나는 한계는 코드가 아니라 데이터베이스(DB)입니다.
서버 리소스를 아무리 늘려도, 쿼리 한 줄이 비효율적이면 전체 시스템은 병목을 피할 수 없습니다.
그 병목을 해결하는 가장 강력한 두 가지 도구가 바로 인덱스(Index)캐시(Cache)입니다.
이 글은 단순히 이론을 나열하는 것이 아니라, 실무에서 DB 성능을 직접 튜닝하면서 얻은 경험을 기반으로 정리한 “살아있는 가이드”입니다.

핵심 키워드: 대규모 트래픽, 데이터베이스 병목, 인덱스 최적화, 캐싱 전략, Redis, MySQL, 실무 성능 튜닝

1. 트래픽 폭증의 현실 — DB는 어떻게 무너지는가

운영 초기의 서비스는 단일 DB, 단순 쿼리로도 충분합니다.
그러나 사용자가 늘어나면, 하루 수천 건의 요청이 수만, 수십만으로 커지며 “SELECT 쿼리”가 폭증합니다.
이때 DB CPU 사용률이 급상승하고, 특정 테이블의 풀 스캔(Full Table Scan)이 빈번하게 발생합니다.
풀 스캔은 말 그대로 모든 데이터를 훑는 연산으로, 수십만 건 중 일부만 필요한 경우에도 전부 탐색합니다.
이때의 지연이 누적되면, 서버의 응답이 1초 이상으로 늘어나고, 결국 API 타임아웃이 발생합니다.

실제 사례: 주문 내역 조회의 병목

한 쇼핑몰 서비스에서 다음과 같은 쿼리가 있었습니다.

SELECT * FROM orders
WHERE user_id = 1234
ORDER BY created_at DESC
LIMIT 20;

문제는 user_id에 인덱스가 없었다는 것입니다.
결과적으로 200만 개의 주문 데이터 중 해당 사용자의 20건을 찾기 위해 모든 레코드를 스캔했습니다.
단 0.3초의 지연처럼 보이지만, 초당 수백 번 호출되면 서버는 순식간에 마비됩니다.
해결책은 단순했습니다 — INDEX (user_id, created_at DESC) 추가.
그 한 줄로 쿼리 속도가 0.3초 → 0.01초로 단축되었습니다.

2. 인덱스(Index)란 무엇인가?

인덱스는 데이터베이스가 데이터를 빠르게 찾기 위한 검색용 목차입니다.
일반적으로 MySQL과 PostgreSQL은 B-Tree 구조를 사용합니다.
이는 균형 트리(Balanced Tree)로, 루트 → 중간 노드 → 리프 노드를 따라가며 키 값 기준으로 탐색합니다.
인덱스가 없다면 DB는 모든 레코드를 확인해야 하지만, 인덱스는 정렬된 키 구조를 통해
O(log n) 시간에 원하는 데이터를 찾을 수 있습니다.

B-Tree 인덱스 구조의 원리

  • 루트 노드에 정렬된 키 범위가 저장됩니다.
  • 중간 노드에서 분기하며 하위 리프 노드로 탐색합니다.
  • 리프 노드에는 실제 데이터(혹은 데이터 위치 포인터)가 저장됩니다.

즉, 100만 건 중 원하는 값을 찾을 때 최대 20번 이하의 비교 연산으로 결과를 반환할 수 있습니다.

3. 인덱스 설계의 실무 원칙

인덱스를 잘못 만들면 오히려 성능을 떨어뜨릴 수 있습니다.
아래는 실무에서 반드시 지켜야 하는 원칙들입니다.

  • 조회 빈도 높은 컬럼에만 생성 — 자주 쓰이지 않는 인덱스는 오히려 쓰기 부하를 증가시킵니다.
  • 카디널리티(값의 다양성)가 높은 컬럼 우선 — 예를 들어 “성별”보다 “이메일”이 효율적입니다.
  • 복합 인덱스는 순서가 중요 — 쿼리 패턴의 WHERE 절 순서와 동일하게 구성해야 합니다.
  • 인덱스는 조인 키에 반드시 추가 — JOIN 시 두 테이블 모두 키 컬럼이 인덱스화되어야 합니다.
  • 커버링 인덱스(covering index)를 활용 — 쿼리에서 필요한 모든 컬럼이 인덱스에 포함되면 테이블 접근이 불필요합니다.

예시:

CREATE INDEX idx_user_created
ON orders (user_id, created_at DESC);

이 인덱스는 특정 사용자의 최신 주문을 빠르게 조회하도록 최적화되어 있습니다.
그러나 created_at 단독 검색에는 효과가 거의 없기 때문에, 인덱스 설계는 “쿼리 패턴 중심”으로 해야 합니다.

4. 캐싱(Cache) — DB의 부담을 줄이는 또 다른 축

인덱스가 “DB 내부의 최적화”라면, 캐시는 “DB 외부의 완충 장치”입니다.
캐시는 자주 요청되는 데이터를 메모리(RAM)에 저장하여,
매번 DB를 조회하지 않고 빠르게 응답할 수 있게 해줍니다.
대표적으로 Redis, Memcached가 사용되며, 이 둘은 읽기 중심 시스템에서 트래픽 절감 효과가 탁월합니다.

실무 예시: Redis로 인기 게시글 캐싱

const key = 'posts:popular';
let posts = await redis.get(key);

if (!posts) {
  posts = await Post.findAll({ order: [['likes', 'DESC']], limit: 10 });
  await redis.set(key, JSON.stringify(posts), 'EX', 60); // TTL 60초
  console.log('캐시 미스 → DB 조회 후 캐싱');
} else {
  console.log('캐시 히트 → Redis에서 바로 응답');
}

return JSON.parse(posts);

이 구조를 사용하면 초당 수천 번 호출되는 인기글 API에서도 DB 부하가 거의 발생하지 않습니다.
DB는 1분에 한 번만 조회되고, 나머지는 모두 Redis가 응답합니다.
단, TTL(Time To Live)을 적절히 설정해 오래된 데이터를 주기적으로 갱신해야 합니다.

5. 캐싱 전략의 패턴과 주의점

  • Read-Through — 캐시에 없을 때만 DB 조회 후 저장
  • Write-Through — DB 수정 시 캐시도 즉시 갱신
  • Write-Behind — 캐시 갱신 후 비동기로 DB 반영
  • TTL + LRU 정책 — 일정 시간 지나면 자동 만료, 오래된 항목은 제거

캐시의 가장 큰 문제는 “일관성(Consistency)”입니다.
DB 데이터가 변경되었는데 캐시가 갱신되지 않으면, 사용자는 오래된 정보를 보게 됩니다.
이를 막기 위해 캐시 무효화 정책(invalidation policy)을 설계해야 합니다.
예를 들어 게시글 수정 시 redis.del('posts:popular')로 관련 캐시를 강제로 제거합니다.

6. 인덱스와 캐시의 조합 전략

인덱스는 DB 내부 최적화, 캐시는 외부 요청 완화.
이 두 가지를 함께 사용하면 트래픽이 수십 배 증가해도 안정적으로 버틸 수 있습니다.
실무에서는 인덱스로 쿼리 효율을 올리고, 캐시로 요청 빈도를 줄이는 것이 기본 전략입니다.

실무 팁:
1️⃣ 캐시는 무조건 빠르지만, 데이터 일관성 관리가 어렵습니다.
2️⃣ 인덱스는 항상 EXPLAIN으로 실제 사용 여부를 검증하세요.
3️⃣ Redis는 단순 캐시 외에도 세션 스토리지, 메시지 큐, Pub/Sub으로 활용 가능합니다.
4️⃣ 쿼리 튜닝은 캐시보다 먼저 — 불필요한 쿼리를 캐시하는 것은 기술 부채입니다.

7. 결론 — 성능은 설계에서 시작된다

시스템의 속도는 코드가 아니라 데이터 구조의 이해도에서 결정됩니다.
데이터를 어떻게 저장하고, 어떻게 접근할지를 고민한 개발자는
트래픽이 10배, 100배가 되어도 시스템을 지켜낼 수 있습니다.
인덱스와 캐시는 단순한 기술이 아니라, 확장성과 안정성을 담보하는 설계 전략입니다.

다음 편에서는 “데이터를 연결하는 힘 — API 설계와 GraphQL”을 통해
효율적으로 데이터를 전달하고, 불필요한 요청을 줄이는 백엔드 구조 설계를 다루겠습니다.


댓글 남기기