27장. Database 작업 — Schema · Query · Migration · Index
DB 작업은 성격이 둘로 갈린다.
읽는 쪽은 되돌릴 수 있고,
쓰는 쪽은 되돌릴 수 없다.
에이전틱 코딩에서 이 구분이 그대로 경계선이 된다.
스키마를 파악시키는 방법
12장에서 스키마 덤프를 오염원으로 꼽았다.
테이블 200개짜리 모놀리스에서\d+ 를 전부 넣으면 Context가 바로 찬다.
필요한 것만 넣는다.
주문 취소와 관련된 테이블 구조를 확인해줘.
- 엔티티 클래스에서 먼저 찾아줘 (order, payment, point 패키지)
- 실제 DDL이 필요하면 해당 테이블만 조회해줘
🔥 JPA를 쓴다면 엔티티가 스키마 문서다.
@Entity 클래스를 읽는 편이
DB에 붙는 것보다 싸고 정확하다.
다만 둘이 어긋나 있을 수 있다.
레거시에서는 흔하다.
OrderEntity 와 실제 orders 테이블의 컬럼이 일치하는지 확인해줘.
차이가 있으면 목록만 보여줘.
쿼리 — 잘 쓰지만 계획은 모른다
Agent는 쿼리를 잘 쓴다.
JPQL, QueryDSL, 네이티브 쿼리 모두
문법적으로 정확하게 만든다.
⚠️ 그런데 그 쿼리가 어떻게 실행되는지는 모른다.
인덱스가 있는지, 데이터가 몇 건인지,
카디널리티가 어떤지 코드에는 없다.
그래서 습관 하나가 필요하다.
이 쿼리의 실행 계획을 확인해줘.
EXPLAIN 결과를 보여주고,
풀스캔이 있으면 어떤 인덱스가 필요한지 알려줘.
로컬 DB에 운영과 비슷한 데이터가 없으면
실행 계획도 의미가 없다.
그 사실을 CLAUDE.md 에 적어둔다.
- 로컬 DB는 데이터가 적어 실행 계획이 운영과 다르다
쿼리 성능 판단은 사람에게 확인을 요청한다
마이그레이션이 특별한 이유
DB 작업에서 유일하게 되돌릴 수 없는 부분이다.
flowchart LR
A[마이그레이션 작성] -->|Agent| B[검토]
B -->|사람| C[실행]
C -->|사람| D[운영 반영]
Agent는 작성까지만 한다.
# CLAUDE.md
- 마이그레이션 파일 작성까지만 한다. 실행하지 않는다
- `flyway migrate`, `liquibase update` 를 실행하지 않는다
{
"permissions": {
"deny": [
"Bash(./gradlew flywayMigrate:*)",
"Bash(psql:*)"
]
}
}
7장의 원칙 그대로다.
되돌릴 수 없는 일은 사람이 결정한다.
위험한 마이그레이션 패턴
Agent가 만든 마이그레이션에서 반드시 확인할 것들이다.
| 패턴 | 무슨 일이 일어나는가 |
|---|---|
NOT NULL 컬럼 추가 | 기존 행이 있으면 실패 |
| 컬럼 rename | 구버전 앱이 죽는다 |
| 컬럼 타입 변경 | 테이블 잠금 + 데이터 손실 |
| 큰 테이블에 인덱스 추가 | 잠금 시간이 길다 |
DROP COLUMN | 롤백 불가 |
| 기본값 있는 컬럼 추가 | DB 버전에 따라 전체 rewrite |
⚠️ 두 번째와 세 번째가 배포 사고의 단골이다.
Agent는 최종 상태만 본다.
배포 중간에 구버전과 신버전이 함께 도는 시간을 고려하지 않는다.
확장-수축 패턴을 규칙으로
무중단 배포에서 쓰는 순서다.
이것을 CLAUDE.md 에 적어두면
Agent가 매번 맞게 만든다.
## 마이그레이션 규칙
컬럼 변경은 3단계로 나눈다 (배포 3회)
1. 확장: 새 컬럼 추가 (nullable, 기본값 없음)
2. 이행: 양쪽 쓰기 → 백필 → 읽기 전환
3. 수축: 구 컬럼 제거
- `NOT NULL` 은 백필 완료 후 별도 마이그레이션으로 건다
- 컬럼 rename은 하지 않는다 (추가 → 이행 → 제거)
- 100만 건 이상 테이블 인덱스는 온라인 방식으로 생성한다
🔥 이 열 줄이 마이그레이션 사고의 대부분을 막는다.
Agent가 몰라서 틀리는 것이지,
알려주면 정확히 지킨다.
인덱스 판단은 사람이 한다
인덱스는 코드만 봐서는 결정할 수 없다.
| Agent가 아는 것 | 알 수 없는 것 |
|---|---|
| 어떤 컬럼으로 조회하는가 | 데이터 분포 |
| 쿼리 문법 | 실제 카디널리티 |
| 기존 인덱스 목록 | 쓰기 부하 |
| 이 인덱스가 이미 다른 것에 포함되는지 |
그래서 이렇게 나눈다.
인덱스가 필요해 보이는 쿼리를 찾아줘.
- 어떤 컬럼 조합이 필요한지 후보만 제시해줘
- 기존 인덱스로 커버되는지 확인해줘
- 마이그레이션은 아직 만들지 마
후보 도출은 Agent가, 결정은 사람이.
트랜잭션 경계는 다음 장에서
DB 작업에서 가장 자주 나는 사고는
사실 쿼리도 마이그레이션도 아니다.
트랜잭션 경계다.
28장에서 따로 다룬다.
완료 조건 예시
## Acceptance Criteria
- 마이그레이션 파일이 V32 번호로 생성됨 (실행하지 않음)
- 로컬에서 `flyway migrate` 를 사람이 실행해 성공 확인
- 롤백 스크립트 또는 되돌리는 방법이 문서화됨
- 신규 컬럼은 nullable, 기본값 없음
- 기존 24개 결제 테스트 통과
⚠️ 세 번째 항목을 자주 빠뜨린다.
되돌리는 방법이 없는 마이그레이션은
배포 후에 손이 묶인다.
이 장의 핵심
- DB 작업은 읽는 쪽과 쓰는 쪽의 성격이 다르다
- JPA 엔티티가 가장 싼 스키마 문서다 — 다만 실제 DDL과 어긋날 수 있다
- Agent는 쿼리를 잘 쓰지만 실행 계획은 모른다
- 로컬 데이터가 적으면 실행 계획도 신뢰할 수 없다 — 그 사실을 적어둔다
- 마이그레이션은 작성까지만 맡기고 실행은 사람이 한다
- Agent는 최종 상태만 보고 배포 중간 상태를 고려하지 않는다
- 확장-수축 3단계를 규칙으로 적어두면 대부분의 사고가 막힌다
- 인덱스 후보는 Agent가 뽑고 결정은 사람이 한다
- 되돌리는 방법이 없는 마이그레이션은 배포 후 손을 묶는다