Web-Database 실전 가이드북

이 문서의 사용법

이 문서는 Web-Database 문서들의 또 다른 요약본이 아니다. 실제 백엔드 개발자가 마주치는 상황을 먼저 던지고, 그 상황에서 어떤 증거를 모아야 하는지, 어떤 기존 문서를 다시 펼쳐야 하는지 안내하는 실전 길잡이다.

읽는 순서는 단순하다. 먼저 Case 제목 중 지금 겪는 상황과 가장 가까운 것을 고른다. 그다음 관찰할 증거로 로그, SQL, 실행 계획, 지표를 모으고, 먼저 생각해보기 질문에 스스로 답해본다. 마지막으로 찾아볼 문서 링크를 따라가 기존 상세 문서를 다시 읽으면 된다.

한 번에 모두 외우려고 하지 않아도 된다. DB 실력은 용어 암기보다 “지금 이 증상이 쿼리 문제인지, 락 문제인지, pool 문제인지, 스키마 변경 문제인지”를 나누는 감각에서 자란다. 이 문서는 그 감각을 반복 연습하기 위한 장치다.

개인 프로젝트에서는 설계 의도, migration, 인덱스, backup, pagination을 설명하는 포트폴리오 문서로 활용한다. 면접에서는 Case의 가져갈 한 문장을 자기 경험과 연결한다. 실무 장애 대응에서는 Case의 증거 목록을 그대로 초기 확인 체크리스트로 쓰고, 인프라/DBA 협업에서는 “제가 확인한 것”과 “같이 봐야 할 것”을 나누는 틀로 쓰면 좋다.

커버리지 지도

기존 문서연결된 Case다시 읽어야 하는 이유
00. Database 기본 구조와 백엔드 개발자의 관점 압축 정리Case 1, Case 12, Case 21DB를 기능 저장소가 아니라 요청 흐름과 운영 자원으로 보는 기준을 잡기 위해
01. DB가 요청 흐름에서 하는 일 상세Case 12, Case 20API 지연을 애플리케이션, pool, DB 실행 단계로 나누기 위해
02. Storage Buffer WAL Cache 상세Case 15, Case 19backfill, WAL, cache, disk I/O 영향을 백엔드 작업과 연결하기 위해
03. DB 한계와 비용 모델 상세Case 12, Case 18, Case 21CPU만이 아니라 lock, I/O, hot row, 동시성 한계를 비용 모델로 읽기 위해
04. 백엔드 운영 체크포인트 상세Case 17, Case 20, Case 22운영 지표와 협업 요청에 필요한 공통 체크포인트를 정리하기 위해
05. 개인 프로젝트와 기업 운영 수준 비교 상세Case 2, Case 21작은 프로젝트에서도 운영 수준의 설계 근거를 남기는 기준을 잡기 위해
00. Relational Model Table Key Constraint 압축 정리Case 1, Case 2테이블, key, constraint를 기능 설계의 출발점으로 다시 보기 위해
01. 테이블 행 컬럼 관계 상세Case 2화면 DTO를 그대로 테이블로 만들 때 생기는 모델링 위험을 보기 위해
02. Primary Foreign Unique Check 상세Case 1, Case 3, Case 9중복 방지와 불변식을 DB 제약으로 끝까지 막는 기준을 잡기 위해
03. 무결성과 제약 설계 상세Case 1, Case 9애플리케이션 검증과 DB 제약의 역할 분담을 판단하기 위해
04. 관계 모델링 판단 상세Case 2, Case 21관계, 생명주기, 조회 방향, FK 선택을 설계 근거로 설명하기 위해
05. 제약 조건과 운영 예외 처리 상세Case 3, Case 16Unique, FK, Check 위반을 운영 예외와 복구 흐름으로 연결하기 위해
00. SQL 기본과 쿼리 작성 원칙 압축 정리Case 3, Case 4SQL을 결과가 아니라 비용, 잠금, 복구 가능성으로 읽기 위해
01. SELECT WHERE JOIN GROUP BY 상세Case 4, Case 13목록, 조인, 집계가 운영 DB에 어떤 부담을 만드는지 보기 위해
02. INSERT UPDATE DELETE 안전한 변경 상세Case 3, Case 15, Case 16쓰기 SQL의 영향 row 수, lock, rollback, forward fix를 점검하기 위해
03. 집계 서브쿼리 CTE Window 상세Case 13, Case 18리포트, 집계, window function을 API와 운영 작업으로 구분하기 위해
04. 쿼리 작성 원칙과 리뷰 상세Case 5, Case 20, Case 22SQL 리뷰 기록을 협업과 회고의 증거로 남기는 방법을 익히기 위해
05. 운영 DB에서 직접 쿼리할 때 상세Case 3, Case 16, Case 20운영 정정 쿼리를 배포와 같은 수준의 변경으로 다루기 위해
00. Index 압축 정리Case 4, Case 5인덱스를 조회 속도뿐 아니라 쓰기 비용과 배포 위험까지 함께 보기 위해
01. 인덱스가 필요한 이유 상세Case 4, Case 20느린 API와 인덱스 요청을 증거 기반으로 설명하기 위해
02. B-Tree 인덱스와 탐색 원리 상세Case 4, Case 5동등 조건, 범위 조건, 정렬 조건이 인덱스와 어떻게 맞는지 보기 위해
03. 복합 인덱스와 컬럼 순서 상세Case 4, Case 6WHERE, ORDER BY, LIMIT을 하나의 복합 인덱스 전략으로 묶기 위해
04. 인덱스가 실패하는 패턴 상세Case 5, Case 7함수, 타입 불일치, 앞쪽 와일드카드, 낮은 선택도 문제를 확인하기 위해
05. 실무 인덱스 설계 기준 상세Case 1, Case 20, Case 21인덱스 추가를 운영 변경 요청으로 다루는 기준을 익히기 위해
00. Execution Plan Query Optimizer 압축 정리Case 4, Case 12느린 API에서 실제 SQL, bind, 실행 계획을 먼저 확보하기 위해
01. EXPLAIN 읽는 법 상세Case 4, Case 12scan, sort, join, rows estimate를 보고 병목 지점을 찾기 위해
02. Optimizer와 Statistics 상세Case 12, Case 22데이터 분포, 통계, 특정 파라미터가 plan을 바꾸는 이유를 보기 위해
03. Join Sort Aggregate 비용 상세Case 8, Case 13최종 20건 뒤에 숨어 있는 join row, sort, aggregate 비용을 보기 위해
04. 느린 쿼리 분석 루틴 상세Case 10, Case 12, Case 22slow query, lock wait, pool wait를 분리하는 진단 순서를 익히기 위해
05. Plan Regression과 운영 대응 상세Case 22어제 빠르던 쿼리가 오늘 느려진 상황을 회고와 기준선으로 다루기 위해
00. Transaction 압축 정리Case 9, Case 15트랜잭션을 정합성 경계이자 connection/lock 점유 시간으로 보기 위해
01. ACID와 실제 보장 상세Case 3, Case 16DB 롤백과 외부 부작용, 복구 가능성을 구분하기 위해
02. 트랜잭션 경계와 백엔드 코드 상세Case 9, Case 11외부 I/O와 긴 작업이 DB 커넥션과 lock을 오래 잡는 흐름을 보기 위해
03. Rollback Retry Idempotency 상세Case 9, Case 16재시도, 멱등 키, Unique 제약, 보상 처리를 함께 설계하기 위해
04. Spring Transaction과 DB 연결 상세Case 7, Case 11@Transactional, flush, proxy, connection 반납 시점을 DB 지표와 연결하기 위해
05. 긴 트랜잭션 위험 상세Case 11, Case 13, Case 19export, batch, streaming 조회가 pool, lock, vacuum에 미치는 영향을 보기 위해
00. Isolation Level Lock Concurrency 압축 정리Case 9, Case 10격리 수준과 lock을 동시성 설계 선택지로 바라보기 위해
01. Isolation Level과 이상 현상 상세Case 9재고, 쿠폰, 포인트에서 count 후 insert가 왜 위험한지 보기 위해
02. Row Lock Table Lock Gap Lock 상세Case 10, Case 14row, range, metadata lock이 백엔드 SQL 조건과 어떻게 연결되는지 보기 위해
03. Deadlock과 Lock Wait 상세Case 10deadlock 로그를 코드 경로, 접근 순서, 재시도 정책으로 되돌리기 위해
04. 동시성 제어 패턴 상세Case 9, Case 18Unique, 원자적 update, 낙관적/비관적 락, 큐잉을 상황별로 고르기 위해
05. 재고 포인트 카운터 사례 상세Case 9, Case 18, Case 21숫자 컬럼이라도 재고, 포인트, 카운터의 요구가 다름을 익히기 위해
00. Schema Design Normalization Denormalization 압축 정리Case 1, Case 21스키마 설계를 정합성, 조회 패턴, 변경 가능성까지 포함해 보기 위해
01. 정규화와 반정규화 상세Case 2, Case 13snapshot, projection, cache 성격의 중복 데이터를 구분하기 위해
02. 엔티티 관계와 카디널리티 상세Case 2, Case 7관계 크기와 조회 방향이 JPA, 인덱스, pagination에 미치는 영향을 보기 위해
03. 데이터 타입과 제약 선택 상세Case 1, Case 14금액, 시간, 상태값, 식별자를 운영 가능한 계약으로 설계하기 위해
04. 이력 감사 소프트 삭제 상세Case 16, Case 21삭제, 복구, 감사, 개인정보 보관 정책을 DB 설계와 연결하기 위해
05. 성장 가능한 스키마 상세Case 14, Case 19, Case 21expand/contract와 JSON 승격 기준으로 스키마 진화를 준비하기 위해
00. Pagination Sorting Search 압축 정리Case 4, Case 5, Case 6목록 API를 pagination, search, count, sort 비용으로 나눠 보기 위해
01. Offset vs Cursor 상세Case 4, Case 6offset, cursor, Page, Slice 선택 기준을 잡기 위해
02. ORDER BY와 인덱스 상세Case 4, Case 5정렬이 인덱스와 맞지 않을 때 limit 이전에 비용이 커지는 이유를 보기 위해
03. 검색 LIKE Full Text 상세Case 5LIKE '%keyword%', full-text, 검색 엔진 도입 기준을 익히기 위해
04. 무한 스크롤 API 설계 상세Case 4, Case 5cursor token, tie-breaker, filter 검증을 API 계약으로 다루기 위해
05. 대용량 목록 실전 상세Case 4, Case 13사용자 목록, 관리자 검색, export를 서로 다른 운영 경로로 분리하기 위해
00. Connection Pool Timeout 장애 전파 압축 정리Case 11, Case 12pool timeout을 원인과 결과 양쪽으로 읽기 위해
01. DB 커넥션이 비싼 이유 상세Case 11, Case 12connection 보유 시간이 API 전체 지연으로 번지는 흐름을 보기 위해
02. HikariCP와 Pool 크기 상세Case 11, Case 20pool size를 전체 DB connection budget으로 계산하기 위해
03. Timeout 계층 설계 상세Case 11, Case 12HTTP, transaction, query, pool timeout의 실패 순서를 맞추기 위해
04. Pool 고갈과 장애 전파 상세Case 11, Case 12특정 병목이 정상 API까지 전파되는 경로를 이해하기 위해
05. Cloud Pool Proxy 운영 상세Case 20, Case 21RDS Proxy, Cloud SQL Pooling, 인프라 연결 예산을 협업 언어로 다루기 위해
00. ORM과 Database JPA 관점 압축 정리Case 7, Case 8JPA 코드가 실제 SQL, flush, N+1, 인덱스로 변환되는 흐름을 보기 위해
01. ORM은 SQL을 없애지 않는다 상세Case 7, Case 20Repository 메서드와 SQL fingerprint를 연결하는 습관을 만들기 위해
02. 영속성 컨텍스트 Flush Dirty Checking 상세Case 7, Case 15flush 시점, bulk update, 대량 처리에서 DB 상태와 메모리 상태를 구분하기 위해
03. N+1 Fetch Join EntityGraph 상세Case 7, Case 8N+1 해결책별 trade-off와 pagination 위험을 보기 위해
04. 트랜잭션 범위와 Lazy Loading 상세Case 7, Case 11OSIV, lazy loading, DTO 경계가 connection 보유 시간과 연결되는 지점을 보기 위해
05. JPA 쿼리와 인덱스 상세Case 6, Case 7Query Method, JPQL, Querydsl, native query를 인덱스와 함께 리뷰하기 위해
00. Migration 운영 성능 진단 압축 정리Case 14, Case 15, Case 16, Case 22migration, backfill, backup, 성능 진단을 운영 절차로 묶기 위해
01. Schema Migration 원칙 상세Case 14migration 도구와 운영 안전성이 다른 문제임을 이해하기 위해
02. Zero Downtime DDL 상세Case 14대형 테이블 DDL, 온라인 인덱스, 롤링 배포 호환성을 확인하기 위해
03. Backfill과 대량 작업 상세Case 15, Case 19대량 변경을 batch, sleep, 진행률, 중단 기준으로 운영하기 위해
04. Backup Restore PITR 상세Case 16, Case 21백업 존재와 실제 복구 가능성을 구분하기 위해
05. Slow Query와 운영 진단 상세Case 12, Case 22DB 지표와 endpoint, Repository, 배포 이력을 연결하기 위해
00. 실전 DB 시나리오 압축 정리Case 20, Case 22실전 대응에서 증거, 완화, 근본 해결을 분리하기 위해
01. 신규 기능 개발 시 DB 설계 판단 상세Case 1, Case 21신규 기능 설계를 불변식, QPS, migration, 복구 기준까지 확장하기 위해
02. 느린 API와 슬로우 쿼리 대응 상세Case 12, Case 22느린 API를 SQL, pool, lock, 배포 타임라인으로 진단하기 위해
03. 장애 상황과 백엔드 대응 상세Case 10, Case 22장애 중 완화와 원인 분석을 섞지 않는 대응 순서를 익히기 위해
04. 마이그레이션과 무중단 배포 상세Case 14, Case 15DB migration을 코드 배포, backfill, cleanup으로 쪼개기 위해
05. 인프라 DBA와 협업하는 법 상세Case 20요청 종류별로 어떤 정보를 인프라/DBA에게 가져갈지 익히기 위해
00. 고급 DB 장애와 한계 압축 정리Case 17, Case 18, Case 19, Case 22드문 장애를 DB 내부 용어와 백엔드 코드 패턴으로 연결하기 위해
01. Lock 경합과 Deadlock 상세Case 10lock 경합과 deadlock을 코드 경로, SQL 조건, 인덱스로 되돌리기 위해
02. Connection 고갈과 장애 전파 상세Case 11, Case 12connection 고갈이 장애 증폭기가 되는 흐름을 보기 위해
03. Replica Lag와 읽기 일관성 상세Case 17read/write split에서 최신성 요구사항을 API별로 나누기 위해
04. Hot Row와 Write 병목 상세Case 18같은 row에 몰리는 write 병목을 구조적으로 완화하기 위해
05. Vacuum Bloat WAL 같은 운영 이슈 상세Case 19긴 트랜잭션, 대량 변경, WAL, vacuum, replica lag를 함께 보기 위해

Case 1. 신규 기능을 설계하는데 테이블부터 만들고 싶어진다

상황

  • 쿠폰, 주문, 포인트 같은 새 기능을 만들 때 화면 DTO와 엔티티 이름이 먼저 떠오른다.
  • 팀에서는 “테이블만 빨리 만들고 API 붙이자”고 말하지만, 이 기능은 중복 요청, 재고 제한, 관리자 조회, 장애 후 정정까지 연결될 수 있다.

관찰할 증거

  • 기능 요구사항에서 깨지면 안 되는 불변식을 적는다.
  • 예상 쓰기 피크, 읽기 패턴, 관리자 검색 조건, 보관 기간, 복구 필요성을 분리한다.
기능: 이벤트 쿠폰 발급
불변식: 회원은 이벤트당 1회만 발급 가능, 재고는 0 미만 불가
쓰기 피크: 오픈 직후 3분간 1,000 RPS 예상
읽기 패턴: 내 쿠폰 목록 최신순, 관리자 발급 현황
운영 질문: 실패 재처리, 중복 요청, backfill, 복구 기준

먼저 생각해보기

  • 이 기능에서 DB가 최종 방어선으로 막아야 하는 규칙은 무엇인가?
  • 목록 조회의 기본 정렬과 필터는 무엇이며, 그 쿼리는 어떤 인덱스를 요구하는가?
  • 트래픽이 커졌을 때 hot row가 될 만한 row는 어디인가?

찾아볼 문서

정리

  • 테이블은 설계의 출발점이 아니라 요구사항을 DB 불변식, 조회 패턴, 트랜잭션 경계, 운영 절차로 번역한 결과물이다.
  • 작게 시작하더라도 Unique, Not Null, 상태값, 인덱스, migration 계획을 의도적으로 남기면 나중에 기능이 커져도 설명 가능한 구조가 된다.

가져갈 한 문장

  • 새 기능의 DB 설계는 “무엇을 저장할까”보다 “무엇이 깨지면 안 되고, 어떤 경로가 가장 자주 읽고 쓰는가”에서 시작한다.

Case 2. 화면 DTO를 그대로 테이블로 만들고 싶어진다

상황

  • 프론트 화면에는 회원 이름, 주문 수, 최근 결제 상태, 배송 요약이 한 카드에 보인다.
  • 이 모양을 그대로 member_order_cards 같은 테이블로 만들면 빠르게 구현될 것 같지만, 어떤 값이 원본이고 어떤 값이 snapshot인지 불분명해진다.

관찰할 증거

  • 각 필드가 현재 값인지, 과거 snapshot인지, 재계산 가능한 projection인지 표시한다.
  • 1:N 관계의 N 최대 크기, 주요 조회 방향, 삭제와 보관 정책을 확인한다.

먼저 생각해보기

  • 이 중복 데이터는 성능을 위한 projection인가, 계약 기록을 위한 snapshot인가?
  • 원본 값이 바뀌면 이 테이블은 누가, 언제, 어떻게 갱신하는가?
  • 불일치가 생겼을 때 원본에서 다시 만들 수 있는가?

찾아볼 문서

  • 반정규화가 적합한 경우 - 의미 있는 중복과 위험한 중복을 구분한다.
  • 원리 - 관계 크기가 API, JPA, 인덱스에 미치는 영향을 본다.
  • 개요 - 테이블을 비즈니스 사실 단위로 쪼개는 기본을 확인한다.
  • 실전 팁 - 작은 프로젝트에서도 설계 의도를 문서화하는 기준을 잡는다.

정리

  • 화면은 데이터를 보여주는 결과이고, 테이블은 비즈니스 사실과 변경 책임을 저장하는 구조다.
  • 반정규화를 도입한다면 원본, 갱신 경로, 검증 쿼리, rebuild 방법을 같이 설계해야 한다.

가져갈 한 문장

  • 화면 모양이 아니라 데이터의 소유권과 생명주기가 테이블 경계를 결정한다.

Case 3. 안전한 쓰기 API와 운영 정정 쿼리를 설계해야 한다

상황

  • 주문 상태를 일괄 만료시키거나, 잘못 적립된 포인트를 정정해야 한다.
  • 개발 코드에서는 save() 한 줄이지만 운영 DB에서는 lock, WAL, 영향 row 수, 복구 가능성이 모두 문제가 된다.

관찰할 증거

  • 같은 조건의 SELECT count(*), 샘플 조회, 실행 계획, 예상 lock 범위, backup/PITR 상태를 확인한다.
SELECT count(*)
FROM orders
WHERE status = 'CREATED'
  AND created_at < timestamp '2026-01-01';

먼저 생각해보기

  • 이 변경은 실패하면 rollback이 나은가, forward fix가 나은가?
  • 상태 전이 조건이 SQL에 포함되어 있는가?
  • 영향 row 수가 0일 때 도메인적으로 어떤 의미인가?

찾아볼 문서

정리

  • 쓰기 SQL은 “몇 row가 바뀌는가”와 “실수했을 때 어떻게 증명하고 복구하는가”가 핵심이다.
  • 운영 정정은 배포와 같은 수준의 변경이므로 목적, 대상, 검증, 중단 기준, 복구 전략을 기록해야 한다.

가져갈 한 문장

  • 운영 쓰기 쿼리는 실행 전보다 실행 후를 더 많이 준비해야 안전하다.

Case 4. 회원별 주문 목록 API가 데이터 증가 후 느려진다

상황

  • GET /members/{id}/orders는 개발 DB에서 빨랐지만, 운영에서 데이터가 늘자 p99가 튄다.
  • 응답은 20건인데 DB는 훨씬 많은 row를 읽고 정렬할 수 있다.

관찰할 증거

  • 실제 SQL, bind 값, Page가 만든 count 쿼리, 실행 계획의 rows, sort, index 사용 여부를 확인한다.
EXPLAIN
SELECT id, status, created_at
FROM orders
WHERE member_id = 10
ORDER BY created_at DESC, id DESC
LIMIT 20;

먼저 생각해보기

  • 이 API는 전체 페이지 수가 필요한가, 다음 페이지 존재 여부만 알면 되는가?
  • WHERE + ORDER BY + LIMIT을 한 인덱스 전략으로 지원하는가?
  • 특정 회원만 느리다면 데이터 분포가 치우친 것은 아닌가?

찾아볼 문서

정리

  • 목록 API는 반환 건수보다 “그 20건을 찾기 위해 얼마나 읽고 정렬했는가”가 중요하다.
  • 사용자 피드는 cursor, 관리자 검색은 제한된 offset 또는 비동기 export처럼 UX와 DB 비용을 함께 선택한다.

가져갈 한 문장

  • 목록 API의 성능은 LIMIT 20이 아니라 limit 이전에 DB가 버린 row 수가 결정한다.

Case 5. 검색 기능을 LIKE '%keyword%'로 만들었는데 점점 버거워진다

상황

  • 상품명 검색을 급히 만들기 위해 LIKE '%keyword%'를 사용했다.
  • 데이터가 적을 때는 문제가 없었지만, 검색어가 짧거나 트래픽이 늘면서 DB CPU와 slow query가 증가한다.

관찰할 증거

  • 검색어 길이 분포, prefix/contains/full-text 요구사항, 실행 계획의 rows와 임시 정렬, 검색 API 호출량을 본다.

먼저 생각해보기

  • 사용자가 원하는 것은 prefix 검색인가, 부분 검색인가, 관련도순 검색인가?
  • 검색어 최소 길이와 rate limit이 있는가?
  • 검색 결과는 최신순이 중요한가, 관련도순이 중요한가?

찾아볼 문서

정리

  • 검색은 단순 문자열 조건이 아니라 제품 요구사항이다.
  • DB full-text, trigram, 외부 검색 엔진은 성능만이 아니라 동기화 지연, 권한 필터, 재색인 절차까지 함께 판단해야 한다.

가져갈 한 문장

  • %keyword%는 임시 구현일 수는 있어도 검색 요구사항의 최종 설계라고 보기는 어렵다.

Case 6. Pageable을 붙였더니 count 쿼리가 병목이 된다

상황

  • Spring Data Pageable을 붙였더니 목록 data query는 빠른데 count(*)가 느리다.
  • 화면에서는 전체 페이지 수를 거의 쓰지 않는데 API는 매번 전체 count를 계산한다.

관찰할 증거

  • data query와 count query를 분리해 시간, 실행 계획, 호출 빈도를 비교한다.
data query p95: 45ms
count query p95: 1.9s
UX 사용: hasNext만 필요

먼저 생각해보기

  • 전체 count가 사용자 의사결정에 실제로 필요한가?
  • Slice, cursor, 지연 count, 캐시 count로 요구를 만족할 수 있는가?
  • JPA가 만든 count query가 조인이나 필터를 과하게 포함하지 않는가?

찾아볼 문서

정리

  • Page는 편하지만 “전체 페이지 수”라는 제품 비용을 자동으로 떠안는다.
  • count가 병목이면 UX 요구를 먼저 확인하고, 필요 없는 count를 제거하는 것이 인덱스보다 더 나은 해결일 수 있다.

가져갈 한 문장

  • 전체 count는 무료 메타데이터가 아니라 별도의 대형 쿼리일 수 있다.

Case 7. JPA Repository 메서드는 짧은데 SQL이 너무 많이 나간다

상황

  • Repository 코드는 깔끔하지만 API 한 번에 SQL이 수십 번 나간다.
  • query 하나하나는 빠르지만 전체 API p95가 느리고 pool 사용 시간이 길다.

관찰할 증거

  • API별 query count, 반복 SQL fingerprint, lazy loading 발생 위치, flush 시점, OSIV 설정을 확인한다.

먼저 생각해보기

  • 이 API 응답에 필요한 데이터 모양은 엔티티 그래프인가 DTO인가?
  • 반복 SQL은 N+1인가, batch fetch로 줄일 수 있는가?
  • Controller나 JSON 직렬화 단계에서 lazy loading이 발생하는가?

찾아볼 문서

정리

  • JPA 문제는 Java 코드만으로 찾기 어렵다. SQL 로그, query count, 실행 계획, pool acquire time을 함께 봐야 한다.
  • fetch join, EntityGraph, batch size, DTO projection은 서로 다른 도구이며, 목록/상세/대량 조회마다 선택이 달라진다.

가져갈 한 문장

  • ORM 코드는 짧아질 수 있지만 DB가 실행한 SQL의 비용은 사라지지 않는다.

Case 8. N+1을 fetch join으로 고쳤더니 pagination이 이상해진다

상황

  • 주문 목록에서 N+1이 보여 fetch join을 추가했다.
  • 쿼리 수는 줄었지만 pagination 결과가 중복되거나, 메모리 사용량이 늘거나, DB row 수가 폭증한다.

관찰할 증거

  • API 응답 객체 수와 DB 조인 row 수를 분리한다.
  • 컬렉션 fetch join 여부, distinct, limit 적용 위치, 실행 계획의 join row와 sort 비용을 확인한다.

먼저 생각해보기

  • 이 API는 목록인가, 단건 상세인가?
  • 컬렉션을 한 번에 가져와야 하는가, ID 목록 조회 후 별도 batch fetch가 나은가?
  • DTO projection으로 필요한 컬럼만 가져올 수 있는가?

찾아볼 문서

정리

  • N+1을 없애는 것이 곧 최적화의 끝은 아니다.
  • 쿼리 수, 읽은 row 수, 응답 p95, DB CPU/I/O, pagination 안정성을 함께 비교해야 한다.

가져갈 한 문장

  • fetch join은 쿼리 수를 줄일 수 있지만, 목록 API에서는 row 폭증과 pagination 문제를 새로 만들 수 있다.

Case 9. 쿠폰, 재고, 포인트에서 동시 요청이 몰린다

상황

  • 이벤트 오픈 직후 같은 쿠폰과 상품에 요청이 몰린다.
  • 단일 요청 테스트는 통과했지만, 운영에서는 중복 발급, 초과 차감, 재시도 중복이 문제가 된다.

관찰할 증거

  • 동시 요청 수, update 성공 row 수, Unique 위반, deadlock/retry count, hot row update latency를 확인한다.
UPDATE products
SET stock = stock - 1
WHERE id = :productId
  AND stock > 0;

먼저 생각해보기

  • 막아야 하는 불변식은 중복 방지인가, 초과 차감 방지인가, 감사 가능한 이력인가?
  • 같은 요청이 timeout 후 재시도되어도 안전한가?
  • 정확한 즉시 값이 필요한가, 지연 집계가 가능한가?

찾아볼 문서

  • 원리 - 불변식별 동시성 패턴을 고른다.
  • 비교 기준 - 재고, 포인트, 카운터를 나눠 본다.
  • Idempotency Key - retry와 idempotency key를 설계한다.
  • 원리 - 최종 중복 방지를 DB 제약에 둔다.
  • Phantom Read - count 후 insert의 위험을 확인한다.

정리

  • 동시성 설계는 격리 수준을 올리는 일이 아니라 불변식을 가장 짧고 강하게 지키는 방법을 고르는 일이다.
  • 재고는 조건부 update, 중복은 Unique, 포인트는 ledger와 idempotency, 카운터는 지연 집계처럼 요구별로 시작점이 다르다.

가져갈 한 문장

  • 숫자 컬럼 하나를 업데이트해도 재고, 포인트, 카운터는 서로 다른 정합성 계약을 가진다.

Case 10. deadlock과 lock wait가 간헐적으로 발생한다

상황

  • 하루 몇 번씩 deadlock 로그가 남고, 특정 시간대에는 lock wait로 API p99가 급증한다.
  • 재시도를 넣으면 성공률은 올라가지만, 원인이 줄어드는지는 불분명하다.

관찰할 증거

  • deadlock 로그의 두 트랜잭션 SQL, lock 획득 순서, endpoint, batch 시간대, blocking query, 긴 트랜잭션을 모은다.
time: 14:10~14:18 KST
API: POST /teams/{source}/merge/{target}
batch: team member rebalance
need: deadlock log의 두 transaction SQL과 접근 순서 확인

먼저 생각해보기

  • 여러 row나 table을 항상 같은 순서로 잠그고 있는가?
  • 사용자 API와 batch가 같은 데이터를 반대 순서로 갱신하는가?
  • 재시도 대상 작업은 멱등적이고 짧은가?

찾아볼 문서

정리

  • deadlock은 가능한 실패 모드지만, 빈번한 deadlock은 접근 순서, 트랜잭션 경계, 인덱스, batch 충돌을 다시 보라는 신호다.
  • 재시도는 짧고 멱등적인 작업에 제한하고, 발생 빈도와 원인 SQL은 별도 개선 작업으로 남긴다.

가져갈 한 문장

  • deadlock은 재시도 코드로 숨길 수 있지만, 빈도는 설계 품질을 말해준다.

Case 11. Hikari connection timeout이 갑자기 늘어난다

상황

  • 여러 API에서 Connection is not available 오류가 증가한다.
  • DB CPU는 높지 않거나 애매한데, Hikari active가 maximum에 붙고 pending이 늘어난다.

관찰할 증거

  • active/idle/pending, acquire time, timeout count, slow query, lock wait, long transaction, 최근 배포와 batch를 같은 타임라인에 둔다.

먼저 생각해보기

  • 커넥션을 오래 잡는 쿼리, lock wait, 외부 API 포함 트랜잭션이 있는가?
  • pool size를 키우면 DB가 실제로 더 많은 동시 쿼리를 감당할 수 있는가?
  • timeout이 어느 계층에서 먼저 발생해야 하는가?

찾아볼 문서

정리

  • Hikari timeout은 DB 접속 실패처럼 보이지만 실제로는 pool에서 커넥션을 못 빌린 결과다.
  • pool을 키우기 전에 무엇이 connection을 오래 잡고 있는지 찾아야 한다.

가져갈 한 문장

  • connection timeout은 “시간을 늘려 달라”가 아니라 “커넥션 보유 시간이 길어졌다”는 관측 신호다.

Case 12. DB CPU는 낮은데 API가 느리다

상황

  • API p99와 gateway timeout이 증가하지만 DB CPU는 40% 수준이다.
  • 그래서 “DB 문제는 아닌가?”라고 생각하기 쉽지만, lock wait나 pool wait는 CPU를 많이 쓰지 않아도 요청을 멈춘다.

관찰할 증거

  • pool acquire time, pending, lock wait, active session wait event, network latency, long transaction, slow query 호출량을 함께 본다.
API p99: 350ms -> 4.2s
DB CPU: 42%
Hikari pending: 0 -> 80
wait event: Lock 일부 관측
최근 변경: 주문 목록 fetch 전략 변경

먼저 생각해보기

  • 요청은 DB에서 실행 중인가, pool 앞에서 기다리는가, lock 앞에서 기다리는가?
  • 특정 endpoint만 느린가, pool 공유 때문에 정상 API까지 느린가?
  • 최근 배포가 query count나 transaction 범위를 바꾸었는가?

찾아볼 문서

정리

  • CPU가 낮다는 사실은 “병목이 없다”가 아니라 “CPU가 아닌 곳에서 기다릴 수 있다”는 뜻이다.
  • API 지연은 query 실행 시간, lock wait, pool acquire, external I/O, serialization을 나눠서 봐야 한다.

가져갈 한 문장

  • DB CPU가 낮아도 요청은 lock, pool, replica, network 앞에서 충분히 오래 멈출 수 있다.

Case 13. 관리자 export와 리포트가 운영 DB를 밀어붙인다

상황

  • 운영자가 CSV export를 눌렀고, 사용자 API도 함께 느려졌다.
  • export는 read-only라 안전하다고 여겼지만 긴 조회가 connection, transaction, sort, I/O를 오래 점유한다.

관찰할 증거

  • export SQL, 조회 기간, 반환 row 수, transaction 지속 시간, connection 보유 시간, replica 사용 여부, temp file/sort 지표를 본다.

먼저 생각해보기

  • 이 작업은 HTTP 요청 안에서 동기 처리해야 하는가?
  • primary가 아니라 replica, batch, object storage 다운로드 모델로 분리할 수 있는가?
  • export 기간, row 수, 실행 시간대 제한이 있는가?

찾아볼 문서

정리

  • 관리자 기능은 호출 빈도가 낮아도 한 번의 작업이 운영 DB를 오래 붙잡을 수 있다.
  • 대량 export는 job id 발급, 비동기 생성, 파일 다운로드, 별도 pool/replica, 중단 기준을 갖춘 업무로 보는 편이 안전하다.

가져갈 한 문장

  • read-only 작업도 오래 열리면 운영 자원을 쓰는 write만큼 부담스러울 수 있다.

Case 14. 대형 테이블에 컬럼이나 인덱스를 추가해야 한다

상황

  • orders 같은 대형 테이블에 새 컬럼이나 복합 인덱스를 추가해야 한다.
  • 개발 DB에서는 즉시 끝났지만, 운영에서는 lock, table rewrite, replica lag, rolling 배포 호환성이 문제다.

관찰할 증거

  • 테이블 row 수, DDL 종류, DB 버전, online DDL 지원 여부, lock timeout, replica lag 기준, 구버전 코드 호환성을 확인한다.

먼저 생각해보기

  • 이 DDL은 구버전 애플리케이션과 함께 동작하는가?
  • 새 컬럼 추가, backfill, 제약 추가, old column 제거를 한 번에 묶고 있지 않은가?
  • 실패했을 때 down migration보다 forward fix가 현실적인가?

찾아볼 문서

정리

  • 대형 테이블 DDL은 SQL 한 줄이 아니라 runbook이다.
  • 실행 시간대, 담당자, 중단 조건, 검증 쿼리, replica lag 기준, cleanup 계획까지 준비해야 한다.

가져갈 한 문장

  • 운영 DDL은 “컬럼 하나 추가”가 아니라 모든 애플리케이션 버전이 공유하는 스키마 변경이다.

Case 15. backfill을 해야 하는데 운영 트래픽이 걱정된다

상황

  • 새 컬럼을 채우거나 projection을 만들기 위해 수천만 row backfill이 필요하다.
  • 한 번에 update하면 빠를 것 같지만 lock, WAL, replica lag, rollback 부담이 커진다.

관찰할 증거

  • 대상 row 수, batch size, sleep, 진행률 저장 위치, lock wait, replica lag, DB CPU/I/O, 사용자 API p95를 정한다.
backfill:
  enabled: true
  batch-size: 5000
  sleep-ms: 200
  max-replica-lag-seconds: 5

먼저 생각해보기

  • 작업은 멱등적이며 중단 후 재시작 가능한가?
  • 사용자 API와 같은 pool을 쓰는가?
  • 어떤 지표가 임계치를 넘으면 즉시 멈출 것인가?

찾아볼 문서

정리

  • backfill은 빠른 SQL 하나가 아니라 조절 가능한 운영 작업이다.
  • 설정으로 속도를 낮추거나 멈출 수 있고, 진행률과 검증 쿼리가 있어야 인프라와 함께 안전하게 운전할 수 있다.

가져갈 한 문장

  • 좋은 backfill은 빨리 끝나는 작업보다 안전하게 멈추고 다시 시작할 수 있는 작업이다.

Case 16. 잘못된 DELETE 후 복구 가능성을 판단해야 한다

상황

  • 운영에서 잘못된 조건으로 데이터를 삭제했거나, 삭제 배치가 예상보다 넓게 실행됐다.
  • “백업이 있다”는 말만으로는 충분하지 않다. 어느 시점으로 얼마나 빨리, 어떤 범위만 복원할 수 있는지가 중요하다.

관찰할 증거

  • 삭제 시간, 영향 row 수, 백업/PITR 가능 시점, 삭제 후 정상 write 범위, 감사 로그, 원장/event log 존재 여부를 확인한다.

먼저 생각해보기

  • 전체 DB를 과거로 되돌리면 그 사이 정상 거래는 어떻게 되는가?
  • 별도 복구 DB에서 삭제된 데이터만 추출할 수 있는가?
  • forward fix가 가능한 데이터인가, 원장으로 재계산할 수 있는가?

찾아볼 문서

정리

  • 복구는 인프라만의 일이 아니다. 어떤 데이터가 재처리 가능한지, 어떤 외부 부작용이 있었는지, 어떤 상태 전이가 정상인지 백엔드가 설명해야 한다.
  • 잘못된 DELETE 대응은 현재 DB 보존, 별도 복구, 영향 범위 추출, 검증, 감사 기록 순서로 차분히 진행한다.

가져갈 한 문장

  • 백업이 있다는 말보다 “어느 시점의 어떤 데이터를 어떻게 현재 상태에 되돌릴 것인가”가 더 중요하다.

Case 17. read replica를 붙였더니 저장 직후 조회가 안 된다

상황

  • 읽기 부하를 줄이려고 replica를 붙였는데, 사용자가 글을 저장한 직후 상세 조회에서 404나 이전 상태를 본다.
  • 이것은 버그일 수도 있지만, read/write split 정책이 최신성 요구를 반영하지 못한 결과일 수 있다.

관찰할 증거

  • replica lag, read routing 규칙, 저장 직후 조회 API, 권한/잔액/결제 상태 조회 위치, fallback 정책을 확인한다.

먼저 생각해보기

  • 이 읽기는 stale read를 허용할 수 있는가?
  • 방금 쓴 사용자의 다음 읽기는 일정 시간 primary로 보내야 하는가?
  • lag가 임계치를 넘으면 replica routing을 어떻게 제한하는가?

찾아볼 문서

정리

  • replica는 읽기 성능 장치이지만 동시에 일관성 정책이다.
  • API별로 stale read 허용 여부를 명시하고, write 직후 primary stickiness나 lag 기반 routing 제한을 설계해야 한다.

가져갈 한 문장

  • read replica 도입의 첫 질문은 “어떤 읽기가 늦어도 되는가”이다.

Case 18. hot row 때문에 DB 스펙을 올려도 해결되지 않는다

상황

  • 인기 상품 재고, 게시글 좋아요 수, 포인트 잔액처럼 같은 row에 write가 몰린다.
  • DB 스펙을 올려도 lock wait와 update latency가 크게 줄지 않는다.

관찰할 증거

  • 특정 row update TPS, lock wait, retry count, deadlock, queue lag, API p99, DB CPU 대비 대기 비율을 확인한다.

먼저 생각해보기

  • 이 값은 반드시 즉시 정확해야 하는가?
  • 조건부 update, 분산 카운터, 이벤트 적재, 큐잉, 사전 할당 중 어떤 요구에 맞는가?
  • 재시도는 hot row를 더 세게 두드리고 있지 않은가?

찾아볼 문서

정리

  • hot row는 애플리케이션 서버를 늘려도 같은 DB 행에 직렬화되는 병목이다.
  • 정확성, 최신성, 감사 필요성에 따라 DB 원자 update, 큐, 분산 카운터, ledger, 비동기 집계를 선택한다.

가져갈 한 문장

  • 같은 row를 모두가 바꾸는 구조라면 스케일업보다 쓰기 집중을 줄이는 설계가 먼저다.

Case 19. vacuum, WAL, replica lag 이야기가 백엔드 코드와 연결되지 않는다

상황

  • 인프라/DBA가 WAL 증가, vacuum 지연, bloat, replica lag를 이야기한다.
  • 백엔드 입장에서는 DB 내부 용어처럼 들리지만, 긴 트랜잭션과 대량 update/delete는 코드에서 시작될 수 있다.

관찰할 증거

  • long transaction, idle in transaction, 대량 DML, streaming export, backfill batch size, WAL 생성량, replica lag를 작업 타임라인과 맞춘다.

먼저 생각해보기

  • 최근 긴 read transaction이나 export가 있었는가?
  • 대량 update/delete가 얼마나 많은 row version과 WAL을 만들었는가?
  • 작업 후 vacuum/analyze 또는 replica catch-up 시간을 고려했는가?

찾아볼 문서

정리

  • 운영 용어가 낯설어도 백엔드가 만든 긴 트랜잭션, batch, export, hot row가 DB 정리와 복제에 영향을 준다.
  • 인프라와 이야기할 때는 대상 row 수, batch size, transaction 길이, 중단 기준, lag 허용치를 가져가면 대화가 빨라진다.

가져갈 한 문장

  • vacuum과 WAL은 DB 내부 현상이지만, 그 입력값은 백엔드의 트랜잭션과 대량 변경일 때가 많다.

Case 20. 인프라/DBA에게 “DB가 느려요”가 아니라 제대로 요청해야 한다

상황

  • 장애나 인덱스 요청이 있을 때 “DB 확인 부탁드립니다”라고만 보내면 대화가 오래 걸린다.
  • DBA는 DB 내부 상태를 볼 수 있지만, 어떤 API가 어떤 비즈니스 맥락에서 어떤 SQL을 만드는지는 백엔드가 가장 잘 안다.

관찰할 증거

  • 시간대, endpoint, SQL, bind 패턴, 실행 계획, 호출량, p95/p99, pool 지표, 최근 배포/배치, 원하는 결정을 한 메시지에 담는다.
요청 종류: 인덱스 생성 검토
문제: GET /api/posts p95 300ms -> 2.5s
SQL: board_id 조건, created_at desc 정렬, limit 20
현재 계획: rows 120만, Sort 발생
후보: (board_id, created_at desc, id desc)
원하는 결정: 야간 온라인 생성 가능 여부와 중단 기준

먼저 생각해보기

  • 내가 확인한 사실과 내가 모르는 DB 내부 정보를 나누었는가?
  • 인프라가 판단해야 할 위험은 lock, replica lag, connection, backup 중 무엇인가?
  • 기능 중요도와 중단 가능성을 설명했는가?

찾아볼 문서

정리

  • 좋은 협업은 추측을 넘기는 것이 아니라 서로에게 없는 맥락을 제공하는 일이다.
  • 백엔드는 비즈니스 흐름과 코드 변경을, 인프라는 DB 내부 wait, 복제, 백업, 용량을 제공할 때 판단이 빨라진다.

가져갈 한 문장

  • “DB가 느려요” 대신 “이 시간대의 이 SQL과 이 지표를 함께 판단하고 싶습니다”라고 말하자.

Case 21. 개인 프로젝트를 운영 수준으로 보이게 만들고 싶다

상황

  • 개인 프로젝트라 트래픽은 작지만, 전문 백엔드 개발자답게 DB 설계와 운영 감각을 보여주고 싶다.
  • 거창한 인프라보다 중요한 것은 작은 서비스에서도 중요한 선택을 설명할 수 있는 흔적이다.

관찰할 증거

  • ERD, migration history, 핵심 제약, 주요 목록 API 인덱스, cursor pagination, backup/restore 절차, slow query 확인 루틴이 있는지 본다.

먼저 생각해보기

  • README나 기술 문서에서 “왜 이 인덱스가 필요한가”를 설명할 수 있는가?
  • 중복 방지와 상태 전이를 DB 제약과 코드로 함께 막고 있는가?
  • 백업을 실제로 복구해본 기록이 있는가?

찾아볼 문서

정리

  • 개인 프로젝트에서 기업 수준의 모든 장치를 다 갖출 필요는 없다.
  • 대신 migration, 제약, 인덱스, pagination, backup, 장애 대응 메모처럼 운영 판단의 흔적을 남기면 단순 CRUD와 분명히 달라진다.

가져갈 한 문장

  • 개인 프로젝트의 DB 완성도는 기능 수보다 선택 이유와 복구 가능성을 설명할 수 있는지에서 드러난다.

Case 22. 장애 회고를 “인덱스 추가함”으로 끝내지 않으려 한다

상황

  • 장애 후 인덱스를 추가했고 지표는 좋아졌다.
  • 하지만 왜 그 plan이 나왔는지, 왜 관측이 늦었는지, 어떤 완화가 효과였는지 남기지 않으면 다음 장애에서 다시 헤맨다.

관찰할 증거

  • 장애 타임라인, 사용자 영향, SQL fingerprint, 전후 실행 계획, pool/lock/DB 지표, 배포/배치 이력, 완화 조치별 기대 지표와 실제 결과를 모은다.

먼저 생각해보기

  • 이 문제는 쿼리 구조, 인덱스, 데이터 분포, 통계, API 계약, 운영 절차 중 무엇이 핵심이었는가?
  • 장애 중 어떤 증거가 없어서 판단이 늦었는가?
  • 임시 완화가 영구 설정으로 남지 않도록 제거 조건이 있는가?

찾아볼 문서

정리

  • 좋은 회고는 “무엇을 고쳤다”보다 “어떤 증거로 무엇을 판단했고, 다음에는 어떤 관측과 제한으로 더 빨리 대응할 것인가”를 남긴다.
  • 인덱스 추가가 해결책이었더라도 API 계약, count 제거, timeout, pool 분리, backfill 절차, 알림 기준까지 재발 방지 후보로 검토한다.

가져갈 한 문장

  • 장애 회고의 목적은 정답을 자랑하는 것이 아니라 다음 판단 시간을 줄이는 것이다.

반복해서 떠올릴 판단 기준

  • SQL은 결과가 맞는지만 보지 않는다. 읽은 row 수, 정렬, join 중간 결과, lock 범위, 호출 빈도, pool 점유 시간을 함께 본다.
  • 인덱스는 조회를 빠르게 하는 장치이면서 쓰기 비용, 저장 공간, DDL 위험을 만드는 운영 변경이다.
  • 트랜잭션은 정합성 경계이면서 connection과 lock을 붙잡는 시간 경계다.
  • Page, Pageable, Repository Method, fetch join 같은 편의 기능은 DB 비용을 없애지 않는다.
  • 운영 장애에서는 원인 추측보다 영향 범위 축소, 증거 수집, 완화 조치, 근본 해결을 분리하는 것이 먼저다.
  • replica, proxy, pool, cache는 성능 장치이지만 일관성, 연결 예산, 장애 전파 정책을 새로 만든다.
  • 인프라/DBA와의 좋은 대화는 SQL, 시간대, 지표, 변경 이력, 비즈니스 영향, 원하는 결정을 포함한다.
  • 개인 프로젝트에서도 migration, backup, index 의도, pagination 선택, 복구 리허설을 남기면 백엔드다운 결과물이 된다.

마무리 체크리스트

  • 이 Case에서 실제 SQL과 bind 값을 확보했는가?
  • data query와 count query를 분리해서 봤는가?
  • 실행 계획에서 rows, sort, join, filter, 임시 작업을 확인했는가?
  • Hikari active/idle/pending과 acquire time을 API 지표와 함께 봤는가?
  • lock wait와 long transaction을 확인했는가?
  • 대량 DDL/DML에는 runbook, 중단 기준, 검증 쿼리, 복구 계획이 있는가?
  • 인덱스 추가 전 기존 인덱스, 쓰기 부하, 온라인 생성 가능 여부를 확인했는가?
  • retry에는 idempotency key, Unique 제약, backoff, 최대 횟수가 있는가?
  • read replica를 쓰는 API는 stale read 허용 여부가 명시되어 있는가?
  • 장애 회고에는 전후 지표와 다음 관측 개선 항목이 남았는가?