데이터베이스(DB) 설계의 핵심 원칙과 기본기
DB 설계의 본질은 “비즈니스 데이터를 어떻게 저장, 연결, 보호, 확장할지 결정하는 설계도”를 만드는 것입니다. 이는 단순히 ERD(Entity Relationship Diagram)를 그리는 것을 넘어 데이터의 생애 주기 전체를 아우르는 의사결정의 집합이며, 정규화, 트랜잭션, 동시성 제어, 스케일 아웃 전략까지 포괄하는 광범위한 영역입니다.
1. DB 설계의 중요성
잘못된 DB 설계는 서비스 성장의 발목을 잡는 기술 부채로 작용할 수 있습니다. 초기에는 사소해 보였던 문제가 데이터 규모와 트래픽이 증가하면서 심각한 장애로 이어질 수 있기 때문입니다.
- 성능 관점: 인덱스를 잘못 설계하면 풀스캔으로 인해 응답 시간이 수 초까지 지연될 수 있습니다. 또한, 파티션 키를 잘못 선정하면 특정 샤드에 트래픽이 몰리는 핫스팟(Hotspot)이 발생하여 반복적인 장애를 유발할 수 있습니다.
- 정합성 관점: 과도한 테이블 분리는 JOIN 연산 중 락(Lock) 경합을 유발하여 데드락(Deadlock)이나 Phantom Read 같은 동시성 문제를 일으킬 수 있습니다. 반대로 무분별한 컬럼 중복은 데이터 불일치의 원인이 됩니다.
- 확장 및 비용 관점: 서비스 초기에 편리했던 스키마 구조가 데이터가 수십TB 규모로 커졌을 때 온라인 스키마 변경(Online Schema Change, OSC)을 불가능하게 만들 수 있습니다. 복제 지연(Replication Lag)을 방치하면 복제본 DB에서 오래된 데이터를 읽게 되어 고객 불만으로 이어질 수도 있습니다.
- 운영 및 보안 관점: 개인정보 컬럼의 위치나 암호화 정책이 체계적이지 않으면 GDPR, ISMS-P와 같은 규제 준수 심사에서 벌금이나 법적 책임을 질 위험이 있습니다.
궁극적으로 DB 설계의 목표는 “애플리케이션 코드의 오류와 관계없이, DB 데이터만큼은 언제나 신뢰할 수 있는 상태”를 유지하는 것입니다.
2. DB 설계의 핵심 원칙 및 기본기
효과적인 DB 설계를 위해서는 다음의 핵심 주제들을 이해하고 상황에 맞게 적용해야 합니다.
2.1. 데이터 모델링
데이터 모델링은 비즈니스의 개념적인 아이디어를 DB의 물리적인 구조(테이블, 컬럼, 관계)로 변환하는 과정입니다. 일반적으로 다음 3단계로 진행됩니다.
- 개념 모델: 비즈니스의 핵심 개체(Entity)와 그들 간의 관계를 ERD로 도식화합니다. (예: 회원, 주문, 상품)
- 논리 모델: 개념 모델을 기반으로 각 테이블의 키(Key)를 정의하고, 정규화를 적용하여 데이터의 관계와 제약 조건을 명확하게 설정합니다.
- 물리 모델: 실제 사용할 DBMS의 특성과 성능을 고려하여 인덱스, 파티셔닝 전략, 스토리지 포맷 등을 구체적으로 설계하며 최적화를 진행합니다.
2.2. 정규화와 반정규화
- 정규화 (Normalization): 데이터의 중복을 제거하고 이상 현상(Anomaly)을 방지하여 데이터 무결성을 높이는 과정입니다. 같은 데이터가 여러 곳에서 충돌하거나 반복되지 않도록 테이블을 논리적으로 분해하는 작업입니다.
- 1NF(제1정규형): 모든 컬럼이 원자값(하나의 셀에 하나의 값)을 갖도록 합니다.
- 2NF(제2정규형): 기본 키의 일부에만 종속되는 컬럼(부분 함수 종속)을 제거합니다.
- 3NF(제3정규형): 기본 키가 아닌 다른 컬럼에 종속되는 경우(이행적 함수 종속)를 제거합니다.
- BCNF, 4NF 등: 더 높은 수준의 정규형도 존재하며, 데이터 일관성을 최우선으로 할 때 적용합니다.
- 반정규화 (Denormalization): 정규화된 테이블 구조를 성능이나 운영 편의를 위해 의도적으로 되돌리는 설계 방식입니다. 데이터 중복을 감수하는 대신, 잦은 JOIN 연산을 줄여 조회 속도를 높이는 것이 핵심입니다.
- 적용 시점: 읽기 트랜잭션(TPS)이 쓰기 TPS보다 현저히 많고, JOIN 비용이 시스템의 병목 지점이 될 때 신중하게 고려합니다.
- 주요 패턴: 집계 테이블, 중복 컬럼, 이력 테이블 등이 있습니다.
2.3. 키(Key) 설계
- 자연 키 (Natural Key): 주민등록번호, 이메일 주소처럼 실제 비즈니스 환경에서 고유 식별자로 사용되는 값입니다. 사람이 이해하기 쉽지만, 비즈니스 정책 변경에 따라 값이 변할 수 있다는 단점이 있습니다.
- 대리 키 (Surrogate Key): 비즈니스와 무관하게 시스템이 부여하는 인위적인 식별자입니다. (예: 자동 증가 ID, UUID) 값 자체가 비즈니스 의미를 갖지 않아 절대 변경되지 않으며, 샤딩이나 인덱스 구성 시 더 유리한 경우가 많습니다.
2.4. 관계 및 제약 조건 설계
데이터의 무결성, 즉 정확성과 일관성을 보장하기 위해 다양한 제약 조건을 활용해야 합니다.
- 외래 키 (Foreign Key): 두 테이블 간의 참조 무결성을 보장합니다. 부모 테이블에 존재하지 않는 값이 자식 테이블에 입력되는 것을 막습니다. 실무에서는 삭제/수정 시의 락 경합 문제나 대량 데이터 마이그레이션의 어려움 때문에 물리적인 FK 제약을 걸지 않고, 애플리케이션 레벨에서 논리적으로만 관계를 유지하는 경우도 많습니다.
- 고유 제약 (Unique Constraint): 테이블 내 특정 컬럼 또는 컬럼 조합의 값이 중복되지 않도록 보장합니다. API의 멱등성(Idempotency)을 보장하는 데 활용되기도 합니다.
- 체크 제약 (Check Constraint): 컬럼 값이 지정된 범위나 패턴을 만족하는지 검사합니다.
- 기타 제약:
NOT NULL,DEFAULT등을 적절히 사용하여 데이터의 품질을 높이고 코드의 복잡성을 줄일 수 있습니다.
2.5. 동시성 설계 원칙
여러 트랜잭션이 동시에 실행될 때 데이터 정합성을 보장하고 충돌을 관리하는 것은 매우 중요합니다.
- 격리 수준 (Isolation Level): 동시에 실행되는 트랜잭션이 서로에게 얼마나 영향을 미칠지 결정하는 기준입니다.
- READ COMMITTED: 다른 트랜잭션이 커밋한 데이터만 읽도록 허용하여 Dirty Read를 방지합니다. 하지만 한 트랜잭션 내에서 같은 데이터를 두 번 읽었을 때 결과가 다를 수 있는 Non-repeatable Read는 발생할 수 있습니다.
- REPEATABLE READ (MySQL 기본값): 한 트랜잭션이 시작되기 전에 커밋된 내용만 조회하여 Non-repeatable Read를 방지합니다. MySQL에서는 Gap Lock을 통해 Phantom Read 현상도 대부분 막아줍니다.
- SERIALIZABLE: 가장 엄격한 수준으로, 트랜잭션을 순차적으로 실행하는 것처럼 동작시켜 동시성 문제를 원천 차단합니다. 정합성은 가장 높지만 성능 저하가 크므로 회계, 결제 등 높은 신뢰도가 필요한 시스템에 제한적으로 사용됩니다.
- 데드락 예방: 여러 트랜잭션이 테이블에 접근하는 순서를 항상 동일하게(예:
users→orders→payments) 유지하면 대부분의 데드락을 예방할 수 있습니다.
2.6. 인덱스, 통계, 쿼리 설계
- 인덱스 (Index): 테이블에서 원하는 데이터를 빠르게 찾기 위한 ‘목차’ 역할을 합니다.
- 복합 인덱스: 여러 컬럼을 묶어 생성하는 인덱스로, 컬럼 순서가 매우 중요합니다. WHERE 절에서 자주 사용되는 순서대로, 그리고 선별도(Cardinality)가 높은 컬럼을 앞에 배치하는 것이 일반적입니다.
- 커버링 인덱스 (Covering Index): 쿼리에 필요한 모든 컬럼이 인덱스에 포함되어 있어, 실제 테이블에 접근하지 않고 인덱스만으로 결과를 반환하는 경우를 말합니다. 디스크 I/O를 크게 줄여 성능을 극적으로 향상시킬 수 있습니다.
- 통계 갱신: DBMS의 옵티마이저는 내부 통계 정보를 기반으로 쿼리 실행 계획을 수립합니다. 데이터가 자주 변경되면 통계가 부정확해져 비효율적인 실행 계획을 세울 수 있으므로, 주기적인 통계 갱신(
ANALYZE)이 필수적입니다. - 쿼리 성능 분석:
EXPLAIN명령어를 통해 쿼리 실행 계획을 직접 확인하고, 풀스캔이나 비효율적인 조인이 발생하는지 분석해야 합니다.
2.7. 읽기 스케일아웃 및 복제 지연 대응
읽기 트래픽 분산을 위해 복제 DB(Replica)를 사용하지만, 비동기 복제 방식은 원본(Master)과 복제본 간의 데이터 불일치를 유발하는 ‘복제 지연’ 문제를 야기할 수 있습니다.
- 쓰기 후 즉시 읽기(Read-after-Write) 패턴: 사용자가 데이터를 생성/수정한 직후 해당 데이터를 다시 조회하는 경우, 복제 지연으로 인해 변경 전 데이터가 보일 수 있습니다. 이런 조회 요청은 반드시 원본 DB로 보내야 합니다.
- 복제 지연 모니터링:
Seconds_behind_master(MySQL 기준)와 같은 지표를 지속적으로 모니터링하고, 지연이 심각할 경우 읽기 요청을 일시적으로 원본 DB로 보내는 Fallback 정책을 마련해야 합니다.
2.8. 파티셔닝과 샤딩 전략
데이터가 수억 건 이상으로 증가하거나 트래픽이 단일 DB 서버의 한계를 초과할 때 사용하는 확장 전략입니다.
- 파티셔닝 (Partitioning): 하나의 테이블을 논리적으로는 하나로 유지하되, 내부적으로 여러 개의 물리적 파티션으로 분할하여 저장하는 기법입니다. 주로 날짜 범위(Range)나 특정 코드(List)를 기준으로 분할하며, 관리 용이성과 쿼리 성능을 높일 수 있습니다.
- 샤딩 (Sharding): 데이터베이스 자체를 수평으로 분할하여 여러 개의 독립적인 DB 인스턴스에 분산 저장하는 전략입니다. DB 서버 단위로 부하를 분산하고 장애를 격리할 수 있지만, 여러 샤드에 걸친 JOIN이 불가능하고 운영 복잡도가 크게 증가하는 단점이 있습니다.
- 차이점: 파티셔닝은 단일 DB 서버 내에서의 분할이고, 샤딩은 여러 DB 서버에 걸친 물리적 분산이라는 점에서 근본적인 차이가 있습니다.
2.9. CQRS와 Outbox 패턴
분산 시스템 환경에서 확장성과 데이터 정합성을 동시에 확보하기 위한 고급 아키텍처 패턴입니다.
- CQRS (Command Query Responsibility Segregation): 쓰기(명령) 모델과 읽기(조회) 모델의 책임을 분리하는 패턴입니다. 쓰기 모델은 데이터 정합성에, 읽기 모델은 조회 성능에 최적화하여 각자의 요구사항에 맞게 독립적으로 확장할 수 있습니다.
- Outbox 패턴: 서비스 로직을 처리하는 DB 트랜잭션과 메시지 큐에 이벤트를 발행하는 작업을 원자적으로 묶기 위한 패턴입니다. 로직 처리와 이벤트 발행을 하나의 트랜잭션으로 처리하는 대신, 발행할 이벤트를 DB 내의
outbox테이블에 먼저 저장합니다. 별도의 프로세스가 이 테이블을 읽어 메시지 큐로 안정적으로 이벤트를 전달함으로써, 데이터 정합성을 보장합니다.
2.10. 운영, 보안, 거버넌스 설계
기술적인 설계 외에도 장기적인 운영과 데이터 관리를 위한 정책 또한 중요합니다.
- 무중단 스키마 변경 (Online Schema Change): 서비스 중단 없이 테이블 구조를 변경하기 위해
pt-online-schema-change나gh-ost와 같은 도구를 활용하는 전략을 수립해야 합니다. - 보안 설계: 개인정보와 같은 민감 데이터는 암호화하고, 비밀번호는 단방향 해시 함수(bcrypt, argon2)로 저장해야 합니다. DB 사용자 계정은 역할에 따라 최소한의 권한만 부여하는 ‘최소 권한 원칙’을 준수해야 합니다.
- 데이터 거버넌스: 테이블과 컬럼의 명명 규칙(예: snake_case, 단수형 명사 사용)을 정립하여 협업의 효율을 높여야 합니다. 또한, 법적 규제에 따라 데이터 보관 주기와 파기 정책을 수립하고, 데이터 접근에 대한 감사 로그를 철저히 관리해야 합니다.
이러한 DB 설계 원칙들은 모든 상황에 일괄적으로 적용해야 하는 규칙이 아닙니다. 오히려 “어떤 상황에서 어떤 설계 카드를 선택하는 것이 가장 효과적인가?”를 판단할 수 있는 의사결정의 틀을 제공합니다. 각 기법의 장단점을 이해하고, 현재 비즈니스 상황과 미래의 확장 가능성을 고려하여 최적의 솔루션을 선택하는 감각을 기르는 것이 중요합니다.