Web-Database 실전 훈련북
이 문서의 사용법
이 문서는 Web-Database 문서들의 요약본이 아니다. 이미 정리된 개념 문서와 15. Web-Database 실전 가이드북을 실제 판단 훈련으로 바꾸는 문제집이다.
각 훈련은 먼저 Evidence를 읽고, 내 판단을 직접 써본 뒤, 힌트와 해설을 비교하는 순서로 사용한다. 정답을 외우는 것보다 “무엇을 먼저 확인하고, 어떤 선택지를 버리며, 누구에게 어떤 정보를 전달할지”를 몸에 익히는 데 초점을 둔다.
훈련 방식
먼저 써보기를 비워 두지 않는다. 짧아도 좋으니 내 판단, 근거, 다음 액션을 쓴다.- 해설은 내 답안을 평가하는 기준으로 읽는다. 내 답안이 틀렸는지보다 빠진 관점이 무엇인지 본다.
연결 문서는 복습 경로다. 훈련에서 막힌 지점이 있으면 해당 문서로 돌아가 개념, 원리, 예시를 다시 읽는다.- 같은 훈련을 최소 2회 반복한다. 1회차에는 판단 순서를 익히고, 2회차에는 근거와 커뮤니케이션 품질을 높인다.
반복 학습 루프
- 상황과 Evidence를 먼저 읽는다.
- 5분 안에 내 1차 판단을 쓴다.
- 힌트를 보고 빠진 관점을 보완한다.
- 해설과 비교하며 놓친 기준을 표시한다.
- 연결 문서로 돌아가 필요한 상세 문서를 다시 읽는다.
- 반복 기록표에 다음에 더 빨리 떠올릴 기준을 남긴다.
훈련 목록
| 구분 | 훈련 | 핵심 판단 | 주요 연결 문서 |
|---|---|---|---|
| Lab 1 | 이벤트 쿠폰 발급 DB 설계 초안 | 불변식, 제약, 인덱스, 트랜잭션 경계 | 15. Web-Database 실전 가이드북 |
| Lab 2 | 회원별 주문 목록 API 설계 | Page/Slice/cursor, count 비용, 복합 인덱스 | 15. Web-Database 실전 가이드북 |
| Lab 3 | 운영 정정과 backfill dry-run | 영향 범위, batch size, 중단 기준, 검증 SQL | 15. Web-Database 실전 가이드북 |
| Lab 4 | JPA 목록 조회 fetch 전략 | N+1, fetch join, DTO projection, pagination | 15. Web-Database 실전 가이드북 |
| Incident 1 | Hikari timeout과 낮은 DB CPU | pool wait, lock wait, long transaction, 외부 I/O | 15. Web-Database 실전 가이드북 |
| Incident 2 | 쿠폰 발급 deadlock과 hot row | lock 순서, 재시도, idempotency, write 집중 완화 | 15. Web-Database 실전 가이드북 |
| Incident 3 | 저장 직후 read replica 조회 실패 | read-after-write, primary routing, lag 임계치 | 15. Web-Database 실전 가이드북 |
| Incident 4 | 대량 DELETE 이후 WAL과 replica lag | 중단 기준, PITR, vacuum, 완료 검증 | 15. Web-Database 실전 가이드북 |
| Review 1 | 관리자 검색/export API 리뷰 | 동적 검색, count, export, pool 격리 | 15. Web-Database 실전 가이드북 |
| Review 2 | 대형 테이블 migration 리뷰 | expand/contract, online DDL, backfill, rollback | 15. Web-Database 실전 가이드북 |
| Review 3 | 인프라/DBA 요청서 리뷰 | SQL, 지표, 실행 계획, 원하는 결정 | 15. Web-Database 실전 가이드북 |
| 종합 | 프로모션 오픈 복합 장애 | triage, 완화, 복구, 회고 action | 15. Web-Database 실전 가이드북 |
Part 1. 실습 랩
Lab 1. 이벤트 쿠폰 발급 DB 설계 초안 만들기
상황
프로모션 팀이 “선착순 10만 명에게 쿠폰을 발급하고, 주문 시 한 번만 사용할 수 있게 해달라”고 요청했다. 백엔드 담당자인 당신은 API 구현 전에 테이블, 제약, 인덱스, 트랜잭션 경계 초안을 먼저 잡아야 한다.
제공된 Evidence
요구사항
- 회원은 같은 프로모션 쿠폰을 1번만 받을 수 있다.
- 쿠폰은 발급 후 7일 동안만 사용할 수 있다.
- 주문 결제 성공 시 쿠폰은 USED 상태가 된다.
- 결제 실패나 취소 시 쿠폰 사용 상태는 되돌릴 수 있어야 한다.
- 관리자 화면에서 회원별 쿠폰 발급/사용 이력을 최신순으로 본다.
초안
coupon_issue(id, user_id, promotion_id, status, issued_at, used_at, order_id)해야 할 일
- 반드시 지켜야 하는 불변식을 3개 이상 쓴다.
- 필요한 Unique 제약과 Check 제약 후보를 쓴다.
- 회원별 이력 조회와 사용 처리에 필요한 인덱스를 제안한다.
- 발급, 사용, 취소의 트랜잭션 경계를 나눈다.
- 운영 중 중복 발급이나 중복 사용이 들어왔을 때 API가 어떤 응답을 줄지 정한다.
먼저 써보기
내 답안
- 불변식:
- 테이블/제약:
- 인덱스:
- 트랜잭션 경계:
- 예외 응답:
- 운영 확인 쿼리:
힌트
- “한 번만”이라는 요구는 애플리케이션 if문보다 Unique 제약과 idempotency 기준으로 먼저 생각한다.
- 상태값은 단순 문자열이 아니라 가능한 전이와 예외 처리까지 같이 설계해야 한다.
- 관리자 목록 인덱스와 결제 처리 인덱스는 목적이 다르다. 한 인덱스로 모든 일을 시키려 하지 않는다.
해설
좋은 초안은 user_id, promotion_id의 중복 발급을 DB 제약으로 막고, 상태값을 ISSUED, USED, CANCELED, EXPIRED처럼 제한한다. 결제 성공 처리에서는 status = 'ISSUED' 조건을 둔 원자적 update가 기본 후보가 된다. 이때 이미 사용된 쿠폰이면 row count가 0이므로 API는 재시도 가능 오류가 아니라 도메인 충돌로 응답해야 한다.
인덱스는 사용 패턴별로 나눈다. 회원 이력은 (user_id, issued_at desc, id desc), 프로모션 발급 중복은 Unique (user_id, promotion_id), 주문과 연결된 사용 확인은 order_id 또는 (promotion_id, status) 후보를 검토한다. 다만 모든 후보를 바로 만들기보다 조회 빈도, cardinality, 쓰기 비용을 함께 본다.
취소와 환불은 “DB rollback”만으로 해결되지 않을 수 있다. 결제 승인, 포인트 적립, 알림 발송 같은 외부 부작용이 있으면 보상 트랜잭션과 idempotency key를 함께 설계해야 한다.
연결 문서
- Case 1. 신규 기능을 설계하는데 테이블부터 만들고 싶어진다 - 신규 기능 설계에서 테이블보다 먼저 봐야 할 불변식을 확인한다.
- 02. Primary Foreign Unique Check 상세 - Unique, Foreign Key, Check 제약을 기능 요구와 연결한다.
- 05. 재고 포인트 카운터 사례 상세 - 선착순, 재고, 카운터성 도메인의 동시성 판단을 복습한다.
- 03. 데이터 타입과 제약 선택 상세 - 상태값, 시간값, NULL 허용 여부를 정하는 기준을 본다.
- 05. 실무 인덱스 설계 기준 상세 - 조회 패턴과 쓰기 비용을 함께 보는 인덱스 리뷰 기준을 확인한다.
가져갈 판단 기준
“한 번만 된다”는 요구는 코드 분기보다 DB 제약, 상태 전이, idempotency로 먼저 고정한다.
Lab 2. 회원별 주문 목록 API의 SQL과 pagination 방식 고르기
상황
GET /api/me/orders가 데이터 증가 후 느려졌다. 기획은 “최신 주문부터 20개씩, 상태 필터와 기간 필터를 지원하고, 전체 개수도 있으면 좋겠다”고 말한다. 당신은 SQL, 인덱스, pagination 방식을 다시 정해야 한다.
제공된 Evidence
-- 현재 data query
select o.*
from orders o
where o.user_id = :userId
and (:status is null or o.status = :status)
and o.created_at between :from and :to
order by o.created_at desc
limit 20 offset :offset;
-- Spring Data Page 사용으로 자동 생성되는 count query
select count(*)
from orders o
where o.user_id = :userId
and (:status is null or o.status = :status)
and o.created_at between :from and :to;관측
- 특정 장기 이용자 주문 수: 830,000 rows
- p95: 180ms -> 1.9s
- offset 0은 빠르지만 offset 20,000 이후부터 급격히 느려진다.
- count query가 전체 응답 시간의 60% 이상을 차지한다.해야 할 일
Page,Slice, cursor 중 무엇을 기본으로 둘지 정한다.- data query와 count query를 분리해서 유지할지 결정한다.
- 후보 인덱스 2개를 쓰고, 각각이 어떤 조건에 맞는지 설명한다.
- cursor를 쓴다면 cursor key를 어떤 컬럼 조합으로 만들지 쓴다.
- API 응답 계약에서 전체 개수 요구를 어떻게 다룰지 정한다.
먼저 써보기
내 답안
- pagination 선택:
- SQL 수정안:
- 인덱스 후보:
- count 처리:
- API 계약 변경:
- 추가로 볼 실행 계획:
힌트
- offset은 뒤 페이지로 갈수록 “건너뛰는 비용”을 낸다.
- 최신순 목록에서 cursor는 보통 정렬 컬럼과 tie-breaker를 함께 쓴다.
- 전체 개수가 정말 필요한 화면인지, 첫 화면에서만 필요한지, 비동기로 캐시해도 되는지 먼저 확인한다.
해설
회원별 최신 주문 목록의 기본 후보는 Slice 또는 cursor다. 사용자가 다음 페이지를 계속 넘기는 UI라면 전체 count를 매 요청마다 보장할 필요가 낮다. cursor는 (created_at, id)처럼 정렬 안정성을 보장하는 키를 함께 써야 한다.
상태 필터가 자주 쓰이고 cardinality가 적당하다면 (user_id, status, created_at desc, id desc)가 후보가 된다. 상태 필터가 선택적이고 대부분 전체 상태 조회라면 (user_id, created_at desc, id desc)가 더 나을 수 있다. “항상 복합 인덱스가 많을수록 좋다”가 아니라 실제 where, order by, limit 조합과 쓰기 비용을 같이 봐야 한다.
count는 API 계약의 문제다. Page가 편해서 붙였더니 DB에 매번 무거운 count를 시키는 형태라면, hasNext 중심 응답으로 바꾸거나 count를 조건부로 제공하는 선택지가 있다.
연결 문서
- Case 4. 회원별 주문 목록 API가 데이터 증가 후 느려진다 - 느린 목록 API에서 먼저 볼 증거를 정리한다.
- [[15. Web-Database 실전 가이드북#case-6-pageable을-붙였더니-count-쿼리가-병목이-된다|Case 6.
Pageable을 붙였더니 count 쿼리가 병목이 된다]] -Pageable편의 기능이 만드는 count 비용을 복습한다. - 01. Offset vs Cursor 상세 - offset과 cursor의 선택 기준을 다시 본다.
- 03. 복합 인덱스와 컬럼 순서 상세 - where, order by, limit을 복합 인덱스 순서와 연결한다.
- 01. EXPLAIN 읽는 법 상세 - 실행 계획에서 rows, sort, scan 방식을 확인하는 루틴을 본다.
가져갈 판단 기준
목록 API는 “몇 번째 페이지인가”보다 “어떤 순서로 다음 묶음을 안정적으로 가져올 것인가”가 먼저다.
Lab 3. 운영 데이터 정정과 backfill dry-run 계획 쓰기
상황
회원 이메일 정규화 로직이 누락되어 검색과 중복 가입 방지에 문제가 생겼다. 운영 데이터 120만 건에 normalized_email을 채워야 한다. 서비스 중단 없이 안전한 dry-run 계획을 먼저 만들어야 한다.
제공된 Evidence
select count(*) as target_count
from users
where normalized_email is null
and email is not null;
-- target_count = 1,247,382
select count(*) as duplicate_after_normalize
from (
select lower(trim(email)) as normalized_email, count(*)
from users
where email is not null
group by lower(trim(email))
having count(*) > 1
) t;
-- duplicate_after_normalize = 318운영 조건
- API p95가 500ms를 넘으면 작업을 멈춰야 한다.
- replica lag가 30초를 넘으면 작업을 멈춰야 한다.
- batch 작업은 별도 worker에서 실행할 수 있다.
- 정규화 후 Unique 제약 추가를 검토 중이다.해야 할 일
- dry-run에서 먼저 확인할 count, duplicate, sample SQL을 쓴다.
- batch size, sleep, timeout, commit 단위를 정한다.
- 중단 기준과 재개 기준을 쓴다.
- Unique 제약 추가 전에 해결해야 할 데이터 예외를 분류한다.
- 완료 검증 SQL을 작성한다.
먼저 써보기
내 답안
- dry-run SQL:
- batch 계획:
- 중단 기준:
- 재개 기준:
- 예외 데이터 처리:
- 완료 검증:
- 인프라/DBA에게 공유할 정보:
힌트
- backfill은 “한 번에 빨리 끝내는 update”가 아니라 멈추고 재개할 수 있는 운영 작업이다.
- Unique 제약은 데이터가 깨끗해진 뒤에야 안전하게 추가할 수 있다.
- 대량 update는 lock 시간뿐 아니라 WAL, replica lag, vacuum 부담도 만든다.
해설
먼저 duplicate 후보를 분리해야 한다. normalized_email을 채운 뒤 Unique 제약을 추가하려면 같은 정규화 값으로 묶이는 계정이 왜 존재하는지 확인해야 한다. 탈퇴 계정, 소셜 계정, 테스트 계정, 과거 마이그레이션 계정이 섞여 있을 수 있다.
batch는 작은 트랜잭션으로 나누고, id 범위 또는 keyset 방식으로 진행률을 추적한다. 예를 들어 1,000건 단위로 commit하고, 각 batch마다 API p95, lock wait, replica lag를 확인해 멈출 수 있어야 한다. timeout을 길게 늘려 한 번에 끝내는 방식은 실패 시 rollback 부담과 WAL 부담을 키운다.
완료 검증은 단순히 작업 성공 로그만 보지 않는다. 대상 row가 0인지, duplicate 예외가 처리됐는지, 샘플 데이터가 기대한 값인지, API 오류율과 replica lag가 정상으로 돌아왔는지를 함께 본다.
연결 문서
- Case 15. backfill을 해야 하는데 운영 트래픽이 걱정된다 - backfill을 운영 작업으로 보는 기준을 복습한다.
- 05. 운영 DB에서 직접 쿼리할 때 상세 - 운영 update 전 확인, 트랜잭션, rollback 한계를 본다.
- 03. Backfill과 대량 작업 상세 - batch, 중단 기준, runbook을 확인한다.
- 05. 긴 트랜잭션 위험 상세 - 긴 트랜잭션이 connection, lock, vacuum에 주는 영향을 본다.
- 05. Vacuum Bloat WAL 같은 운영 이슈 상세 - 대량 변경이 WAL과 replica lag로 이어지는 흐름을 본다.
가져갈 판단 기준
좋은 backfill 계획에는 실행 SQL보다 중단 기준, 재개 기준, 완료 검증이 먼저 보인다.
Lab 4. JPA 목록 조회의 N+1과 fetch 전략 고르기
상황
주문 목록 API가 JPA Repository 메서드 하나로 구현되어 있다. 코드상으로는 짧지만 SQL 로그가 과하게 많다. 단순히 fetch join을 붙이면 될지, DTO projection으로 바꿔야 할지 판단해야 한다.
제공된 Evidence
@Transactional(readOnly = true)
public Page<Order> getOrders(Long userId, Pageable pageable) {
return orderRepository.findByUserIdOrderByCreatedAtDesc(userId, pageable);
}
// Controller에서 Order -> Response 변환 중 order.getItems(), item.getProduct() 접근관측
- 첫 페이지 20건 조회
- order select: 1회
- order_item select: 20회
- product select: 47회
- fetch join 적용 후 Page total count가 이상하게 느려지고, 중복 row 제거 비용이 늘었다.해야 할 일
- N+1이 발생하는 접근 경로를 쓴다.
- fetch join, EntityGraph, batch size, DTO projection 중 1차 후보를 고른다.
- pagination과 함께 쓸 때 조심할 점을 쓴다.
- 필요한 SQL과 인덱스 확인 항목을 쓴다.
- Repository 메서드 이름만 보고 끝내지 않기 위한 리뷰 질문을 만든다.
먼저 써보기
내 답안
- N+1 경로:
- 선택할 fetch 전략:
- pagination 주의점:
- 확인할 SQL:
- 인덱스 확인:
- 코드 리뷰 질문:
힌트
- 목록 화면에 필요한 필드가 제한적이면 엔티티 그래프보다 DTO query가 더 단순할 수 있다.
- 컬렉션 fetch join과 pagination은 row 증폭과 메모리 pagination을 의심해야 한다.
- ORM 문제처럼 보여도 마지막 판단은 실제 SQL, 실행 계획, 인덱스가 한다.
해설
이 상황은 Order 20건을 가져온 뒤 각 주문의 items, 각 item의 product를 lazy loading하면서 N+1이 발생한다. 상세 화면처럼 하나의 aggregate를 깊게 보는 경우와 목록 화면처럼 필요한 필드만 모아 보여주는 경우를 나눠야 한다.
목록 API라면 DTO projection이 강한 후보가 된다. 필요한 주문 요약, 상품 대표명, 금액, 상태만 가져오도록 SQL을 명시하면 fetch join으로 엔티티 전체를 끌고 오는 비용을 줄일 수 있다. 다만 집계나 대표 상품 선택이 들어가면 join, group by, window function 비용과 인덱스도 함께 검토해야 한다.
fetch join은 to-one 관계에는 비교적 안전하지만, to-many 컬렉션과 pagination을 함께 쓰면 row 수가 늘고 중복 제거 비용이 생긴다. batch size는 N+1을 완화할 수 있지만 SQL 비용 자체를 없애지는 않는다.
연결 문서
- Case 7. JPA Repository 메서드는 짧은데 SQL이 너무 많이 나간다 - Repository 메서드 뒤의 실제 SQL을 확인하는 루틴을 본다.
- Case 8. N+1을 fetch join으로 고쳤더니 pagination이 이상해진다 - fetch join과 pagination의 충돌을 복습한다.
- 03. N+1 Fetch Join EntityGraph 상세 - fetch join, EntityGraph, batch size, DTO query의 선택 기준을 본다.
- 05. JPA 쿼리와 인덱스 상세 - JPQL, Querydsl, Native Query와 인덱스 리뷰를 연결한다.
- 04. Spring Transaction과 DB 연결 상세 - readOnly, flush, connection 점유 시간을 함께 본다.
가져갈 판단 기준
JPA 성능 문제는 fetch 키워드를 붙이기 전에 화면이 필요한 데이터 모양과 실제 SQL 비용을 먼저 맞춘다.
Part 2. 장애 대응 훈련
Incident 1. Hikari timeout은 늘었는데 DB CPU는 낮다
Incident 상황
점심 피크 이후 주문 API의 timeout이 늘었다. 애플리케이션 로그에는 Hikari connection timeout이 보이지만 DB CPU는 25% 수준이다. 단순히 DB 스펙 부족으로 보기 어렵다.
현재 보이는 증상
GET /api/orders/{id}p99가 400ms에서 6s로 증가했다.- Hikari pending threads가 0에서 45까지 증가했다.
- DB CPU는 낮고, connection 수는 상한에 가깝다.
- 일부 외부 결제 승인 API도 같은 시간대에 느려졌다.
로그/지표/SQL 단서
WARN HikariPool-1 - Connection is not available, request timed out after 3000ms.
metric.hikari.active=30
metric.hikari.idle=0
metric.hikari.pending=45
metric.db.cpu=25%
metric.api.payment.p99=4.8sselect state, wait_event_type, wait_event, count(*)
from pg_stat_activity
where datname = current_database()
group by state, wait_event_type, wait_event
order by count(*) desc;내 대응 초안
- 5분 안에 확인할 것:
- 원인 후보:
- 하면 안 되는 조치:
- 복구 순서:
- 완료 검증:
5분 안에 확인할 것
- Hikari active, idle, pending, acquire time이 언제부터 변했는지 본다.
- DB에서 lock wait, long transaction, idle in transaction이 있는지 확인한다.
- 느린 API가 connection을 잡은 채 외부 I/O를 기다리는지 본다.
- 최근 배포, batch, export, 장애 알림 타임라인을 맞춘다.
- read-only API와 write API가 같은 pool을 쓰는지 확인한다.
원인 후보 분류
- connection을 잡은 채 외부 결제 API를 기다리는 코드 경계 문제
- lock wait 또는 long transaction 때문에 query는 CPU를 쓰지 못하고 대기하는 문제
- 느린 query가 적은 CPU로 오래 connection을 점유하는 문제
- pool 크기보다 요청 동시성이 커진 문제
- timeout 계층이 맞지 않아 장애가 앞단으로 전파되는 문제
하면 안 되는 조치
- 원인 분류 없이 Hikari maximumPoolSize만 크게 올리지 않는다.
- DB CPU가 낮다는 이유로 DB 문제 가능성을 바로 제외하지 않는다.
- 외부 API timeout, transaction 경계, lock wait를 확인하기 전에 재시작만 반복하지 않는다.
- timeout을 길게 늘려 pending이 더 오래 쌓이게 만들지 않는다.
복구 절차
- 영향 endpoint와 요청량을 제한하거나 부분 degrade한다.
- connection을 오래 잡는 코드 경로가 확인되면 해당 기능을 임시 차단하거나 외부 I/O를 transaction 밖으로 빼는 hotfix를 준비한다.
- lock wait가 원인이면 blocker transaction을 식별하고, 종료 가능 여부를 owner와 확인한다.
- 느린 query가 원인이면 실행 계획, bind 값, 최근 통계 변경을 확보한다.
- pool 조정은 DB max connection, 다른 서비스 pool, proxy 설정을 함께 본 뒤 작은 폭으로 적용한다.
완료 검증
- Hikari pending이 0에 가깝게 돌아왔다.
- acquire time p95가 정상 범위로 돌아왔다.
- 영향 endpoint p99와 오류율이 정상화됐다.
- DB lock wait와 long transaction이 해소됐다.
- 임시 완화가 남아 있다면 제거 조건과 담당자가 정해졌다.
회고 질문
- transaction 안에서 외부 I/O를 호출하는 코드가 리뷰에서 걸러지는가?
- pool, DB, 외부 API timeout의 순서가 설계되어 있는가?
- Hikari pending과 acquire time 알림이 endpoint 지표와 연결되어 있는가?
- read/write 또는 관리자 작업 pool을 분리할 필요가 있는가?
연결 문서
- Case 11. Hikari connection timeout이 갑자기 늘어난다 - connection timeout을 pool 크기만의 문제로 보지 않는 기준을 본다.
- Case 12. DB CPU는 낮은데 API가 느리다 - 낮은 CPU와 높은 대기 시간을 구분한다.
- 04. Pool 고갈과 장애 전파 상세 - pool 고갈이 API 장애로 번지는 흐름을 확인한다.
- 04. 느린 쿼리 분석 루틴 상세 - slow query 확인 순서를 복습한다.
- 01. DB가 요청 흐름에서 하는 일 상세 - 요청 흐름 안에서 DB가 어디서 대기를 만드는지 본다.
가져갈 판단 기준
DB CPU가 낮아도 connection이 부족하면 사용자는 느리다. 대기 위치를 먼저 찾는다.
Incident 2. 쿠폰 발급 중 deadlock과 hot row 경합이 같이 보인다
Incident 상황
프로모션 시작 직후 쿠폰 발급 API에서 deadlock 로그가 나오고, 재시도 로직 때문에 요청량이 더 늘었다. 동시에 promotion.remaining_count row update가 몰리고 있다.
현재 보이는 증상
- 쿠폰 발급 API 성공률이 99.8%에서 92%로 감소했다.
- deadlock detected 로그가 1분에 40건 이상 발생한다.
- 같은
promotion_idrow에 update가 집중된다. - retry가 즉시 재시도라서 순간 TPS가 더 높아진다.
로그/지표/SQL 단서
ERROR Deadlock found when trying to get lock; try restarting transaction
coupon.issue.retry.count=12,480/min
coupon.issue.p99=3.2s
promotion.remaining_count update p95=1.8s-- 코드 경로 A
update promotion set remaining_count = remaining_count - 1 where id = :promotionId;
insert into coupon_issue(user_id, promotion_id, status, issued_at) values (...);
-- 코드 경로 B
insert into coupon_issue(user_id, promotion_id, status, issued_at) values (...);
update promotion set remaining_count = remaining_count - 1 where id = :promotionId;내 대응 초안
- 5분 안에 확인할 것:
- 원인 후보:
- 하면 안 되는 조치:
- 복구 순서:
- 완료 검증:
5분 안에 확인할 것
- lock 획득 순서가 코드 경로마다 다른지 확인한다.
- Unique 제약으로 중복 발급이 막히는지 확인한다.
- 재시도에 backoff와 최대 횟수가 있는지 확인한다.
- hot row가
promotion하나인지,coupon_issue인덱스 경합인지 분리한다. - 발급 성공 기준이 정확한 잔여 수인지, 사후 정산 가능한지 확인한다.
원인 후보 분류
- 서로 다른 lock 획득 순서로 인한 deadlock
remaining_count단일 row update에 write가 집중되는 hot row- idempotency 없는 즉시 재시도가 경합을 증폭하는 문제
- Unique 제약 누락으로 중복 발급 방지가 애플리케이션에만 있는 문제
- 인덱스가 부족해 update 또는 select 범위가 넓어지는 문제
하면 안 되는 조치
- deadlock을 모두 무조건 즉시 재시도하지 않는다.
- 잔여 수 update를 제거하고 중복 발급 가능성을 열어두지 않는다.
- lock 순서가 정리되지 않은 상태에서 worker 수만 늘리지 않는다.
- Unique 제약 없이 cache counter만 믿고 발급 확정 처리하지 않는다.
복구 절차
- 발급 API에 rate limit 또는 queue를 적용해 순간 write 집중을 낮춘다.
- lock 획득 순서를 하나로 통일한다.
coupon_issue(user_id, promotion_id)Unique 제약을 기준으로 idempotent하게 응답한다.- 잔여 수 차감은 조건부 update 또는 사전 할당 bucket, queue 처리로 분산 가능성을 검토한다.
- 재시도에는 backoff, jitter, 최대 횟수를 둔다.
완료 검증
- deadlock 로그가 기준 이하로 감소했다.
- 발급 성공 수와 Unique coupon_issue 수가 일치한다.
- retry count가 안정화됐다.
- hot row update latency와 lock wait가 정상화됐다.
- 중복 발급과 음수 잔여 수가 없다.
회고 질문
- 동시성 높은 도메인의 lock 획득 순서를 설계 문서에 남겼는가?
- idempotency key와 Unique 제약이 같은 기준을 쓰는가?
- retry 정책이 장애를 완화하는가, 증폭하는가?
- hot row를 줄일 수 있는 도메인 타협점이 있는가?
연결 문서
- Case 10. deadlock과 lock wait가 간헐적으로 발생한다 - deadlock과 lock wait를 나눠 보는 기준을 본다.
- Case 18. hot row 때문에 DB 스펙을 올려도 해결되지 않는다 - 단일 row write 집중의 한계를 복습한다.
- 01. Lock 경합과 Deadlock 상세 - 백엔드 코드가 만드는 lock 패턴을 확인한다.
- 04. Hot Row와 Write 병목 상세 - hot row 완화 선택지를 본다.
- 04. 동시성 제어 패턴 상세 - Unique 제약, 조건부 update, 낙관적/비관적 락의 선택 기준을 본다.
- 03. Rollback Retry Idempotency 상세 - retry와 idempotency를 함께 설계한다.
가져갈 판단 기준
deadlock은 재시도만 붙이는 문제가 아니다. lock 순서, idempotency, write 집중 구조를 함께 줄인다.
Incident 3. read replica 이후 저장 직후 조회가 실패한다
Incident 상황
읽기 부하를 줄이기 위해 read replica를 붙였다. 배포 후 사용자가 게시글을 저장하고 바로 상세 화면으로 이동하면 간헐적으로 404가 보인다.
현재 보이는 증상
- 저장 API는 201 Created를 반환한다.
- 저장 직후 상세 조회의 404 비율이 0.7%로 증가했다.
- 5초 후 새로고침하면 대부분 정상 조회된다.
- replica lag가 피크 시간대에 1초에서 12초까지 튄다.
로그/지표/SQL 단서
POST /api/posts -> primary write success id=982341
GET /api/posts/982341 -> replica route, result not found
replica.lag.seconds p95=8.7, max=12.4@Transactional(readOnly = true)
public PostResponse getPost(Long id) {
return postQueryRepository.findById(id)
.orElseThrow(NotFoundException::new);
}내 대응 초안
- 5분 안에 확인할 것:
- 원인 후보:
- 하면 안 되는 조치:
- 복구 순서:
- 완료 검증:
5분 안에 확인할 것
- 저장 직후 이어지는 읽기가 replica로 가는지 확인한다.
- 해당 API가 stale read를 허용할 수 있는지 판단한다.
- replica lag 지표와 404 발생 시간을 맞춘다.
- 사용자별 primary stickiness 또는 request hint가 있는지 확인한다.
- 권한, 결제, 잔액처럼 최신성이 중요한 API도 replica를 타는지 점검한다.
원인 후보 분류
- read-after-write 요구가 있는 API를 replica로 routing한 문제
- lag 임계치 기반 fallback 정책이 없는 문제
- transaction commit 직후 응답과 replica 반영 사이의 시간 차이
- cache 또는 검색 인덱스 stale 문제와 replica lag가 섞인 문제
- routing 설정이 endpoint별 최신성 요구를 반영하지 못한 문제
하면 안 되는 조치
- 404를 단순 재시도만으로 숨기고 routing 정책을 그대로 두지 않는다.
- 모든 읽기를 무조건 primary로 되돌리기 전에 최신성 요구별로 나누지 않는다.
- replica lag 알림 없이 read/write split을 계속 확대하지 않는다.
- 사용자가 방금 쓴 데이터에 대해 “잠시 후 다시 시도”만 안내하고 끝내지 않는다.
복구 절차
- 저장 직후 상세 조회, 결제 상태, 권한 확인 등 최신성이 필요한 API를 primary로 보낸다.
- lag가 임계치를 넘으면 replica routing을 제한하는 fallback을 둔다.
- 사용자의 write 이후 일정 시간 primary stickiness를 적용한다.
- stale read 허용 API와 허용 불가 API를 문서화한다.
- replica lag와 404/이전 상태 응답을 함께 모니터링한다.
완료 검증
- 저장 직후 상세 조회 404가 정상 수준으로 돌아왔다.
- lag 임계치 초과 시 primary fallback이 동작한다.
- stale read 허용 API 목록과 예외 API 목록이 정리됐다.
- read/write routing 변경으로 primary 부하가 허용 범위 안에 있다.
회고 질문
- read replica 도입 전에 API별 최신성 요구를 분류했는가?
- 사용자 경험에서 “방금 쓴 데이터”의 기준이 정리되어 있는가?
- 인프라 proxy가 routing을 담당하더라도 애플리케이션이 최신성 힌트를 줄 수 있는가?
- lag 지표가 사용자 오류율과 연결되어 있는가?
연결 문서
- Case 17. read replica를 붙였더니 저장 직후 조회가 안 된다 - replica 도입을 일관성 정책으로 보는 기준을 본다.
- 03. Replica Lag와 읽기 일관성 상세 - lag 원인과 API별 routing 기준을 복습한다.
- 05. Cloud Pool Proxy 운영 상세 - proxy가 해결하는 문제와 못 하는 문제를 구분한다.
- 04. 백엔드 운영 체크포인트 상세 - API 증상과 운영 지표를 연결하는 방식을 본다.
가져갈 판단 기준
read replica의 첫 설계 질문은 “어떤 읽기가 늦어도 되는가”이다.
Incident 4. 대량 DELETE 이후 WAL과 replica lag가 같이 오른다
Incident 상황
개인정보 보관 기간 정책에 따라 오래된 로그를 삭제하는 배치가 새벽에 실행됐다. 삭제 후 WAL 생성량과 replica lag가 급증했고, vacuum 지연 경고도 나타났다.
현재 보이는 증상
- 삭제 배치가 예상보다 3배 오래 걸렸다.
- replica lag가 5초에서 18분까지 증가했다.
- WAL archive 사용량이 평소 대비 6배 증가했다.
- 일부 read replica 기반 관리자 화면이 오래된 데이터를 보여준다.
로그/지표/SQL 단서
delete from audit_log
where created_at < now() - interval '180 days';
-- affected rows: 42,800,000wal.generated.mb_per_min=4800
replica.lag.max=18m
autovacuum lag warning on audit_log
batch.timeout=none내 대응 초안
- 5분 안에 확인할 것:
- 원인 후보:
- 하면 안 되는 조치:
- 복구 순서:
- 완료 검증:
5분 안에 확인할 것
- 배치가 아직 실행 중인지, 중단 가능한지 확인한다.
- 삭제 범위와 실제 affected rows가 계획과 맞는지 확인한다.
- replica lag가 사용자 API에 영향을 주는지 확인한다.
- PITR 또는 백업 복구가 필요한 데이터 손실인지 판단한다.
- long transaction이 vacuum을 막고 있는지 확인한다.
원인 후보 분류
- 너무 큰 DELETE를 단일 transaction으로 실행한 문제
- batch size, sleep, timeout, 중단 기준이 없는 문제
- 삭제 후 vacuum, bloat, WAL, replica catch-up 부담을 고려하지 않은 문제
- read replica 기반 API가 lag를 허용하지 못하는 문제
- 보관 정책과 감사 요구를 delete 하나로만 처리한 문제
하면 안 되는 조치
- 영향 범위를 모른 채 동일 DELETE를 다시 실행하지 않는다.
- replica lag가 큰 상태에서 read traffic을 무조건 replica로 유지하지 않는다.
- 백업 가능성 확인 전에 추가 정정 update를 겹치지 않는다.
- vacuum을 강제로 실행하기 전에 DB 부하와 long transaction을 확인하지 않는다.
복구 절차
- 배치가 진행 중이면 안전한 중단 지점을 확인하고 멈춘다.
- 영향 API를 primary로 우회하거나 stale read 허용 화면만 replica를 쓰게 조정한다.
- 삭제 대상이 맞는지 audit, backup, sample query로 검증한다.
- 복구가 필요하면 전체 rollback보다 별도 복구 DB에서 필요한 범위만 추출하는 방안을 먼저 검토한다.
- 후속 작업은 작은 batch, sleep, lag 기준, 완료 검증을 포함한 runbook으로 재작성한다.
완료 검증
- replica lag가 정상 범위로 돌아왔다.
- WAL archive 사용량과 disk 여유가 안정화됐다.
- long transaction과 vacuum blocking이 해소됐다.
- 삭제 대상 count, 보관 정책, 감사 요구가 맞는지 검증됐다.
- 다음 실행 runbook에 batch size와 중단 기준이 추가됐다.
회고 질문
- 대량 DELETE 전에 예상 row 수와 WAL/replica 영향 추정이 있었는가?
- 삭제 대신 partition drop, soft delete, archive table 후보를 검토했는가?
- 인프라/DBA에게 작업 시간, batch, 중단 기준, 복구 계획을 공유했는가?
- 완료 검증이 “배치 성공”보다 데이터와 운영 지표를 함께 봤는가?
연결 문서
- Case 16. 잘못된 DELETE 후 복구 가능성을 판단해야 한다 - 삭제 후 복구 가능성을 판단하는 기준을 본다.
- Case 19. vacuum, WAL, replica lag 이야기가 백엔드 코드와 연결되지 않는다 - 백엔드 작업이 DB 운영 부담으로 이어지는 흐름을 본다.
- 04. Backup Restore PITR 상세 - PITR과 부분 복원 판단 기준을 복습한다.
- 05. Vacuum Bloat WAL 같은 운영 이슈 상세 - WAL, bloat, vacuum, replica lag를 함께 본다.
- 04. 이력 감사 소프트 삭제 상세 - 감사 로그와 삭제 정책을 설계 관점에서 다시 본다.
가져갈 판단 기준
대량 DELETE는 삭제 SQL이 아니라 복제, WAL, vacuum, 복구 계획을 포함한 운영 변경이다.
Part 3. 설계 리뷰 훈련
Review 1. 관리자 검색과 export API 설계안 리뷰
제안된 설계안
관리자 화면에서 주문을 검색하고 CSV로 내려받는 기능을 만든다.
GET /admin/orders?keyword=&status=&from=&to=&sort=
설계안
- 모든 조건은 하나의 동적 SQL로 처리한다.
- keyword는 주문자 이름, 전화번호, 주소에 LIKE '%keyword%'를 건다.
- Page<Order>로 전체 개수와 목록을 함께 반환한다.
- CSV export도 같은 API에서 limit 없이 조회한다.
- 운영 DB primary를 그대로 사용한다.내 리뷰 메모
- 겉보기 장점:
- 숨은 리스크:
- 리뷰 질문:
- 빠진 계약:
- 승인 조건:
겉보기 장점
- 구현이 빠르다.
- 화면 목록과 export가 같은 쿼리를 재사용한다.
Page를 쓰면 프론트에서 전체 개수와 페이지 UI를 만들기 쉽다.
숨은 리스크
LIKE '%keyword%'는 일반 B-Tree 인덱스를 잘 타지 못한다.- 동적 조건과 다양한 정렬 옵션은 인덱스 설계를 어렵게 만든다.
- count query가 목록보다 더 무거울 수 있다.
- export가 같은 pool과 primary를 쓰면 사용자 API까지 밀릴 수 있다.
- limit 없는 조회는 긴 transaction, connection 점유, memory 사용을 만들 수 있다.
리뷰 질문
- 관리자 검색에서 반드시 필요한 정렬 옵션은 무엇인가?
- keyword 검색은 정확히 어떤 필드와 어떤 검색 품질을 요구하는가?
- 전체 count가 매 요청마다 필요한가, 첫 페이지나 조건 확정 시에만 필요한가?
- export는 동기 응답이어야 하는가, 비동기 job으로 분리할 수 있는가?
- export가 read replica, 별도 pool, timeout, row limit을 가져야 하는가?
빠진 계약
- 최대 조회 기간과 export 최대 row 수
- 정렬 옵션별 지원 여부
- count 제공 조건
- export job의 SLA, 완료 알림, 재시도 정책
- 운영 DB와 replica 사용 정책
- 관리자 기능의 timeout과 pool 격리 기준
개선된 설계 방향
목록 API와 export API를 분리한다. 목록은 자주 쓰는 필터와 정렬 조합을 제한하고, 필요한 인덱스를 명확히 잡는다. keyword 검색은 필드별 prefix 검색, 별도 검색 엔진, full text 후보를 요구 수준에 따라 나눈다.
CSV export는 비동기 job으로 분리하고, 작업 크기 제한, 별도 pool, replica 사용 가능성, 진행률, 취소, 재시도, 다운로드 만료 정책을 둔다. count는 매 요청 기본값이 아니라 조건부 기능으로 다룬다.
승인 조건
- 지원 필터와 정렬 조합이 문서화되어 있다.
- EXPLAIN 기준으로 대표 쿼리 실행 계획을 확인했다.
- export는 사용자 API pool을 장시간 점유하지 않는다.
- row limit, timeout, 취소 기준이 있다.
- 운영 DB primary 사용 여부에 대해 인프라/DBA와 합의했다.
연결 문서
- Case 13. 관리자 export와 리포트가 운영 DB를 밀어붙인다 - 관리자 기능이 운영 DB에 주는 부담을 본다.
- 05. 대용량 목록 실전 상세 - 대용량 목록과 관리자 검색의 선택지를 확인한다.
- 03. 검색 LIKE Full Text 상세 - LIKE 검색과 full text, 검색 엔진 검토 기준을 본다.
- 03. 집계 서브쿼리 CTE Window 상세 - 실시간 집계와 API 응답의 경계를 복습한다.
- 04. Pool 고갈과 장애 전파 상세 - export가 pool 장애로 번지는 흐름을 본다.
가져갈 판단 기준
관리자 기능은 사용자가 적어 보여도 한 번의 무거운 조회로 운영 DB를 오래 붙잡을 수 있다.
Review 2. 대형 테이블 DDL과 인덱스 migration 설계안 리뷰
제안된 설계안
주문 테이블 2억 건에 payment_provider 컬럼을 추가하고, 결제사별 검색을 위해 인덱스를 만든다.
설계안
- 배포 1에서 orders.payment_provider not null default 'UNKNOWN' 컬럼을 추가한다.
- 같은 migration에서 기존 row를 모두 update한다.
- 바로 index(payment_provider, created_at)도 생성한다.
- 애플리케이션은 배포 직후 새 컬럼을 필수로 읽고 쓴다.
- 문제가 생기면 migration을 rollback한다.내 리뷰 메모
- 겉보기 장점:
- 숨은 리스크:
- 리뷰 질문:
- 빠진 계약:
- 승인 조건:
겉보기 장점
- 한 번의 배포로 schema와 애플리케이션 변경이 끝난다.
- 애플리케이션 코드는 새 컬럼이 항상 있다고 가정할 수 있다.
- 데이터 정합성도 빠르게 맞출 수 있어 보인다.
숨은 리스크
- 대형 테이블의
not null default와 backfill이 lock, WAL, replica lag를 만들 수 있다. - old app과 new app이 동시에 떠 있는 시간의 호환성이 없다.
- 인덱스 생성이 쓰기 부하와 storage를 크게 늘릴 수 있다.
- rollback이 실제로는 오래 걸리거나 불가능할 수 있다.
- migration 실패 시 애플리케이션과 DB 상태가 중간에 걸릴 수 있다.
리뷰 질문
- 컬럼 추가, 코드 쓰기, backfill, not null 강제, 인덱스 생성, cleanup을 단계로 나눴는가?
- old app이 새 컬럼이 없어도 동작하고, new app이 null 값을 만나도 동작하는가?
- 인덱스는 online/concurrent 생성이 가능한가?
- backfill batch size와 중단 기준은 무엇인가?
- rollback 대신 forward fix가 필요한 상황을 정의했는가?
빠진 계약
- 배포 단계별 호환성 계약
- backfill runbook과 완료 검증
- online DDL 가능 여부
- replica lag, lock wait, WAL 증가 중단 기준
- index 생성 시간과 disk 여유
- cleanup 시점과 owner
개선된 설계 방향
expand/contract 방식으로 나눈다. 먼저 nullable 컬럼을 추가하고 old/new app 호환성을 확보한다. 애플리케이션이 새 write를 시작한 뒤 backfill을 작은 batch로 수행한다. 검증이 끝난 뒤 not null, default, index, cleanup을 별도 단계로 진행한다.
인덱스는 실제 query와 실행 계획을 확인한 뒤 만든다. 생성 방식은 DB별 online DDL 또는 concurrent index 생성 가능 여부를 확인하고, 작업 시간과 중단 기준을 인프라/DBA와 합의한다.
승인 조건
- 각 단계가 독립적으로 배포, 중단, 재개 가능하다.
- old/new app 호환성 테스트가 있다.
- backfill 전후 검증 SQL이 있다.
- 인덱스 생성 방식과 작업 시간대가 합의됐다.
- rollback이 아니라 forward fix 기준까지 정리됐다.
연결 문서
- Case 14. 대형 테이블에 컬럼이나 인덱스를 추가해야 한다 - 대형 테이블 변경의 순서를 복습한다.
- 01. Schema Migration 원칙 상세 - migration 작성 원칙을 확인한다.
- 02. Zero Downtime DDL 상세 - 무중단 DDL과 호환성 배포를 본다.
- 04. 마이그레이션과 무중단 배포 상세 - expand and contract 사고방식을 복습한다.
- 05. 성장 가능한 스키마 상세 - 변경 가능한 schema 구조를 설계한다.
가져갈 판단 기준
대형 테이블 migration은 “한 번에 끝내기”보다 “각 단계가 안전하게 멈출 수 있기”가 더 중요하다.
Review 3. 인프라/DBA에게 보낼 DB 성능 개선 요청서 리뷰
제안된 설계안
슬로우 쿼리 알림이 왔다. 백엔드 담당자가 인프라 채널에 아래 메시지를 보내려 한다.
DB가 느린 것 같습니다. 확인 부탁드립니다.
주문 목록 API가 좀 느려졌습니다.
인덱스 하나 추가하면 될까요?내 리뷰 메모
- 겉보기 장점:
- 숨은 리스크:
- 리뷰 질문:
- 빠진 계약:
- 승인 조건:
겉보기 장점
- 빠르게 도움을 요청한다.
- 인프라/DBA에게 DB 내부 상태 확인을 넘길 수 있다.
- 장애 대응을 혼자 끌어안지 않는다.
숨은 리스크
- 어떤 API, 어떤 SQL, 어떤 시간대, 어떤 영향인지 알 수 없다.
- DBA가 실행 계획을 보려 해도 bind 값과 호출 패턴이 없다.
- 원하는 결정이 “확인”인지 “인덱스 승인”인지 “작업 시간 합의”인지 불명확하다.
- 최근 배포, batch, 데이터 증가 같은 백엔드 맥락이 빠져 있다.
- 인프라 쪽은 lock, replica lag, backup, capacity를 같이 걱정해야 하는데 판단 재료가 부족하다.
리뷰 질문
- 느린 endpoint와 시간대, 사용자 영향은 무엇인가?
- 실제 SQL과 대표 bind 값이 있는가?
- 전후 p95/p99, 호출량, error rate, Hikari 지표가 있는가?
- 실행 계획에서 rows, sort, join, filter가 어떻게 보이는가?
- 후보 인덱스와 기대 효과, 쓰기 비용, 생성 방식 질문이 포함되어 있는가?
- 내가 원하는 결정은 무엇인가?
빠진 계약
- 요청 종류: 진단, 인덱스 검토, emergency mitigation, migration 작업 중 무엇인지
- 영향 범위와 우선순위
- SQL fingerprint와 bind 패턴
- 실행 계획과 후보 개선안
- 변경 이력
- 작업 가능 시간대와 중단 기준
- 결정해야 할 owner
개선된 설계 방향
요청서는 아래처럼 사실, 추정, 요청 결정을 분리한다.
요청 종류: 주문 목록 API 인덱스 생성 검토
시간대: 2026-07-01 13:10~13:35 KST
영향: GET /api/me/orders p95 180ms -> 1.9s, p99 4.2s
SQL: user_id, status, created_at range, order by created_at desc, limit 20
대표 bind: user_id=..., status='PAID', from=2026-01-01
현재 계획: rows 추정 80만, Sort 발생, 기존 index는 user_id 단일
후보: (user_id, status, created_at desc, id desc)
확인 요청: online 생성 가능 여부, 예상 lock/lag, 작업 시간, 중단 기준
백엔드 조치: count 제거 검토, cursor pagination 전환 검토 중승인 조건
- DBA가 재현 가능한 SQL과 bind 값이 있다.
- 실행 계획과 현재 인덱스 목록이 있다.
- 후보 선택지와 트레이드오프가 있다.
- 원하는 결정과 작업 시간대가 명확하다.
- 인프라 쪽 우려인 lock, lag, disk, backup 영향을 물었다.
연결 문서
- DBA에게 “DB가 느려요”가 아니라 제대로 요청해야 한다 - 좋은 DB 협업 요청의 구성 요소를 본다.
- 05. 인프라 DBA와 협업하는 법 상세 - 요청 종류별 필요한 정보를 확인한다.
- 05. 실무 인덱스 설계 기준 상세 - 인덱스 후보를 리뷰하는 기준을 본다.
- 05. Slow Query와 운영 진단 상세 - 느린 쿼리를 애플리케이션 경로와 연결한다.
- 02. HikariCP와 Pool 크기 상세 - pool 지표와 DB 요청을 함께 설명한다.
가져갈 판단 기준
인프라/DBA에게는 추측보다 재현 가능한 SQL, 지표, 변경 이력, 원하는 결정을 전달한다.
최종 종합 훈련
종합 상황
프로모션 오픈 후 여러 증상이 동시에 나타났다.
타임라인
- D-1 23:00: orders 테이블 신규 인덱스 배포
- D-1 23:30: coupon_issue.normalized_code backfill 실행
- D-day 10:00: 프로모션 오픈
- D-day 10:05: 주문 목록 API p99 증가
- D-day 10:07: Hikari pending 증가
- D-day 10:09: 쿠폰 발급 deadlock 증가
- D-day 10:12: read replica lag 15초 이상
- D-day 10:15: 저장 직후 주문 상세 조회에서 간헐적 404제공된 Evidence
GET /api/me/orders p99=5.2s
POST /api/coupons/issue success_rate=91%
hikari.active=40, idle=0, pending=62
db.cpu=38%
lock.wait.count=high
deadlock.count=75/min
replica.lag.max=21s
wal.generated=high-- 느린 주문 목록 후보 SQL
select *
from orders
where user_id = :userId
order by created_at desc
limit 20 offset :offset;
-- 쿠폰 발급 처리
update promotion set remaining_count = remaining_count - 1 where id = :promotionId;
insert into coupon_issue(user_id, promotion_id, status, issued_at) values (...);작성할 답안
- 5분 triage 순서
- 원인 후보 분류
- 확인할 SQL/지표
- 즉시 완화
- 하면 안 되는 조치
- 복구 절차
- 완료 검증
- 회고 action
- 연결 문서
먼저 써보기
내 답안
- 5분 triage:
- 원인 후보 분류:
- 확인할 SQL/지표:
- 즉시 완화:
- 하면 안 되는 조치:
- 복구 절차:
- 완료 검증:
- 회고 action:
- 인프라/DBA에게 보낼 메시지 초안:
힌트
- 동시에 보이는 증상을 하나의 원인으로 합치지 않는다. 목록 API, 쿠폰 발급, connection pool, replica lag, 전날 migration을 분리해서 본다.
- 첫 5분은 근본 해결보다 영향 범위 축소와 증거 보존이 먼저다.
- DB CPU가 낮아도 lock wait, connection wait, replica lag, WAL은 충분히 장애를 만든다.
해설
첫 단계는 영향 범위를 줄이는 것이다. 쿠폰 발급은 rate limit 또는 queue로 순간 write를 낮추고, read-after-write가 필요한 주문 상세 조회는 primary로 routing한다. 주문 목록 API는 offset 깊은 페이지와 count를 제한하거나 임시로 Slice 방식으로 바꾸는 완화를 검토한다. 관리자 export나 batch가 같은 pool을 쓰고 있다면 중단 또는 격리를 확인한다.
원인 후보는 최소 다섯 갈래로 나눈다. 전날 인덱스 배포로 plan regression이 생겼는지, backfill이 WAL과 replica lag를 만들었는지, coupon hot row와 deadlock이 pool을 점유하는지, 주문 목록 offset과 count가 connection을 오래 잡는지, read replica routing 정책이 최신성 요구를 깨뜨렸는지 분리한다.
인프라/DBA와의 대화는 “DB가 느림”이 아니라 시간대, SQL, 실행 계획, lock wait, replica lag, WAL, 최근 migration, 원하는 결정을 포함해야 한다. 복구 완료는 API p99 하나만 보지 않는다. Hikari pending, deadlock count, replica lag, WAL 생성량, 주문/쿠폰 데이터 정합성, 사용자 오류율이 함께 정상화되어야 한다.
연결 문서
- Case 22. 장애 회고를 “인덱스 추가함”으로 끝내지 않으려 한다 - 복합 장애를 회고와 재발 방지로 연결한다.
- Case 11. Hikari connection timeout이 갑자기 늘어난다 - pool 대기와 DB 대기를 나눠 본다.
- Case 17. read replica를 붙였더니 저장 직후 조회가 안 된다 - read-after-write 정책을 점검한다.
- 05. Plan Regression과 운영 대응 상세 - 전날 인덱스 배포 후 plan 변화 가능성을 본다.
- 03. Timeout 계층 설계 상세 - timeout 계층과 장애 전파를 점검한다.
- 03. Backfill과 대량 작업 상세 - 전날 backfill의 운영 부담을 확인한다.
- 03. 장애 상황과 백엔드 대응 상세 - 장애 중 완화, 복구, 회고 순서를 복습한다.
- 00. 고급 DB 장애와 한계 압축 정리 - lock, connection, replica lag, WAL 같은 증폭 고리를 한 번에 훑는다.
가져갈 판단 기준
복합 장애에서는 가장 그럴듯한 하나를 고르는 대신, 사용자 영향 축소와 증거 수집을 병렬로 진행하며 후보를 줄인다.
반복 기록표
| 날짜 | 훈련 | 처음 판단 | 놓친 관점 | 다시 볼 문서 | 다음 액션 |
|---|---|---|---|---|---|
| Lab 1 | |||||
| Lab 2 | |||||
| Lab 3 | |||||
| Lab 4 | |||||
| Incident 1 | |||||
| Incident 2 | |||||
| Incident 3 | |||||
| Incident 4 | |||||
| Review 1 | |||||
| Review 2 | |||||
| Review 3 | |||||
| 최종 종합 훈련 |