설계와 변경, 분리까지 다뤘으니 이제 그 결과를 검증하는 방법입니다.
1. 리뷰의 목적
설계 리뷰는 "문법 검사"가 아니라 1강의 좋은 설계 기준(정확성, 무결성, 비중복성, 성능, 변경 용이성)을 증거로 확인하는 과정입니다. 리뷰어는 산출물뿐 아니라 그 결정의 근거를 봅니다.
2. 리뷰 입력물
| 입력물 | 확인 내용 |
|---|---|
| 요구사항·업무 규칙 목록 (2강) | 모든 규칙이 어딘가에 구현되었는가 |
| 개념 ERD (3~4강) | 업무 담당자가 읽고 동의했는가 |
| DDL | 실제 구현 |
| 쿼리 프로파일·볼륨 추정 (2강) | 인덱스·파티션 근거 |
| 반정규화·예외 결정 기록 (10강) | 근거와 정합성 수단 |
| 마이그레이션 계획 (22강) | 운영 중 적용 가능성 |
3. 체크리스트
A. 모델링
- 모든 엔티티에 명확한 정의가 있는가 (용어 사전)
- 관계마다 카디널리티와 참여도가 정의되었는가
- "같은 사건이 두 번 일어날 수 있는가"를 확인했는가 (재처리, 검수 반복)
- 이력이 필요한 속성을 식별했는가 (18강)
- 다중값 속성이 한 컬럼에 저장되지 않았는가
B. 정규화와 중복
- 자연 후보키 기준으로 3NF를 만족하는가
- 반정규화된 모든 컬럼에 결정 기록과 동기화 수단이 있는가
- 같은 의미의 컬럼이 다른 이름·타입으로 여러 곳에 있지 않은가
C. 키와 제약
- 모든 테이블에 PK가 있는가
- 대리키를 쓴 테이블에 자연키 UNIQUE가 있는가
- FK가 정의되어 있고, FK 컬럼에 인덱스가 있는가
- CASCADE 사용처의 삭제 범위를 검토했는가
- NULL 허용 컬럼마다 NULL의 의미가 정의되었는가
- 2강 업무 규칙 중 DB로 표현 가능한 것이 CHECK/UNIQUE/EXCLUDE로 구현되었는가
- 멀티테넌시라면 tenant_id가 모든 키에 포함되었는가
D. 타입
- 금액·수량이 부동소수점이 아닌가
- 절대 시점과 벽시계 시각이 구분되었는가
- 조인 컬럼끼리 타입·길이·콜레이션이 일치하는가
- Oracle 문자열 길이 단위, MySQL 문자셋이 확인되었는가
- 상태가 불리언 여러 개로 표현되지 않았는가
E. 성능
- 상위 쿼리마다 사용할 인덱스가 설명 가능한가
- 중복·미사용 인덱스가 없는가
- 대용량 테이블의 증가량과 보관 정책이 정의되었는가
- 대리키 생성 방식이 삽입 패턴에 적합한가 (무작위 UUID 주의)
F. 운영과 진화
- 스키마 변경이 버전 관리되는가
- 대용량 테이블 변경 시 온라인 적용 방법이 있는가
- 감사 컬럼(created_at, updated_at, created_by)의 정책이 일관적인가
- 소프트 삭제 사용 시 UNIQUE와 조회 조건이 처리되었는가
- 개인정보 컬럼이 식별되고, 암호화·마스킹·보관 기한이 정해졌는가
4. 안티패턴 역추적
체크리스트와 별개로, 알려진 안티패턴 목록을 설계에 하나씩 대입해 봅니다.
| 안티패턴 | 설계에서 찾는 신호 | 관련 강의 |
|---|---|---|
| Jaywalking (콤마 구분 목록) | VARCHAR 컬럼 이름이 복수형 (category_ids) | 3, 8강 |
| Naive Trees | parent_id만 있고 깊은 조회 요구가 많음 | 17강 |
| ID Required | 모든 테이블에 무조건 id, 자연키 UNIQUE 누락 | 5강 |
| Keyless Entry | FK 없음 | 13강 |
| EAV | attr_name, attr_value 컬럼 | 19강 |
| Polymorphic Associations | target_type, target_id 쌍 | 19강 |
| Multicolumn Attributes | tag1, tag2, tag3 | 8강 |
| Metadata Tribbles | order_2025, order_2026 테이블 복제 | 16강 (파티셔닝으로 대체) |
| Rounding Errors | FLOAT 금액 | 12강 |
| 31 Flavors | 자주 바뀌는 값 목록을 CHECK/ENUM으로 고정 | 12강 |
| Fear of the Unknown | NULL 대신 '', 0, '1900-01-01' 사용 | 13강 |
| 무작위 UUID PK | InnoDB에 UUIDv4 PK | 14강 |
| 이중 쓰기 (시스템 간) | 앱이 DB와 메시지 브로커에 각각 쓰기 | 23강 |
5. 리뷰 진행 방식
- 작성자가 요구사항부터 설명한다. DDL부터 보면 근거를 놓칩니다.
- 리뷰어는 구체적인 시나리오로 질문한다. "상품이 다른 카테고리로 이동하면 과거 매출은 어느 카테고리로 집계되나요?"
- 발견 사항은 심각도로 분류한다.
| 심각도 | 기준 | 예시 |
|---|---|---|
| 차단 | 데이터 손실·오염 가능 | 대체키 UNIQUE 누락, 금액 FLOAT |
| 높음 | 운영 중 수정 비용이 큼 | PK 타입 INT, 파티션 키 부적절 |
| 중간 | 성능·유지보수 문제 | FK 인덱스 누락, 명명 불일치 |
| 낮음 | 개선 제안 | 주석 누락 |
- 결정 사항은 ADR(Architecture Decision Record) 형식으로 남긴다.
markdown
# ADR-012: 처리 실적 테이블 월 단위 파티셔닝
## 상태
승인 (2026-09-17)
## 맥락
일 5만 건, 3년 보관. 월별 보관 기한 초과 데이터 삭제가 필요하며,
주요 조회는 처리일 범위 조건을 포함한다.
## 결정
work_date 기준 월 단위 RANGE 파티셔닝. PK는 (log_id, work_date).
## 결과
- 오래된 데이터 삭제가 파티션 분리로 대체됨
- log_id 단독 유일성은 DB가 보장하지 않음 (IDENTITY로 실질 보장)
- 처리 실적을 참조하는 테이블은 work_date를 함께 보유해야 함6. 자동화할 수 있는 것
| 항목 | 방법 |
|---|---|
| PK 없는 테이블 | 카탈로그 쿼리 |
| FK 인덱스 누락 | 카탈로그 쿼리 |
| 명명 규칙 위반 | 스키마 린터 (예: squawk, sqlfluff 규칙) |
| 위험한 마이그레이션 | 마이그레이션 린터 (락을 거는 DDL 탐지) |
| 미사용·중복 인덱스 | 통계 뷰 |
| 환경 간 스키마 차이 | 스키마 diff 도구 |
PostgreSQL: 인덱스 없는 FK 찾기
sql
SELECT c.conrelid::regclass AS table_name,
c.conname AS fk_name
FROM pg_constraint c
WHERE c.contype = 'f'
AND NOT EXISTS (
SELECT 1 FROM pg_index i
WHERE i.indrelid = c.conrelid
AND (i.indkey::int2[])[0:cardinality(c.conkey) - 1] = c.conkey
);FK 컬럼이 어떤 인덱스의 선두 컬럼들과 같은 순서로 일치하는지 검사합니다.
24강 정리
- 리뷰는 좋은 설계 기준을 근거와 함께 확인하는 과정이다.
- 체크리스트와 안티패턴 역추적을 함께 사용한다.
- 발견 사항은 심각도로 분류하고, 결정은 ADR로 남긴다.
- 기계적으로 검사 가능한 항목은 자동화한다.