시니어 백엔드 면접 질문 1편 - 인프라/스케일링 (5~10년차)
5~10년차 시니어 백엔드 면접에서는 단순 개념보다 설계 판단력, 트러블슈팅 경험, 기술 선택의 근거를 봅니다.
이 시리즈에서는 제가 면접관이라면 어떻게 질문하고, 어떤 꼬리질문으로 진짜 실력을 확인할지 정리했습니다.
시리즈 구성:
- 1편 (현재): 인프라/스케일링 - 트래픽, DB, 분산시스템
- 2편: 운영/안정성 - 장애대응, 이벤트 아키텍처, 배포
- 3편: 설계/리더십 - 의사결정, 시스템디자인, 코드리뷰
1. 대용량 트래픽 처리
Q1. 갑자기 트래픽이 10배 증가하면 어떻게 대응하시겠어요?
기대 답변: 단기적으로는 오토스케일링으로 인스턴스 확장, 장기적으로는 병목 구간 분석 후 캐싱, 비동기 처리, DB 최적화 등을 적용합니다.
💡 실제 사례 보기
시나리오: 라이브 커머스에서 인플루언서가 상품을 소개한 직후 5분간 평소 트래픽의 12배가 몰림.
1차 진단 (APM):
- 웹 서버 CPU는 40% → 여유
- DB CPU 98% → 병목
- Connection Pool 100/100 만석, 대기 큐 폭증
대응 (우선순위):
- 상품 페이지 CDN 캐시 TTL을 5분 → 30초로 단축 + Cache-Control 헤더 강화 → 캐시 히트율 92%
- 상품 상세 조회 API를 Redis로 분리 (Cache-Aside, TTL 60초)
- Read Replica 2대 추가 (RDS Auto Scaling으로 자동)
- 읽기 트래픽이 Replica로 분산되며 마스터 CPU 30%로 안정화
결과:
- 응답 p99: 8초 → 220ms
- 5분 후 트래픽 감소하면서 자동 스케일 인
교훈:
- 트래픽이 읽기 중심이면 캐시 → Read Replica 순서가 가장 빠른 진화 수단
- 오토스케일은 인스턴스가 뜨는 데 1~2분이 걸려, 이미 캐시되어 있는 응답이 진짜 안전판
🔄 꼬리질문 1: 병목 구간은 어떻게 찾나요?
기대 답변:
- APM 도구(Datadog, New Relic, Pinpoint)로 트랜잭션별 응답시간 분석
- DB slow query 로그 분석
- CPU/메모리/네트워크 지표 모니터링
- 분산 트레이싱으로 서비스 간 호출 추적
📋 실제 사례
상황: 주문 API p99가 평소 200ms인데 하루에 몇 번씩 4초로 튀는 현상. CPU·메모리·DB 메트릭은 모두 정상이라 일주일째 원인 불명.
진단:
- Datadog APM에서 4초 이상 트랜잭션만 tail-based sampling으로 100% 수집
- Flame graph 분석 → 주문 API → 사용자 서비스 → 외부 휴면 회원 체크 API 구간이 99%는 30ms, 1%만 3.5초
- 외부 API가 특정 사용자 유형에서 timeout 직전까지 갔다가 응답하는 패턴
조치:
- 휴면 체크를 동기 호출 → 비동기 + 로컬 캐시(30분 TTL)로 전환
- 외부 API 자체에는 1초 timeout + 서킷 브레이커 적용
결과:
- p99: 4초 → 180ms, 미스터리 spike 완전 소멸
- 외부 API 장애 시에도 주문은 정상 (캐시 fallback)
교훈: 평균은 멀쩡한데 p99이 튀면 전체 트래픽의 0.1% 슬로우 경로를 의심해야 한다. 집계 메트릭이 아니라 개별 느린 요청의 trace를 봐야 잡힌다.
🔄 꼬리질문 2: 캐시를 도입한다면 어디에, 어떻게 적용하시겠어요?
기대 답변:
- 읽기 비율 높은 데이터: 사용자 프로필, 상품 정보 → Redis Cache-Aside 패턴
- API 응답 캐싱: CDN 또는 API Gateway 레벨
- 세션 저장소: 분산 환경에서 Redis 클러스터
- TTL 설정과 캐시 무효화 전략(Write-Through, Write-Behind)도 고려
📋 실제 사례
상황: 상품 카탈로그 API가 DB 직접 조회로 평균 QPS 12,000, DB CPU 70% 상시. 한 응답 안에 변동 주기가 전혀 다른 데이터가 섞여 있었음 (상품 설명·이미지는 거의 불변, 재고·가격은 실시간, 추천은 분 단위).
조치 — 데이터 변동 주기별 캐시 레이어 분리:
- 정적 정보(설명·이미지): CDN 24시간 TTL → origin 도달 자체를 차단
- 동적 정보(재고·가격): Redis Cache-Aside 30초 TTL
- 사용자 추천: Redis 5분 TTL
- 무효화는 Redis Pub/Sub로 모든 애플리케이션 인스턴스 동시 전파
결과:
- DB QPS: 12,000 → 800 (93% 감소)
- DB CPU: 70% → 20%
- CDN hit 70% / Redis hit 25% / DB 5%
교훈: 한 응답 안에서도 데이터마다 변동 주기가 다르면 캐시 레이어를 분리해야 한다. 전체에 단일 TTL을 걸면 가장 짧은 주기에 맞춰져 캐시 효율이 죽는다.
🔄 꼬리질문 3: 캐시 스탬피드(Cache Stampede)가 뭔지 알고 계시죠? 어떻게 방지하나요?
기대 답변: TTL 만료 시 동시에 수많은 요청이 DB로 몰리는 현상입니다.
- Lock: 한 요청만 DB 조회, 나머지는 대기
- Probabilistic Early Expiration: TTL 만료 전 확률적으로 미리 갱신
- Background Refresh: 별도 스레드가 주기적으로 갱신
📋 실제 사례
상황: 메인 페이지 인기 상품 캐시 TTL 5분. 정확히 5분마다 캐시가 만료되는 순간 수만 건 요청이 동시에 DB로 몰려 DB CPU가 100%로 튀고 추천 쿼리가 8초씩 걸림 (주기적 spike).
진단:
- Grafana에서 DB CPU 그래프가 정확히 5분 간격 톱니파
- 만료 순간 동일 키에 대한 DB 조회가 8,000건 동시 발생 (cache stampede)
조치 — Probabilistic Early Expiration:
- 캐시에 값 + 계산 소요시간(delta) 저장
- 만료 30초 전부터
now - delta * beta * log(random())공식으로 확률적으로 일부 요청만 미리 재계산 - 핫 키일수록 더 자주 미리 갱신되어 만료 시점이 분산됨
결과:
- DB CPU 톱니파 완전 소멸 (피크 100% → 평탄 35%)
- 추천 쿼리 p99: 8초 → 120ms
교훈: 핫 키는 명확한 만료 시점을 두면 그 순간 모든 요청이 몰린다. 만료 시점 자체를 확률적으로 분산시키는 게 lock보다 우아하다 (lock은 대기 시간이 생김).
2. 데이터베이스 확장
Q2. DB가 한계에 도달했을 때 어떻게 확장하시겠어요?
기대 답변:
- Scale-Up: 장비 스펙 업그레이드 (한계 있음)
- Read Replica: 읽기 분산
- Sharding: 데이터 수평 분할
- CQRS: 읽기/쓰기 모델 분리
💡 실제 사례 보기
시나리오: 게시글 테이블 5천만 건, 인덱스 포함 단일 인스턴스 60GB. 마스터 디스크 IOPS 한계 도달.
단계별 진화:
-
Read Replica 3대 도입 (1주차)
- 비즈니스 로직 75%가 읽기 — 마스터 부하 30%로 감소
- Replication lag 평균 80ms, 결제·주문은 마스터 강제 라우팅
-
그래도 쓰기 한계 — 샤딩 검토 (2개월차)
- 후보 키:
user_id,post_id,created_at - 선택:
user_id(해시 8-shard)- 한 사용자의 모든 게시글이 같은 샤드 → cross-shard 회피
- 카디널리티 충분, 분산 균등
- 가장 흔한 쿼리(
내 게시글 목록)가 단일 샤드에서 해결
- 후보 키:
-
마이그레이션 (3개월차)
- Dual-write → backfill → 검증 → traffic switch
- 다운타임 0초
결과:
- 쓰기 TPS: 800 → 5200
- 평균 latency: 120ms → 25ms
- 인스턴스 비용 1.4배 (8 shards × m5.large), 처리량은 6배
교훈:
- Scale-up < Read Replica < Sharding 순서로 단계 평가
- 샤딩 키는 가장 빈번한 쿼리 패턴에 정렬해야 cross-shard가 안 생김
🔄 꼬리질문 1: 샤딩 키는 어떤 기준으로 선택하나요?
기대 답변:
- Cardinality: 값의 종류가 충분히 많아야 함
- 분산 균등성: 특정 샤드에 데이터 쏠림 방지
- 쿼리 패턴: 가장 많이 조회하는 조건과 일치
- 예: 사용자 ID, 지역, 날짜 등
📋 실제 사례
상황: 채팅 메시지 테이블을 chatroom_id로 샤딩했는데, 1만 명 규모 대형 단톡방 하나가 특정 샤드에 모든 메시지를 몰아넣어 그 샤드만 디스크·CPU가 90%인 hotspot 발생.
진단:
- 8개 샤드 중 1개만 부하 90%, 나머지는 20% 미만
chatroom_id는 카디널리티는 충분했지만 분포가 극단적으로 치우침 (상위 0.1% 채팅방이 메시지의 60%)
조치 — 복합 파티션 키로 재설계:
chatroom_id단일 →(chatroom_id, day)복합 키- 대형 채팅방도 일자별로 여러 샤드에 분산
- 조회 패턴(
특정 방의 특정 날짜 메시지)과도 정합
결과:
- 샤드 부하 균등화 (최대 90% → 평균 35%)
- 단일 채팅방 쓰기 TPS 한계 1,200 → 9,000
교훈: 샤딩 키는 카디널리티만 보면 안 된다. 실제 데이터 분포(특히 상위 멱법칙 꼬리) 까지 측정해야 hotspot을 피한다.
🔄 꼬리질문 2: Cross-shard 쿼리가 필요하면 어떻게 하나요?
기대 답변:
- 최대한 피하도록 데이터 모델 설계
- 불가피하면 애플리케이션에서 병합 (Scatter-Gather)
- 집계 데이터는 별도 테이블/서비스로 비정규화
- Elasticsearch 같은 검색엔진 활용
📋 실제 사례
상황: user_id로 8-shard 분할한 후, 운영팀의 전체 사용자 통계 대시보드(가입 추이, 활동량 집계)가 8개 샤드를 scatter-gather하면서 p99 6초, 대시보드 열 때마다 모든 샤드 부하 급증.
진단:
- scatter-gather는 8개 샤드 중 가장 느린 한 곳에 전체 응답이 묶임 (tail latency)
- 통계는 실시간일 필요가 없는데 OLTP 샤드를 직접 때리고 있었음
조치 — 비정규화 OLAP 분리:
- Debezium CDC로 각 샤드의 변경을 Kafka로 흘림
- Kafka → ClickHouse에 집계 전용 비정규화 테이블 적재
- 대시보드는 ClickHouse 단일 조회
결과:
- 대시보드 p99: 6초 → 100ms
- OLTP 샤드는 대시보드 트래픽에서 완전히 격리
교훈: cross-shard는 제거가 정답이고, 불가피한 집계는 별도 OLAP 스토어로 비정규화한다. OLTP 샤드에서 scatter-gather 집계를 돌리면 tail latency와 부하를 동시에 떠안는다.
🔄 꼬리질문 3: 실제로 샤딩 경험이 있으신가요? 어떤 어려움이 있었나요?
이 질문의 의도: 이론만 아는지, 실제로 해봤는지 구분합니다. 마이그레이션 전략, 리샤딩, 분산 트랜잭션 처리 등 실무 경험을 확인합니다.
📋 실제 사례
상황: user_id 샤딩 마이그레이션 중 가장 큰 난관은 글로벌 UNIQUE 제약이었음. 이메일은 전체 시스템에서 유일해야 하는데, 사용자가 어느 샤드에 들어갈지는 user_id 해시로 결정되므로 이메일 기준으로는 어느 샤드인지 알 수 없었음.
문제:
- 회원가입 시 이메일 중복 체크를 하려면 8개 샤드를 모두 조회해야 함
- 동시 가입 race condition으로 같은 이메일이 다른 샤드에 중복 생성될 위험
조치 — 글로벌 인덱스 테이블 분리:
email → user_id매핑만 담는 별도 글로벌 DB (샤딩 안 함)- 이메일에 UNIQUE 제약 → 중복 가입 원천 차단
- 가입 흐름: 글로벌 DB에 이메일 선점(INSERT) → 성공 시 해당 샤드에 사용자 생성
결과:
- 이메일 중복 사고 0건
- 가입 latency +15ms (글로벌 DB 1회 조회), 허용 범위
교훈: 샤딩의 진짜 난이도는 데이터 분할이 아니라 글로벌 제약(unique)과 분산 트랜잭션이다. 샤딩 키가 아닌 컬럼으로 유일성을 보장해야 하면 글로벌 인덱스 테이블을 별도로 둔다.
3. 분산 시스템 설계
Q3. 분산 환경에서 동시에 같은 자원을 수정하면 어떤 문제가 생기나요?
기대 답변: Race Condition이 발생합니다. 동시 구매로 재고가 음수가 되거나, 중복 결제가 발생할 수 있습니다.
💡 실제 사례 보기
시나리오: 한정판 굿즈 1,000개를 5만 명 동시 접속 환경에서 정확히 분배해야 함.
시도 1 — SELECT FOR UPDATE (RDBMS 비관적 락):
- 락 대기로 처리량 100 TPS 절벽
- 5분 동안 1,000개 다 안 팔리는 재고 남는데 사용자는 실패 상황
시도 2 — Redis Redlock:
- 5개 Redis 노드 과반 합의
- 운영 중 1개 노드 장애 + GC pause로 중복 발급 12건 발생
- Martin Kleppmann의 비판처럼 클럭 의존성이 결국 약점
최종 — 멱등키 + Redis DECR (Lua 스크립트):
-- 한 번의 원자적 연산으로 차감 + 사용자 멱등키 기록
if redis.call('GET', KEYS[1]) > 0 and
redis.call('SETNX', KEYS[2], 1) == 1 then
return redis.call('DECR', KEYS[1])
else
return -1
end- 사용자별 멱등키로 중복 요청 차단
- Redis 단일 노드 원자성 활용 (Redlock 회피)
- 처리량 5,000 TPS, 재고 정확히 1,000개 소진
교훈:
- 락은 마지막 수단, 멱등성 설계가 분산 환경에서 가장 안전
- “락 vs 멱등성” 둘 다 필요한 것이 아니라, 가능하면 락 없는 설계부터 검토
🔄 꼬리질문 1: 분산 락(Distributed Lock)을 어떻게 구현하시겠어요?
기대 답변:
- Redis:
SET key value NX PX timeout(Redlock 알고리즘) - Zookeeper: Ephemeral Sequential Node
- DB: SELECT FOR UPDATE, Advisory Lock
📋 실제 사례
상황: 매 정시 실행되는 정산 배치가 인스턴스 3대에서 동시에 떠서 같은 정산을 3번 수행, 정산액이 3배로 기록되는 사고. 처음엔 Redis Redlock 도입을 검토했으나 클럭 의존성·노드 합의 복잡도가 부담.
조치 — PostgreSQL Advisory Lock 채택:
-- 트랜잭션 종료 시 자동 해제되는 advisory lock
SELECT pg_try_advisory_xact_lock(hashtext('settlement-batch'));- 이미 쓰고 있는 PG를 그대로 활용 (추가 인프라 0)
- 트랜잭션 커밋/롤백 시 자동 해제 → 락 누수 불가능
- 인스턴스가 죽어도 세션 종료와 함께 락 해제
결과:
- 중복 정산 사고 0건
- Redlock 대비 운영 복잡도·장애 포인트 대폭 감소
교훈: 분산 락은 새 인프라(Redis 클러스터) 보다 기존 DB의 advisory lock으로 해결되는 경우가 많다. 특히 이미 트랜잭션 경계 안에서 도는 작업이면 트랜잭션 락이 가장 안전하다.
🔄 꼬리질문 2: Redis 단일 노드로 분산 락 걸면 문제가 뭔가요?
기대 답변: Redis 장애 시 락이 풀리거나 중복 획득 가능합니다. Redlock은 다수 노드 과반 합의로 이를 완화하지만, Martin Kleppmann의 비판처럼 완벽하진 않습니다. 비즈니스 특성에 따라 락 없이 멱등성으로 처리하는 게 나을 수도 있습니다.
📋 실제 사례
상황: 쿠폰 발급 배치가 Redis 단일 노드 락(SET NX)으로 동작 중. Redis master가 failover되는 순간, master는 락을 잡았다고 응답했지만 replica로 복제되기 전에 다운 → 새 master에는 락이 없어 두 번째 배치가 락 획득 → 같은 쿠폰 2번 발급 사고.
진단:
- Redis replication은 비동기 → master가
SET NX성공 응답을 줘도 replica에 아직 없을 수 있음 - failover 시 데이터 유실 윈도우에 락이 증발
조치 — 락 신뢰성에 의존하지 않는 멱등 설계로 전환:
- 락 자체를 제거
processed_coupons(user_id, coupon_id)UNIQUE 제약 + 처리 이력 테이블- 중복 발급 시도는 DB가 INSERT 실패로 막음 (락 유무와 무관하게 안전)
결과:
- failover가 발생해도 중복 발급 불가능
- 락 타임아웃 튜닝·Redlock 합의 같은 운영 고민 자체가 사라짐
교훈: 분산 락은 비동기 복제·GC pause·클럭 드리프트 때문에 100% 신뢰할 수 없다. 락으로 사고를 막으려 하지 말고, 멱등성으로 사고가 나도 무해하게 만드는 게 더 견고하다.
🔄 꼬리질문 3: 멱등성(Idempotency)은 어떻게 보장하나요?
기대 답변:
- Idempotency Key: 클라이언트가 고유 키 전송, 서버가 중복 체크
- DB Unique Constraint: 중복 요청 자체를 막음
- 상태 기반 처리: 이미 완료된 상태면 스킵
📋 실제 사례
상황: 모바일 결제 API에서 네트워크 불안정으로 클라이언트가 같은 결제를 재시도 → 사용자가 한 번 결제했는데 두 번 청구되는 클레임이 월 30건 발생.
조치 — Stripe 방식 Idempotency-Key 도입:
POST /payments
Idempotency-Key: 7f3a... (클라이언트가 결제 시도마다 UUID 생성)- 서버는
(idempotency_key)UNIQUE 테이블에 키 + 응답을 저장 - 같은 키로 재요청 오면 저장된 응답을 그대로 반환 (재처리 안 함)
- 키 보존 기간 24시간 (재시도 윈도우보다 길게)
결과:
- 중복 청구 클레임: 월 30건 → 0건
- 클라이언트가 안심하고 재시도 가능해져 결제 성공률 1.8%p 상승
교훈: 멱등키를 서버가 책임지면 클라이언트 재시도가 안전해지고, 그게 전체 시스템 회복력을 올린다. “재시도하면 중복될까봐” 못 하던 것을 마음껏 하게 되는 게 핵심 효과.
4. 인덱스와 쿼리 최적화
Q4. 인덱스가 있으면 무조건 빠른가요?
기대 답변: 아니요.
- 데이터가 적으면 Full Scan이 더 빠를 수 있음
- INSERT/UPDATE/DELETE 시 인덱스 갱신 비용 발생
- 선택도(Selectivity)가 낮으면 효과 없음
💡 실제 사례 보기
시나리오: 회원 목록 조회 API가 3.2초, 일일 호출 100만 회, DB CPU 80%로 항상 비명.
진단:
EXPLAIN SELECT * FROM members
WHERE status = 'ACTIVE' AND created_at > '2025-01-01'
ORDER BY created_at DESC LIMIT 50;- type =
ALL(풀스캔), rows = 1,200만 - 인덱스
(created_at, status)가 있었지만WHERE status = '...'등치 조건이 먼저인 쿼리 패턴에 안 맞음
조치:
- 인덱스 컬럼 순서 변경:
(created_at, status)→(status, created_at DESC)- 등치 조건(status)을 선두로
- 범위·정렬 컬럼(created_at)을 뒤로
- 기존 인덱스는 바로 drop 안 하고 1주일 모니터링 후 제거 (다른 쿼리 회귀 방지)
결과:
- 응답: 3.2초 → 45ms
- DB CPU: 80% → 25%
- type =
range, rows = 8,200
교훈:
- 등치 조건 컬럼이 인덱스 선두가 기본 원칙
- 인덱스 추가/삭제는 항상 EXPLAIN 전후 비교하고, 회귀 방지 윈도우를 둠
🔄 꼬리질문 1: 복합 인덱스에서 컬럼 순서가 왜 중요한가요?
기대 답변:
복합 인덱스는 정의된 순서대로 정렬됩니다. (A, B, C) 인덱스가 있을 때:
WHERE A = 1→ 사용됨WHERE A = 1 AND B = 2→ 사용됨WHERE B = 2→ 사용 안됨 (첫 번째 컬럼 없음)
📋 실제 사례
상황: 주문 조회에 (seller_id, order_date) 복합 인덱스가 있어 셀러별 일자 조회는 빨랐는데, 운영팀의 전체 주문 일자별 집계(seller_id 조건 없이 WHERE order_date BETWEEN ...)는 인덱스를 못 타고 풀스캔으로 12초.
진단:
(seller_id, order_date)인덱스는 seller_id가 선두라 seller_id 없는 쿼리는 인덱스 무용- 복합 인덱스는 왼쪽 prefix 규칙 — 첫 컬럼이 조건에 없으면 못 씀
조치:
- 두 쿼리 패턴이 명확히 다름을 인지하고
(order_date)단일 인덱스를 추가 - 셀러 조회는 기존 복합 인덱스, 전체 집계는 신규 단일 인덱스 사용
결과:
- 전체 일자 집계: 12초 → 90ms
- 셀러 조회는 영향 없음 (기존 인덱스 유지)
교훈: 복합 인덱스 컬럼 순서는 가장 자주 등치 매칭되는 컬럼을 선두에 둔다. 단, 선두 컬럼 없이 후행 컬럼만 조건인 쿼리 패턴이 따로 있으면 별도 인덱스가 필요하다 (하나의 인덱스로 모든 패턴을 못 덮음).
🔄 꼬리질문 2: 커버링 인덱스(Covering Index)가 뭔가요?
기대 답변: 쿼리에 필요한 모든 컬럼이 인덱스에 포함되어 테이블 접근 없이 인덱스만으로 결과를 반환하는 것입니다. 디스크 I/O를 줄여 성능이 크게 향상됩니다.
📋 실제 사례
상황: 사용자 목록 페이징(한 페이지 50건) 쿼리가 인덱스로 행을 찾은 뒤 테이블에서 name·email을 다시 읽는 패턴(인덱스 → 테이블 random access)으로 응답 40ms, 일 200만 호출이라 누적 부하 큼.
조치 — 커버링 인덱스 도입:
-- 조회에 필요한 모든 컬럼을 인덱스에 포함
CREATE INDEX idx_users_cover
ON users (status, created_at) INCLUDE (id, name, email);- 쿼리가 테이블 접근 없이 인덱스만으로 결과 반환 (Index Only Scan)
결과:
- 응답: 40ms → 8ms (테이블 random access 제거)
- 단, 인덱스 크기 1.2GB → 2.8GB로 증가 → 버퍼 풀 적중률이 살짝 떨어져 다른 쿼리에 소폭 영향
교훈: 커버링 인덱스는 읽기 핫 쿼리에 강력하지만 인덱스 크기 증가 → 쓰기 비용·버퍼 풀 압박이라는 대가가 있다. 무분별하게 INCLUDE를 늘리지 말고 호출 빈도 높은 소수 쿼리에만 선별 적용한다.
🔄 꼬리질문 3: 실행계획(EXPLAIN) 봐본 적 있나요? 어떤 걸 확인하나요?
기대 답변:
- type: ALL(풀스캔) < range < ref < eq_ref < const
- rows: 예상 검색 행 수
- Extra: Using filesort, Using temporary는 주의
- key: 실제 사용된 인덱스
📋 실제 사례
상황: 게시글 목록 API가 평소 80ms인데 특정 정렬 옵션(ORDER BY view_count DESC)에서만 1.2초. 인덱스는 분명 있는데 느림.
진단 — EXPLAIN ANALYZE:
Extra컬럼에Using filesort발견 → 인덱스 정렬이 아니라 메모리/디스크에서 별도 정렬 수행- 인덱스는
(category_id, created_at)인데 정렬은view_count→ 정렬 컬럼이 인덱스에 없어 filesort 발생 - 추가로 actual rows(실제) 와 estimated rows(추정) 가 10배 차이 → 통계도 stale 의심
조치:
(category_id, view_count DESC)인덱스 추가로 filesort 제거ANALYZE실행으로 통계 갱신
결과:
- 응답: 1.2초 → 80ms
Using filesort사라지고 Index Scan으로 정렬까지 해결
교훈: EXPLAIN에서 type만 보지 말고 Extra의 Using filesort / Using temporary 를 반드시 확인한다. 또 추정 rows와 실제 rows의 큰 차이는 통계 stale 신호다 (Plan Flip의 전조).
5. CAP 정리와 일관성
Q5. CAP 정리가 뭔가요?
기대 답변: 분산 시스템에서 Consistency(일관성), Availability(가용성), Partition Tolerance(분할 허용) 중 최대 2개만 보장할 수 있다는 이론입니다.
💡 실제 사례 보기
시나리오: 전자상거래 백엔드 — 도메인별로 CAP 축을 다르게 선택한 사례.
| 도메인 | 선택 | 이유 |
|---|---|---|
| 결제·주문 | CP | 네트워크 파티션 시 중복 결제보다 결제 중단 비용이 훨씬 작음 |
| 상품 카탈로그 | AP | CDN + Redis 캐시로 stale 데이터 제공 — 사용자가 옛 가격을 보는 게 페이지 못 봄보다 나음 |
| 장바구니 | AP + 동기화 | 클라이언트 우선, 서버 머지로 일관성 회복 (LWW + 사용자 확인 UI) |
| 재고 | CP | 음수 재고 절대 금지 — 파티션 시 차감 거부, “잠시 후 다시 시도” 응답 |
실제 운영 신호:
- 결제 도메인은 SLO를 *Availability 99.9%*가 아니라 *Consistency 100%*로 정의
- 상품 카탈로그는 캐시 hit ratio 95%+를 유지해 AP 선택이 정당화됨
교훈:
- CAP은 시스템 전체가 아니라 도메인 단위로 결정
- “왜 그쪽을 골랐는가?”의 근거는 항상 비즈니스 손실 비용으로 환산
🔄 꼬리질문 1: 네트워크 파티션이 발생하면 CP와 AP 중 어떤 걸 선택하시겠어요?
기대 답변: 비즈니스에 따라 다릅니다.
- CP: 금융, 결제 등 일관성이 중요한 경우
- AP: SNS, 검색 등 가용성이 중요한 경우
- 실제로는 대부분 AP를 선택하고 Eventual Consistency로 보완
📋 실제 사례
상황: 배달 앱에서 주문 DB와 가게 정보 DB가 서로 다른 가용 영역(AZ)에 있는데, AZ 간 네트워크 파티션 발생. 한 시스템 안에서 두 도메인이 정반대 선택을 해야 했음.
판단:
- 주문 = CP: 파티션 동안 주문 생성을 거부(5xx) → 중복 주문·결제는 절대 불가. “잠시 후 다시 시도” 안내
- 가게 목록 = AP: CDN/Redis에 캐시된 5분 전 가게 정보라도 노출 → 사용자가 앱을 열어볼 수는 있게
근거 — 비즈니스 손실 비용 환산:
- 주문이 중복되면 환불·정산·고객 불만으로 건당 수만 원 손실
- 가게 목록이 5분 stale인 건 거의 무해 (영업시간·메뉴가 5분 새 바뀔 일 드묾)
결과:
- 파티션 30분 동안 주문은 일부 실패했지만 데이터 정합성 100% 유지
- 가게 탐색·메뉴 조회는 정상 동작 → 사용자 이탈 최소화
교훈: 파티션 시 CP/AP는 앱 전체로 하나가 아니라 도메인별로 분기한다. 선택 근거는 항상 “이 도메인이 틀린 데이터를 주면/멈추면 얼마를 잃는가” 로 환산한다.
🔄 꼬리질문 2: Eventual Consistency는 어떻게 구현하나요?
기대 답변:
- 비동기 복제 + 충돌 해결 전략
- Last Write Wins: 타임스탬프 기준 최신 값 선택
- Vector Clock: 인과관계 추적
- CRDT: 충돌 없이 병합 가능한 자료구조
📋 실제 사례
상황: 협업 문서 도구에서 여러 사용자가 같은 문서를 동시 편집. 초기엔 Last Write Wins(타임스탬프 최신 값 채택)로 구현했는데, 두 사람이 동시에 다른 문단을 수정하면 한 쪽 수정이 통째로 사라지는 데이터 유실이 빈번해 사용자 불만 폭주.
진단:
- LWW는 문서 전체를 하나의 값으로 보고 최신 것만 남김 → 동시 편집에 근본적으로 부적합
- 충돌이 “둘 중 하나 버리기”로 해결되어 작업 손실
조치 — CRDT(Yjs) 도입:
- 문서를 문자 단위 CRDT 구조로 표현
- 동시 편집을 충돌 없이 자동 병합 (각 편집이 고유 ID + 위치 정보를 가져 순서 보장)
- 오프라인 편집 후 재접속해도 자동 수렴
결과:
- 동시 편집 작업 유실: 빈번 → 0건
- 오프라인 편집·실시간 협업 모두 매끄럽게 동작
교훈: Eventual Consistency 구현은 데이터 성격에 맞춰야 한다. 단순 키-값엔 LWW로 충분하지만, 동시 편집·병합이 본질인 데이터는 CRDT가 답이다. LWW는 구현이 쉬운 대신 조용한 데이터 유실이 대가.
🔄 꼬리질문 3: 2PC(Two-Phase Commit)의 문제점은 뭔가요?
기대 답변:
- Blocking: Coordinator 장애 시 참여자들이 대기 상태로 멈춤
- Single Point of Failure: Coordinator 의존성
- 성능: 모든 참여자가 응답할 때까지 락 유지
- 대안으로 SAGA 패턴, Outbox 패턴 사용
📋 실제 사례
상황: 주문 시 결제·재고·포인트 3개 서비스를 2PC(XA 트랜잭션)로 묶어 원자성을 보장하려 했음. 평소엔 동작했지만 coordinator가 잠깐 GC pause로 멈추자 3개 서비스 모두 prepared 상태로 락을 잡은 채 30초간 정지 → 그 시간 동안 다른 주문들도 같은 자원을 못 잡아 연쇄 정지.
진단:
- 2PC는 coordinator가 commit 결정을 내릴 때까지 모든 참여자가 락을 holding
- coordinator가 SPOF — 잠깐만 멈춰도 전체 블로킹
- 참여자 수·네트워크 홉이 늘수록 락 holding 시간이 길어짐 → 처리량 급락
조치 — SAGA + Outbox로 전환:
- 2PC 제거, 각 단계를 로컬 트랜잭션 + 도메인 이벤트로 분리
- 결제 → 재고 → 포인트를 순차 처리, 실패 시 보상 트랜잭션(환불·재고 복원)
- 각 단계 이벤트 발행은 Outbox로 원자성 보장
결과:
- coordinator 블로킹으로 인한 연쇄 정지 소멸
- 주문 처리량 3배 (락 holding 제거 효과)
- 대신 eventual consistency 윈도우(수 초)를 감수하고 DLQ·운영 콘솔로 보완
교훈: MSA에서 2PC는 가용성을 갉아먹는 안티패턴이다. 강한 원자성을 포기하고 SAGA로 eventual consistency를 받아들이는 것이 분산 환경에선 더 견고하다. 단, 보상 트랜잭션과 DLQ 설계가 동반되어야 한다.
마무리: 1편 핵심 정리
- 트래픽 처리: 오토스케일링 + 캐싱 + 비동기 처리
- DB 확장: Read Replica → Sharding → CQRS
- 분산 시스템: 락보다 멱등성, Trade-off 이해
- 쿼리 최적화: 실행계획 분석, 인덱스 설계
- 일관성: CAP 이해, Eventual Consistency 전략
다음 편에서는 장애 대응, 이벤트 아키텍처, 배포 전략을 다룹니다.