Web-Database 실전 가이드북
이 문서의 사용법
이 문서는 Web-Database 문서들의 또 다른 요약본이 아니다. 실제 백엔드 개발자가 마주치는 상황을 먼저 던지고, 그 상황에서 어떤 증거를 모아야 하는지, 어떤 기존 문서를 다시 펼쳐야 하는지 안내하는 실전 길잡이다.
읽는 순서는 단순하다. 먼저 Case 제목 중 지금 겪는 상황과 가장 가까운 것을 고른다. 그다음 관찰할 증거로 로그, SQL, 실행 계획, 지표를 모으고, 먼저 생각해보기 질문에 스스로 답해본다. 마지막으로 찾아볼 문서 링크를 따라가 기존 상세 문서를 다시 읽으면 된다.
한 번에 모두 외우려고 하지 않아도 된다. DB 실력은 용어 암기보다 “지금 이 증상이 쿼리 문제인지, 락 문제인지, pool 문제인지, 스키마 변경 문제인지”를 나누는 감각에서 자란다. 이 문서는 그 감각을 반복 연습하기 위한 장치다.
개인 프로젝트에서는 설계 의도, migration, 인덱스, backup, pagination을 설명하는 포트폴리오 문서로 활용한다. 면접에서는 Case의 가져갈 한 문장을 자기 경험과 연결한다. 실무 장애 대응에서는 Case의 증거 목록을 그대로 초기 확인 체크리스트로 쓰고, 인프라/DBA 협업에서는 “제가 확인한 것”과 “같이 봐야 할 것”을 나누는 틀로 쓰면 좋다.
커버리지 지도
| 기존 문서 | 연결된 Case | 다시 읽어야 하는 이유 |
|---|---|---|
| 00. Database 기본 구조와 백엔드 개발자의 관점 압축 정리 | Case 1, Case 12, Case 21 | DB를 기능 저장소가 아니라 요청 흐름과 운영 자원으로 보는 기준을 잡기 위해 |
| 01. DB가 요청 흐름에서 하는 일 상세 | Case 12, Case 20 | API 지연을 애플리케이션, pool, DB 실행 단계로 나누기 위해 |
| 02. Storage Buffer WAL Cache 상세 | Case 15, Case 19 | backfill, WAL, cache, disk I/O 영향을 백엔드 작업과 연결하기 위해 |
| 03. DB 한계와 비용 모델 상세 | Case 12, Case 18, Case 21 | CPU만이 아니라 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 16 | Unique, FK, Check 위반을 운영 예외와 복구 흐름으로 연결하기 위해 |
| 00. SQL 기본과 쿼리 작성 원칙 압축 정리 | Case 3, Case 4 | SQL을 결과가 아니라 비용, 잠금, 복구 가능성으로 읽기 위해 |
| 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 22 | SQL 리뷰 기록을 협업과 회고의 증거로 남기는 방법을 익히기 위해 |
| 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 6 | WHERE, 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 12 | scan, 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 22 | slow query, lock wait, pool wait를 분리하는 진단 순서를 익히기 위해 |
| 05. Plan Regression과 운영 대응 상세 | Case 22 | 어제 빠르던 쿼리가 오늘 느려진 상황을 회고와 기준선으로 다루기 위해 |
| 00. Transaction 압축 정리 | Case 9, Case 15 | 트랜잭션을 정합성 경계이자 connection/lock 점유 시간으로 보기 위해 |
| 01. ACID와 실제 보장 상세 | Case 3, Case 16 | DB 롤백과 외부 부작용, 복구 가능성을 구분하기 위해 |
| 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 19 | export, 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 14 | row, range, metadata lock이 백엔드 SQL 조건과 어떻게 연결되는지 보기 위해 |
| 03. Deadlock과 Lock Wait 상세 | Case 10 | deadlock 로그를 코드 경로, 접근 순서, 재시도 정책으로 되돌리기 위해 |
| 04. 동시성 제어 패턴 상세 | Case 9, Case 18 | Unique, 원자적 update, 낙관적/비관적 락, 큐잉을 상황별로 고르기 위해 |
| 05. 재고 포인트 카운터 사례 상세 | Case 9, Case 18, Case 21 | 숫자 컬럼이라도 재고, 포인트, 카운터의 요구가 다름을 익히기 위해 |
| 00. Schema Design Normalization Denormalization 압축 정리 | Case 1, Case 21 | 스키마 설계를 정합성, 조회 패턴, 변경 가능성까지 포함해 보기 위해 |
| 01. 정규화와 반정규화 상세 | Case 2, Case 13 | snapshot, 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 21 | expand/contract와 JSON 승격 기준으로 스키마 진화를 준비하기 위해 |
| 00. Pagination Sorting Search 압축 정리 | Case 4, Case 5, Case 6 | 목록 API를 pagination, search, count, sort 비용으로 나눠 보기 위해 |
| 01. Offset vs Cursor 상세 | Case 4, Case 6 | offset, cursor, Page, Slice 선택 기준을 잡기 위해 |
| 02. ORDER BY와 인덱스 상세 | Case 4, Case 5 | 정렬이 인덱스와 맞지 않을 때 limit 이전에 비용이 커지는 이유를 보기 위해 |
| 03. 검색 LIKE Full Text 상세 | Case 5 | LIKE '%keyword%', full-text, 검색 엔진 도입 기준을 익히기 위해 |
| 04. 무한 스크롤 API 설계 상세 | Case 4, Case 5 | cursor token, tie-breaker, filter 검증을 API 계약으로 다루기 위해 |
| 05. 대용량 목록 실전 상세 | Case 4, Case 13 | 사용자 목록, 관리자 검색, export를 서로 다른 운영 경로로 분리하기 위해 |
| 00. Connection Pool Timeout 장애 전파 압축 정리 | Case 11, Case 12 | pool timeout을 원인과 결과 양쪽으로 읽기 위해 |
| 01. DB 커넥션이 비싼 이유 상세 | Case 11, Case 12 | connection 보유 시간이 API 전체 지연으로 번지는 흐름을 보기 위해 |
| 02. HikariCP와 Pool 크기 상세 | Case 11, Case 20 | pool size를 전체 DB connection budget으로 계산하기 위해 |
| 03. Timeout 계층 설계 상세 | Case 11, Case 12 | HTTP, transaction, query, pool timeout의 실패 순서를 맞추기 위해 |
| 04. Pool 고갈과 장애 전파 상세 | Case 11, Case 12 | 특정 병목이 정상 API까지 전파되는 경로를 이해하기 위해 |
| 05. Cloud Pool Proxy 운영 상세 | Case 20, Case 21 | RDS Proxy, Cloud SQL Pooling, 인프라 연결 예산을 협업 언어로 다루기 위해 |
| 00. ORM과 Database JPA 관점 압축 정리 | Case 7, Case 8 | JPA 코드가 실제 SQL, flush, N+1, 인덱스로 변환되는 흐름을 보기 위해 |
| 01. ORM은 SQL을 없애지 않는다 상세 | Case 7, Case 20 | Repository 메서드와 SQL fingerprint를 연결하는 습관을 만들기 위해 |
| 02. 영속성 컨텍스트 Flush Dirty Checking 상세 | Case 7, Case 15 | flush 시점, bulk update, 대량 처리에서 DB 상태와 메모리 상태를 구분하기 위해 |
| 03. N+1 Fetch Join EntityGraph 상세 | Case 7, Case 8 | N+1 해결책별 trade-off와 pagination 위험을 보기 위해 |
| 04. 트랜잭션 범위와 Lazy Loading 상세 | Case 7, Case 11 | OSIV, lazy loading, DTO 경계가 connection 보유 시간과 연결되는 지점을 보기 위해 |
| 05. JPA 쿼리와 인덱스 상세 | Case 6, Case 7 | Query Method, JPQL, Querydsl, native query를 인덱스와 함께 리뷰하기 위해 |
| 00. Migration 운영 성능 진단 압축 정리 | Case 14, Case 15, Case 16, Case 22 | migration, backfill, backup, 성능 진단을 운영 절차로 묶기 위해 |
| 01. Schema Migration 원칙 상세 | Case 14 | migration 도구와 운영 안전성이 다른 문제임을 이해하기 위해 |
| 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 22 | DB 지표와 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 15 | DB migration을 코드 배포, backfill, cleanup으로 쪼개기 위해 |
| 05. 인프라 DBA와 협업하는 법 상세 | Case 20 | 요청 종류별로 어떤 정보를 인프라/DBA에게 가져갈지 익히기 위해 |
| 00. 고급 DB 장애와 한계 압축 정리 | Case 17, Case 18, Case 19, Case 22 | 드문 장애를 DB 내부 용어와 백엔드 코드 패턴으로 연결하기 위해 |
| 01. Lock 경합과 Deadlock 상세 | Case 10 | lock 경합과 deadlock을 코드 경로, SQL 조건, 인덱스로 되돌리기 위해 |
| 02. Connection 고갈과 장애 전파 상세 | Case 11, Case 12 | connection 고갈이 장애 증폭기가 되는 흐름을 보기 위해 |
| 03. Replica Lag와 읽기 일관성 상세 | Case 17 | read/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 제약과 애플리케이션 검증의 역할을 나눈다.
- 인덱스 리뷰 체크리스트 - 신규 API의 SQL과 인덱스를 같은 단위로 리뷰한다.
정리
- 테이블은 설계의 출발점이 아니라 요구사항을 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 전 검증 절차를 확인한다.
- UPDATE 절차 - 운영 정정 쿼리를 단계화한다.
- Durability - DB rollback과 외부 부작용을 구분한다.
- 위험 신호! - 제약 위반을 운영 예외로 변환하는 기준을 본다.
정리
- 쓰기 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을 한 인덱스 전략으로 지원하는가?- 특정 회원만 느리다면 데이터 분포가 치우친 것은 아닌가?
찾아볼 문서
- Offset Pagination - 뒤 페이지와 count 비용을 확인한다.
- 원리 - 정렬을 인덱스와 함께 설계한다.
- 실전 팁 - 복합 인덱스 컬럼 순서를 잡는다.
- 실제 SQL 확보 - 실행 계획 전에 실제 SQL을 확보한다.
- 제한과 보호장치 - size, 기간, sort 제한을 둔다.
정리
- 목록 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이 있는가?
- 검색 결과는 최신순이 중요한가, 관련도순이 중요한가?
찾아볼 문서
- 부분 검색 - B-Tree와 부분 검색의 한계를 확인한다.
- 앞쪽 와일드카드 LIKE - 앞쪽 와일드카드가 왜 인덱스에 불리한지 본다.
- 코드 리뷰 예시 - 검색 SQL을 리뷰 가능한 형태로 남긴다.
- Cursor Token - 검색 결과 목록의 cursor 계약도 함께 본다.
정리
- 검색은 단순 문자열 조건이 아니라 제품 요구사항이다.
- 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가 조인이나 필터를 과하게 포함하지 않는가?
찾아볼 문서
- 헷갈리는 지점 -
Pageable이 비용 문제를 없애지 않음을 확인한다. - Offset Pagination -
Page,Slice, cursor 선택 기준을 본다. - JPQL과 DTO Projection - countQuery 분리와 projection을 검토한다.
- 확인 질문 - 목록 쿼리와 인덱스의 결합을 확인한다.
정리
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이 발생하는가?
찾아볼 문서
- ORM 관측 루틴 - API와 SQL fingerprint를 연결한다.
- Flush 시점 - SQL 실행 타이밍을 확인한다.
- 위험 신호! - N+1 해결책의 부작용을 함께 본다.
- Open Session In View - OSIV와 connection 점유 시간을 확인한다.
- 실행 계획으로 확인하기 - ORM이 만든 SQL도 실행 계획으로 본다.
정리
- 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으로 필요한 컬럼만 가져올 수 있는가?
찾아볼 문서
- 확인 질문 - fetch join과 pagination의 충돌을 확인한다.
- 위험 신호! - join 중간 row 증가를 본다.
- Cursor Pagination - 목록 API의 안정적인 pagination 조건을 본다.
- 코드 리뷰 체크리스트 - JPA 쿼리를 DB 관점으로 리뷰한다.
정리
- 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 재시도 조건을 확인한다.
- 장애 중 확인 - 장애 중 lock 확인 순서를 본다.
- 인덱스와 Lock 범위 - 인덱스와 lock 범위를 연결한다.
- 확인 질문 - 완화와 원인 분석을 분리한다.
정리
- 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이 어느 계층에서 먼저 발생해야 하는가?
찾아볼 문서
- 지표로 보는 상태 - pool 지표 해석을 확인한다.
- 먼저 할 일 - pool 고갈 대응 순서를 본다.
- 실전 팁 - timeout budget을 맞춘다.
- 위험 신호! - 긴 transaction과 streaming 조회를 의심한다.
- 원인 구분 - 원인 후보를 나눈다.
정리
- 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 범위를 바꾸었는가?
찾아볼 문서
- 먼저 확인할 것 - 느린 API 초기 증거를 모은다.
- 장애 전파 흐름 - pool 고갈 전파를 확인한다.
- 진단 순서 - 운영 진단 순서를 따른다.
- 원리 - 요청 흐름의 각 대기 위치를 분리한다.
- DB max connections - 연결 예산을 확인한다.
정리
- 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 수, 실행 시간대 제한이 있는가?
찾아볼 문서
- 관리자 검색 - 관리자 검색과 사용자 피드를 분리한다.
- 리포트와 스트리밍 - streaming 조회의 connection 점유 위험을 본다.
- 실시간 집계와 API - 집계 API의 운영 비용을 본다.
- 실전 팁 - sort, aggregate, temporary 비용을 확인한다.
- 코드에서 만드는 원인 - export가 pool을 고갈시키는 패턴을 본다.
정리
- 관리자 기능은 호출 빈도가 낮아도 한 번의 작업이 운영 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가 현실적인가?
찾아볼 문서
- 배포 호환성 테스트 - old/new app과 schema 조합을 확인한다.
- Migration 작성 원칙 - migration 파일 원칙을 점검한다.
- Expand and Contract - 변경 단계를 쪼갠다.
- 변경을 위한 구조 - 안전한 스키마 진화 절차를 본다.
- Table Lock - DDL lock 위험을 확인한다.
정리
- 대형 테이블 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을 쓰는가?
- 어떤 지표가 임계치를 넘으면 즉시 멈출 것인가?
찾아볼 문서
- 운영 Runbook - backfill runbook 항목을 확인한다.
- 작은 배치로 나누기 - 작은 트랜잭션으로 나누는 이유를 본다.
- 실전 팁 - 대량 DML 전 체크를 본다.
- WAL - 대량 변경과 WAL/replica lag를 연결한다.
- 대량 처리 문제 - JPA 대량 처리의 flush/clear 전략을 본다.
정리
- backfill은 빠른 SQL 하나가 아니라 조절 가능한 운영 작업이다.
- 설정으로 속도를 낮추거나 멈출 수 있고, 진행률과 검증 쿼리가 있어야 인프라와 함께 안전하게 운전할 수 있다.
가져갈 한 문장
- 좋은 backfill은 빨리 끝나는 작업보다 안전하게 멈추고 다시 시작할 수 있는 작업이다.
Case 16. 잘못된 DELETE 후 복구 가능성을 판단해야 한다
상황
- 운영에서 잘못된 조건으로 데이터를 삭제했거나, 삭제 배치가 예상보다 넓게 실행됐다.
- “백업이 있다”는 말만으로는 충분하지 않다. 어느 시점으로 얼마나 빨리, 어떤 범위만 복원할 수 있는지가 중요하다.
관찰할 증거
- 삭제 시간, 영향 row 수, 백업/PITR 가능 시점, 삭제 후 정상 write 범위, 감사 로그, 원장/event log 존재 여부를 확인한다.
먼저 생각해보기
- 전체 DB를 과거로 되돌리면 그 사이 정상 거래는 어떻게 되는가?
- 별도 복구 DB에서 삭제된 데이터만 추출할 수 있는가?
- forward fix가 가능한 데이터인가, 원장으로 재계산할 수 있는가?
찾아볼 문서
- PITR 판단 기준 - 전체 복원과 부분 복원을 나눠 판단한다.
- 트랜잭션과 롤백 - 운영 쿼리의 rollback 한계를 확인한다.
- 감사 로그 - 감사와 삭제 정책을 본다.
- 롤백의 범위 - DB rollback과 외부 부작용을 구분한다.
- 실전 팁 - 복구 후 제약 검증과 예외 처리를 확인한다.
정리
- 복구는 인프라만의 일이 아니다. 어떤 데이터가 재처리 가능한지, 어떤 외부 부작용이 있었는지, 어떤 상태 전이가 정상인지 백엔드가 설명해야 한다.
- 잘못된 DELETE 대응은 현재 DB 보존, 별도 복구, 영향 범위 추출, 검증, 감사 기록 순서로 차분히 진행한다.
가져갈 한 문장
- 백업이 있다는 말보다 “어느 시점의 어떤 데이터를 어떻게 현재 상태에 되돌릴 것인가”가 더 중요하다.
Case 17. read replica를 붙였더니 저장 직후 조회가 안 된다
상황
- 읽기 부하를 줄이려고 replica를 붙였는데, 사용자가 글을 저장한 직후 상세 조회에서 404나 이전 상태를 본다.
- 이것은 버그일 수도 있지만, read/write split 정책이 최신성 요구를 반영하지 못한 결과일 수 있다.
관찰할 증거
- replica lag, read routing 규칙, 저장 직후 조회 API, 권한/잔액/결제 상태 조회 위치, fallback 정책을 확인한다.
먼저 생각해보기
- 이 읽기는 stale read를 허용할 수 있는가?
- 방금 쓴 사용자의 다음 읽기는 일정 시간 primary로 보내야 하는가?
- lag가 임계치를 넘으면 replica routing을 어떻게 제한하는가?
찾아볼 문서
- 설계 기준 - primary로 보내야 할 읽기를 구분한다.
- 프록시가 해결하는 것과 못 하는 것 - 인프라 계층이 정합성 문제를 없애지 않음을 본다.
- PITR 판단 기준 - 시간선과 복구 관점을 함께 익힌다.
- 실전 팁 - 운영 지표와 API 증상을 연결한다.
정리
- 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를 더 세게 두드리고 있지 않은가?
찾아볼 문서
- 재고 차감 - 같은 row 업데이트의 구조적 한계를 본다.
- 비교 기준 - 숫자 도메인별 정확성 요구를 나눈다.
- 상태 전이 조건 - 조건부 update와 상태 전이 조건을 본다.
- 실전 팁 - DB 한계를 단순 스펙 문제가 아니라 병목 구조로 본다.
- 실시간 집계와 API - 지연 집계 가능성을 검토한다.
정리
- 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 시간을 고려했는가?
찾아볼 문서
- 원리 - MVCC와 vacuum의 연결을 이해한다.
- 원리 - storage, buffer, WAL을 요청 비용과 연결한다.
- 삭제 작업 - 대량 삭제의 후속 부담을 본다.
- idle in transaction - 오래 열린 transaction의 위험을 확인한다.
- 변경을 위한 구조 - 대량 변경을 단계화한다.
정리
- 운영 용어가 낯설어도 백엔드가 만든 긴 트랜잭션, 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 요청별 필요한 정보 - 요청 종류별 정보를 확인한다.
- SQL 리뷰 체크리스트 - SQL 리뷰 근거를 만든다.
- 인덱스 리뷰 체크리스트 - 인덱스 요청의 운영 정보를 정리한다.
- DBA의 고충 - 연결 예산과 인프라 부담을 이해한다.
- 애플리케이션 연결 - SQL을 코드 경로로 연결한다.
정리
- 좋은 협업은 추측을 넘기는 것이 아니라 서로에게 없는 맥락을 제공하는 일이다.
- 백엔드는 비즈니스 흐름과 코드 변경을, 인프라는 DB 내부 wait, 복제, 백업, 용량을 제공할 때 판단이 빨라진다.
가져갈 한 문장
- “DB가 느려요” 대신 “이 시간대의 이 SQL과 이 지표를 함께 판단하고 싶습니다”라고 말하자.
Case 21. 개인 프로젝트를 운영 수준으로 보이게 만들고 싶다
상황
- 개인 프로젝트라 트래픽은 작지만, 전문 백엔드 개발자답게 DB 설계와 운영 감각을 보여주고 싶다.
- 거창한 인프라보다 중요한 것은 작은 서비스에서도 중요한 선택을 설명할 수 있는 흔적이다.
관찰할 증거
- ERD, migration history, 핵심 제약, 주요 목록 API 인덱스, cursor pagination, backup/restore 절차, slow query 확인 루틴이 있는지 본다.
먼저 생각해보기
- README나 기술 문서에서 “왜 이 인덱스가 필요한가”를 설명할 수 있는가?
- 중복 방지와 상태 전이를 DB 제약과 코드로 함께 막고 있는가?
- 백업을 실제로 복구해본 기록이 있는가?
찾아볼 문서
- 실전 팁 - 개인 프로젝트의 운영 감각 기준을 잡는다.
- 개인 프로젝트 기준 - migration과 설계 의도를 남긴다.
- 개인 프로젝트 기준 - 인덱스 선택 이유를 문서화한다.
- 개인 프로젝트 기준 - backup과 restore 리허설을 준비한다.
- 개인 프로젝트 기준 - 동시성 역량을 작은 기능으로 보여준다.
- 주니어 팁 - API별 조건, 정렬, 동시성을 설계 문서에 녹인다.
정리
- 개인 프로젝트에서 기업 수준의 모든 장치를 다 갖출 필요는 없다.
- 대신 migration, 제약, 인덱스, pagination, backup, 장애 대응 메모처럼 운영 판단의 흔적을 남기면 단순 CRUD와 분명히 달라진다.
가져갈 한 문장
- 개인 프로젝트의 DB 완성도는 기능 수보다 선택 이유와 복구 가능성을 설명할 수 있는지에서 드러난다.
Case 22. 장애 회고를 “인덱스 추가함”으로 끝내지 않으려 한다
상황
- 장애 후 인덱스를 추가했고 지표는 좋아졌다.
- 하지만 왜 그 plan이 나왔는지, 왜 관측이 늦었는지, 어떤 완화가 효과였는지 남기지 않으면 다음 장애에서 다시 헤맨다.
관찰할 증거
- 장애 타임라인, 사용자 영향, SQL fingerprint, 전후 실행 계획, pool/lock/DB 지표, 배포/배치 이력, 완화 조치별 기대 지표와 실제 결과를 모은다.
먼저 생각해보기
- 이 문제는 쿼리 구조, 인덱스, 데이터 분포, 통계, API 계약, 운영 절차 중 무엇이 핵심이었는가?
- 장애 중 어떤 증거가 없어서 판단이 늦었는가?
- 임시 완화가 영구 설정으로 남지 않도록 제거 조건이 있는가?
찾아볼 문서
- 핵심 정리 - 실전 대응의 큰 기준을 다시 본다.
- 실전 팁 - 장애 중 완화와 회고 항목을 확인한다.
- 증거 수집 - plan regression의 기준선을 남긴다.
- 진단 결과를 작업으로 만들기 - 진단을 재발 방지 작업으로 연결한다.
- 데이터 분포의 함정 - 데이터 분포와 통계 문제를 회고에 포함한다.
- 핵심 정리 - 드문 장애의 증폭 고리를 함께 본다.
정리
- 좋은 회고는 “무엇을 고쳤다”보다 “어떤 증거로 무엇을 판단했고, 다음에는 어떤 관측과 제한으로 더 빨리 대응할 것인가”를 남긴다.
- 인덱스 추가가 해결책이었더라도 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 허용 여부가 명시되어 있는가?
- 장애 회고에는 전후 지표와 다음 관측 개선 항목이 남았는가?