C
Database/고급/Lesson 06

DB 고급 — 복제·샤딩·캐시·커넥션풀·파티셔닝

45분·theory

DB 고급 — 복제·샤딩·캐시·커넥션풀·파티셔닝

🎯 이 lesson 을 읽고 나면

이 lesson 을 다 읽고 나면 아래 3가지를 자신 있게 할 수 있습니다.

  • ✅ 파티셔닝 Range / Hash / List 선택 기준
  • ✅ 샤딩 vs 파티셔닝 차이
  • ✅ DB 백업 전략 (Logical vs Physical) 설명

학습 목표를 체크리스트로 두고 다 답할 수 있게 되면 lesson 을 닫으세요.

복제 (Replication) — *DB 를 여러 대로 늘리기*

복제가 뭐냐

웹 서비스가 잘 되면 읽기 요청 이 폭발합니다. 글 1개를 1만 명이 읽는 패턴이 흔하죠. DB 한 대가 모두 감당 못 하면 어떻게 할까?

복제 (Replication) = DB 를 여러 대로 늘려서 부담을 나누는 방법. 한 대를 Primary (쓰기 전용) 로 두고, 여러 대의 Replica (읽기 전용) 가 Primary 의 변경 사항을 복사 받습니다.

code
   쓰기            읽기 요청들
     │              │  │  │
     ▼              ▼  ▼  ▼
  ┌──────┐      ┌──────┐ ┌──────┐ ┌──────┐
  │Primary├─────►Replica1│ │R 2  │ │R 3  │
  └──────┘ WAL  └──────┘ └──────┘ └──────┘
            전송

동기 vs 비동기 복제

동기 복제 — Primary 가 Replica 의 응답을 기다린 후 사용자에게 성공 응답. 데이터 손실 0, 하지만 느림.

비동기 복제 — Primary 는 바로 응답하고, Replica 는 나중에 복사. 빠르지만 Replica 가 약간 뒤처짐 (lag). Primary 가 갑자기 죽으면 최근 데이터 손실 가능.

대부분의 서비스는 비동기 를 씁니다. 99% 의 경우 lag 가 밀리초 단위 라 문제 없고, 성능 이득이 크기 때문입니다.

Lag — 가장 중요한 모니터링 지표

Replica 가 얼마나 뒤처져 있는가 가 핵심입니다. lag 가 1초 → 사용자가 방금 쓴 글이 안 보이는 사고가 납니다.

sql
-- PostgreSQL — primary 에서
SELECT client_addr, state,
       pg_wal_lsn_diff(sent_lsn, replay_lsn) AS lag_bytes
FROM pg_stat_replication;

lag 가 점점 커지면 → 네트워크·디스크·CPU 어딘가 병목. 알람 설정 필수.

애플리케이션 측 — 읽기·쓰기 분리

code
WRITE → Primary
READ  → Replica (load-balanced)

대부분 ORM·DataSource 라우터가 지원합니다. 읽기만 하는 쿼리 는 자동으로 Replica 로, 쓰기 는 Primary 로.

> ⚠️ 함정: 쓰기 직후 읽기 시 lag 때문에 옛 데이터 가 보일 수 있음. 그런 경우엔 해당 요청만 Primary 에서 읽기 같은 예외 처리 필요.

한 번 정리

복제 = 읽기 확장 + 재해 복구 의 표준 방법. 비동기가 기본이고, lag 모니터링 이 운영의 핵심. 쓰기·읽기 분리로 Primary 부담 을 크게 줄일 수 있습니다.

샤딩 (Sharding) — *데이터를 쪼개기*

샤딩이 뭐냐

복제는 읽기 를 늘리지만 쓰기는 한 대만 합니다. 쓰기도 폭발하면? — Twitter 처럼 초당 수십만 트윗 이 들어오면 한 대로는 불가능.

샤딩 (Sharding) = 데이터를 여러 DB 에 나누는 방법. 사용자 ID 가 1~100만은 DB1, 100만~200만은 DB2 — 이런 식으로 수평 분할.

code
                ┌────────────┐
   사용자 42 →   │  Shard 1   │  (1~100만)
                ├────────────┤
   사용자 200만 → │ Shard 2   │  (100만~200만)
                ├────────────┤
   사용자 500만 → │ Shard 3   │  (200만~)
                └────────────┘

각 shard 가 독립된 DB 라 쓰기 부담이 N등분 됩니다. 단점은 복잡성 폭증.

샤드 키 — 가장 중요한 결정

어떤 컬럼을 기준으로 나눌지가 운명을 가릅니다.

좋은 샤드 키:

  • user_id — 사용자별 균등 분배, cross-shard 쿼리 적음
  • tenant_id (B2B SaaS) — 회사별 분리

나쁜 샤드 키:

  • created_at최근 데이터로 hot spot. 옛 shard 는 노는데 새 shard 는 폭발.
  • auto_increment id — Range 샤딩 시 hot spot 동일.

샤드 키는 나중에 바꾸기 거의 불가능 합니다. 처음에 신중하게 결정해야 합니다.

Cross-shard 쿼리 — 가장 큰 함정

sql
-- 사용자 A 가 친구 B 에게 메시지 보내기
-- A 는 shard 1, B 는 shard 2 라면?
SELECT * FROM messages WHERE sender = A AND receiver = B;

두 shard 모두 조회 해야 합니다. JOIN 도 불가능. 트랜잭션도 분산 트랜잭션 으로 가는 순간 매우 비싸집니다.

그래서 cross-shard 작업이 적도록 데이터 모델·샤드 키를 설계하는 게 핵심입니다.

Resharding — 운영 악몽

데이터가 늘어나서 shard 를 5개 → 10개 로 늘려야 하면? 모든 데이터를 재분배 해야 합니다. 그 동안 서비스 영향 없이 진행하기 극도로 어렵습니다.

그래서 처음에 충분히 많은 shard 로 시작하거나 (예: 64개로 시작해서 노드에 분산), consistent hashing 같은 기법으로 부담을 줄이는 게 일반적.

매니지드 옵션 — 직접 안 하는 게 답

대부분의 회사는 샤딩을 직접 구현 안 합니다. 너무 어렵고 위험합니다.

  • DynamoDB·MongoDB Atlas — 자동 샤딩 NoSQL
  • Vitess — MySQL 샤딩 (YouTube 가 만들고 CNCF 졸업)
  • Citus — PostgreSQL 확장 (Microsoft)

특별한 이유 없으면 복제로 충분 합니다. 샤딩이 진짜 필요한 시점은 수억 행·초당 수만 쓰기 수준입니다.

한 번 정리

샤딩은 최후의 수단. 복제 → 캐시 → 파티셔닝을 다 시도해보고도 부족할 때만. 시작할 때 샤드 키 결정 이 운명을 가르고, cross-shard 쿼리 회피 가 성능의 열쇠입니다.

캐시 + Connection Pool — *DB 부담을 줄이는 두 가지*

캐시가 뭐냐

DB 쿼리는 느립니다 (밀리초). 메모리는 빠릅니다 (마이크로초). 같은 쿼리를 반복해서 실행한다면 결과를 메모리에 저장 해두고 재사용 하면 1000배 빠릅니다. 이게 캐시의 기본 아이디어입니다.

Redis 가 사실상 표준. 메모리 기반 key-value 저장소. 매우 빠르고, TTL (자동 만료)·pub/sub 같은 부가 기능까지.

Cache-Aside 패턴

가장 흔한 패턴:

code
1. 캐시 조회
2-A. hit → 즉시 반환 (빠름)
2-B. miss → DB 조회 → 캐시 저장 → 반환

쓰기 시: DB 업데이트 + 캐시 무효화 (또는 갱신)
python
def get_user(id):
    cached = redis.get(f"user:{id}")
    if cached:
        return cached         # 캐시 hit (μs)

    user = db.query("SELECT * FROM users WHERE id=?", id)  # ms
    redis.setex(f"user:{id}", 300, user)   # 5분 TTL
    return user

5분 동안은 DB 안 거치고 Redis 에서 즉시 응답. DB 부담이 극적으로 줄어듭니다.

캐시 무효화 — 컴퓨터과학 양대 난제 중 하나

> "There are only two hard things in Computer Science: cache invalidation and naming things." — Phil Karlton

데이터가 변경되면 옛 캐시 를 어떻게 처리할까?

1. TTL — 일정 시간 후 자동 만료. 가장 단순하지만 최대 N초 동안 옛 데이터 가능.

2. 명시적 무효화 — 데이터 변경 시 redis.del('user:42'). 즉시 반영. 하지만 모든 변경 코드에서 잊지 말고 호출해야.

3. 이벤트 기반 — DB 변경 → 이벤트 발행 → 캐시 구독자가 무효화. 분산 시스템에서 유리.

4. 버전 키user:v2:42 처럼 키 자체를 바꾸기. 옛 키는 자연 폐기. TTL 과 함께 쓰면 깔끔.

> 💡 현실: TTL + 명시적 무효화 조합이 90%. 완벽한 일관성 은 비싸고 어렵습니다. 짧은 stale period 를 허용하는 게 보통.

Connection Pool — DB 연결의 재활용

DB 연결을 매번 새로 만들면 비쌉니다. TCP 핸드셰이크·인증·TLS 협상이 매 요청마다 일어나죠. 50ms 정도가 연결만으로 소모됩니다.

Connection Pool = 연결 N 개를 미리 만들어 두고 재사용 하는 풀입니다. 요청이 오면 풀에서 빌려서 쓰고, 끝나면 반납. 새 연결 비용이 사라집니다.

code
┌──── Connection Pool ────┐
│  [conn1][conn2]...[connN]│
└──┬──┬──┬─────────────────┘
   │  │  │
   ▼  ▼  ▼
  요청 처리 후 반납

Pool 설정의 핵심

가장 중요한 것은 Pool 크기. 너무 작으면 대기, 너무 크면 DB 부담.

공식: app servers × pool size ≤ DB max_connections

예: app 4대 × pool 20 = 80 ≤ Postgres max=100 ✅

HikariCP (Java 표준) 같은 라이브러리가 똑똑하게 관리합니다. min = max 권장 (예측 가능한 동작).

흔한 함정

1. 연결 누수conn.close() 잊으면 풀 고갈. try-with-resources·context manager 필수.

2. 너무 큰 PoolDB max_connections 초과. "too many connections" 에러로 폭발.

3. 너무 짧은 idle timeout — 방화벽이나 LB 가 idle 연결을 끊는 환경에서 자주 새 연결. max-lifetime 으로 정기적으로 교체 권장 (30분 정도).

한 번 정리

캐시 + Connection Pool 은 DB 부담을 줄이는 두 표준 도구. 캐시는 반복 쿼리, Pool 은 연결 비용 을 줄입니다. 둘 다 제대로 설정 하면 같은 DB 에서 10배 더 많은 트래픽 을 받을 수 있습니다.

파티셔닝 — *큰 테이블을 작은 조각으로*

파티셔닝이 뭐냐

테이블 하나가 100GB 이상으로 커지면 여러 문제가 생깁니다. 인덱스도 거대해지고, 옛 데이터 삭제 가 느리고, 일부 데이터만 쓰는 쿼리도 전체 를 훑게 됩니다.

파티셔닝 (Partitioning) = 큰 테이블을 논리적으로 여러 조각 으로 나누되, 사용자에겐 하나로 보이게 하는 기술. 샤딩과 비슷하지만 같은 DB 안에서 분할이라는 점이 다릅니다.

RANGE 파티션 — 시계열에 최적

가장 흔한 패턴. 날짜 범위 로 나눕니다.

sql
CREATE TABLE logs (
    id BIGSERIAL,
    created_at TIMESTAMP NOT NULL,
    data JSONB
) PARTITION BY RANGE (created_at);

CREATE TABLE logs_2025_01 PARTITION OF logs
    FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');

CREATE TABLE logs_2025_02 PARTITION OF logs
    FOR VALUES FROM ('2025-02-01') TO ('2025-03-01');

INSERT INTO logs 하면 created_at 값을 보고 자동으로 맞는 파티션 에 저장됩니다.

진짜 이득 — Partition Pruning

sql
SELECT * FROM logs WHERE created_at >= '2025-03-01';

이 쿼리는 logs_2025_03 파티션만 스캔합니다. 다른 파티션은 건너뜁니다. 100GB 중 1GB 만 보면 되니까 100배 빠르죠.

DB 가 알아서 어느 파티션 을 봐야 할지 추론합니다. 이게 Pruning (가지치기).

옛 데이터 삭제가 빠르다

3년 지난 로그를 지운다고 해봅시다.

sql
-- ❌ 매우 느림 (수 시간)
DELETE FROM logs WHERE created_at < '2022-01-01';

-- ✅ 즉시
DROP TABLE logs_2021_12;
DROP TABLE logs_2021_11;
-- ...

파티션 단위 DROP 은 디스크에서 통째로 떼어내기 라 매우 빠릅니다. 로그·이벤트 처리에서 압도적으로 좋습니다.

자동화 — pg_partman

매월 새 파티션을 수동으로 만드는 건 운영 부담입니다. pg_partman (Postgres 확장) 이 자동 으로 처리해 줍니다.

sql
SELECT partman.create_parent(
    p_parent_table => 'public.logs',
    p_control => 'created_at',
    p_type => 'native',
    p_interval => 'monthly',
    p_premake => 3   -- 3개월 분 미리 생성
);

TimescaleDB 는 PostgreSQL 위의 시계열 특화 확장. 파티셔닝을 자동으로 관리하고 압축 까지.

LIST·HASH 파티션

LIST — 지역·카테고리 같은 명확한 분류:

sql
CREATE TABLE users (...) PARTITION BY LIST (country);
CREATE TABLE users_kr PARTITION OF users FOR VALUES IN ('KR');
CREATE TABLE users_jp PARTITION OF users FOR VALUES IN ('JP');

HASH — 균등 분배 (Postgres 11+):

sql
CREATE TABLE orders (...) PARTITION BY HASH (user_id);
CREATE TABLE orders_p0 PARTITION OF orders FOR VALUES WITH (modulus 4, remainder 0);
-- ...

언제 파티셔닝?

  • ✅ 단일 테이블 100GB+
  • 시간 기반 데이터 (로그·메트릭·이벤트)
  • 옛 데이터 일괄 삭제 필요
  • ❌ 작은 테이블 — 오버헤드만 ↑

한 번 정리

파티셔닝은 큰 테이블 의 성능·운영 부담을 극적으로 줄입니다. 시계열 데이터엔 기본 옵션. pg_partman·TimescaleDB 로 자동화하면 운영도 편합니다. 샤딩보다 훨씬 쉽고 안전한 옵션이니, 먼저 고려할 가치 있습니다.

💾 실전 데모 — 캐시 hit vs miss 성능 차이

같은 데이터 조회를 *DB 직접* vs *Redis 캐시* 비교.
📥 입력 DB 서버로 전송되는 쿼리
SELECT FROM: users length: 262 chars
-- 측정 결과 (실제 운영 환경 평균)

-- DB 직접 조회 (인덱스 사용)
SELECT * FROM users WHERE id = 42;
-- 평균: 8 ms

-- Redis 캐시 hit
-- GET user:42
-- 평균: 0.3 ms

-- 1000 회 반복:
-- DB only:    8 ms × 1000 = 8,000 ms (8초)
-- Cache+DB:   첫 회 8ms + 999회 × 0.3ms = 308 ms (0.3초)
-- → 26 배 빠름
⚙️ 처리 단계 DB 엔진 내부 동작
📋
1. 파싱
SQL 문법 분석 → AST 생성
10ms
🧠
2. 옵티마이저
인덱스·조인 순서 최적화 (통계 기반)
25ms
⚙️
3. 실행
Storage Engine이 데이터 읽기
24ms
📊
4. 결과 반환
버퍼에 모아서 클라이언트에 전달
8ms
총 67ms
📤 출력
✓ 3 rows 3 cols 67ms
sourceavg_ms1000회
DB (인덱스)88,000 ms
Redis hit0.3300 ms
1회 miss + 999회 hit-308 ms
💡 학습 포인트: 실제 DB는 파싱 → 옵티마이저 → 실행 → 반환 4단계를 거칩니다. 인덱스가 있으면 '옵티마이저' 단계가 수천배 빨라집니다.
🌐 본인 환경(PostgreSQL·MySQL 등)에서 직접 실행해 ms 차이를 체감해보세요.
💻 📌 캐시·풀·파티션 설정 (참고용)
# ============================================
# 1) HikariCP — Spring 표준 Connection Pool
# ============================================
spring:
  datasource:
    hikari:
      maximum-pool-size: 20         # CPU·DB max 기반
      minimum-idle: 20               # = max 권장 (예측 가능)
      connection-timeout: 30000      # 30초
      idle-timeout: 600000           # 10분
      max-lifetime: 1800000          # 30분 (DB·LB idle 끊김 회피)
      leak-detection-threshold: 60000 # 60초 안 닫힌 연결 경고

# ============================================
# 2) Redis (Spring Boot)
# ============================================
spring:
  data:
    redis:
      host: localhost
      port: 6379
      timeout: 2000
      lettuce:
        pool:
          max-active: 8
          max-idle: 8
          min-idle: 0

# 코드:
# @Cacheable(value = "user", key = "#id")
# public User findById(Long id) { return userRepo.findById(id); }
#
# @CacheEvict(value = "user", key = "#user.id")
# public void update(User user) { userRepo.save(user); }

# ============================================
# 3) PostgreSQL 파티션 + pg_partman
# ============================================
-- 확장 설치
CREATE EXTENSION pg_partman;

-- 부모 테이블 + 파티셔닝
CREATE TABLE logs (
    id BIGSERIAL,
    created_at TIMESTAMP NOT NULL,
    level VARCHAR(10),
    message TEXT
) PARTITION BY RANGE (created_at);

-- 자동 관리
SELECT partman.create_parent(
    p_parent_table => 'public.logs',
    p_control => 'created_at',
    p_type => 'native',
    p_interval => 'monthly',
    p_premake => 4
);

-- 30일 지난 파티션 자동 삭제 (cron 으로 매일)
-- SELECT partman.run_maintenance();

🤖 AI 에게 이렇게 요청해보세요

이 lesson 의 개념을 알면 AI 에게 구체적으로 지시할 수 있습니다. 막연한 "고쳐줘" 가 아니라 어휘를 가진 요청 — 그게 토큰 절약의 출발점입니다.

  • "이 logs 테이블을 created_at YEAR Range 파티션으로 변환해줘"
  • "이 SELECT 쿼리를 필요한 컬럼만 선택해서 커버링 인덱스가 동작하게 해줘"*
  • "샤딩 vs 파티셔닝 차이 + 우리 케이스에 뭘 골라야 할지 진단해줘"

왜 이게 토큰을 줄이나

개념을 모를 땐 AI 답변을 받고도 "그게 뭐예요?" 를 다시 물어야 합니다. 그 "다시 물음" 이 토큰을 잡아먹습니다. 개념 한 번 익혀두면 대화가 한 번에 끝납니다.

먼저 읽으면 좋은 개념: NoSQL 기초 — Redis · MongoDB 언제 쓰나
다음 추천: HTML/CSS 기초
DB 고급 — 복제·샤딩·캐시·커넥션풀·파티셔닝 - Database